1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-09-02 20:46:21 +02:00

Compare commits

...

96 Commits

Author SHA1 Message Date
Laurent Cozic
f5f117cb72 Electron release v1.2.6 2020-10-09 12:17:54 +01:00
Laurent Cozic
fc6da04081 CLI v1.2.3 2020-10-09 12:17:40 +01:00
Laurent Cozic
12ff654986 Merge branch 'release-1.2' of github.com:laurent22/joplin into release-1.2 2020-10-09 12:10:16 +01:00
Laurent Cozic
e852ad846f Electron release v1.2.5 2020-10-09 12:10:06 +01:00
Laurent Cozic
28e00fdf2e Android release v1.2.5 2020-10-08 12:56:12 +01:00
Laurent Cozic
3bd0656eab Android release v1.2.4 2020-10-08 12:51:48 +01:00
Laurent Cozic
e9af71dd76 Android: Reverted app to singleTop launch mode and fixed potential crash when sharing with app 2020-10-08 11:49:39 +01:00
Laurent Cozic
73b33e8e32 Android: Fixes #3800: Simplify initialisation code to prevent sharing
with app to create multiple instance of app and break settings.

Revert "Mobile: Add startup screen to show progress of db migration"

This reverts commit 569355a318.
2020-10-08 11:35:29 +01:00
Laurent Cozic
c2c7efee91 Desktop: Also make toggle button area wider 2020-10-07 21:03:56 +01:00
Laurent Cozic
0836fca822 Merge branch 'release-1.2' of github.com:laurent22/joplin into release-1.2 2020-10-07 20:59:50 +01:00
Laurent Cozic
566df5039c Desktop: Fixes #3876: Notebooks and tags click area was too narrow 2020-10-07 20:58:43 +01:00
Laurent Cozic
559655bf33 Android release v1.2.3 2020-10-06 13:06:48 +01:00
Laurent Cozic
0eab23fbcf Android: Set app launchMode to singleInstance to try to fix lost settings issue 2020-10-06 13:02:41 +01:00
Laurent Cozic
f334f4f487 All: Improved handling of database migration failures 2020-10-06 12:47:33 +01:00
Laurent Cozic
00057da17d Electron release v1.2.4 2020-09-30 08:16:46 +01:00
Laurent Cozic
0a05464013 Desktop: Regression: Context menu on sidebar did not work anymore 2020-09-30 08:16:20 +01:00
Laurent Cozic
9ebb574059 Merge branch 'release-1.2' of github.com:laurent22/joplin into release-1.2 2020-09-29 14:27:33 +01:00
Laurent Cozic
d29c3c2466 Desktop: Regression: Sidebar toggle button did not work anymore 2020-09-29 14:26:05 +01:00
Laurent Cozic
a71f1c19ec Android release v1.2.2 2020-09-29 12:40:46 +01:00
Laurent Cozic
485921d879 CLI v1.2.2 2020-09-29 12:34:42 +01:00
Laurent Cozic
15de7572c0 Electron release v1.2.3 2020-09-29 12:32:24 +01:00
Laurent Cozic
09f41dd50e Desktop: Make global search field wider when it has focus 2020-09-29 12:31:19 +01:00
Laurent Cozic
7b8ee467a0 Desktop: Improved rendering of All Notes item in sidebar 2020-09-29 11:49:51 +01:00
Laurent Cozic
99a496d684 Desktop: Always label "Click to add tags" 2020-09-29 11:33:22 +01:00
Laurent Cozic
f43ee123d8 Tools: Fixed tests 2020-09-29 10:54:31 +01:00
Laurent Cozic
f42fb1b871 Changed tag label 2020-09-29 10:51:47 +01:00
Laurent Cozic
cf2442c5b2 Desktop: Fixes #3835: Prevent crash in rare case when opening the config screen 2020-09-29 08:40:14 +01:00
Laurent Cozic
e0e4735b03 Desktop: Fixes #3754: Refresh search results when searching by tag and when a tag is changed 2020-09-29 08:11:52 +01:00
Laurent Cozic
8bd58c9608 Merge branch 'release-1.2' of github.com:laurent22/joplin into release-1.2 2020-09-28 19:19:52 +01:00
Laurent Cozic
215a725ded Mobile: Fixes #3834: Fixed search highlights 2020-09-28 19:19:21 +01:00
Naveen M V
12c0a05af0 Desktop: Keep search fuzzy scores between 0 and 2 (#3812) 2020-09-28 18:58:19 +01:00
Caleb John
a7fa119041 Desktop: Extend functionality of codemirror vim (#3823)
add swapLine(Up/Down)
have `o` use the more complex list indent
enable sync initializing from vim (and maybe emacs)
split keymap stuff into it's own file
2020-09-28 18:57:17 +01:00
Laurent Cozic
7fb52b8b0e Desktop: Fix issue with highlighted search terms in CodeMirror viewer 2020-09-28 18:44:21 +01:00
Laurent Cozic
3e86ae4a82 Desktop: Disable fuzzy search for now due to performance issues 2020-09-28 18:41:16 +01:00
Laurent Cozic
947d81d96d Desktop: Optimised sidebar rendering speed 2020-09-24 14:30:20 +01:00
Laurent Cozic
6ca640d2ed Desktop: Fix: Fade out checked items in Rich Text editor too 2020-09-23 17:49:25 +01:00
Laurent Cozic
6aca233b21 CLI v1.2.1 2020-09-23 12:16:58 +01:00
Laurent Cozic
2200be697e Cli: Fixed crash due to missing spellfix extension 2020-09-23 12:14:17 +01:00
Laurent Cozic
25ab3c323b Desktop: Fixes #3801: Fixed editor font size 2020-09-23 11:39:36 +01:00
Laurent Cozic
5bf30a9586 Merge branch 'release-1.2' into dev 2020-09-23 10:24:55 +01:00
Laurent Cozic
b6779a8074 Desktop: Fixes #3810: Only disable relevant toolbar buttons when editor is read-only 2020-09-23 10:21:24 +01:00
Caleb John
59599d318c Desktop: Adjust the codemirror code block colors for the dark theme (#3794) 2020-09-23 09:34:39 +01:00
Arda Kılıçdağı
538600fd6c All: Translation: Update tr_TR.po (#3798) 2020-09-22 21:12:31 -04:00
Ji-Hyeon Gim
63264ba471 All: Translation: Update ko.po (#3778)
This patch includes the translation of missing strings, the improvement of the existing translation

Signed-off-by: Ji-Hyeon Gim <potatogim@potatogim.net>
2020-09-22 21:11:55 -04:00
Laurent Cozic
95e7f3df7d Electron release v1.2.2 2020-09-22 16:39:18 +01:00
Laurent Cozic
366fd2a333 Fixed desktop build 2020-09-22 16:38:47 +01:00
Laurent Cozic
5be99a4a16 Merge branch 'release-1.2' of github.com:laurent22/joplin into release-1.2 2020-09-22 16:37:23 +01:00
Laurent Cozic
d86f6a1fbd Tools: Require setting type flag for new Android releases 2020-09-22 16:36:46 +01:00
Laurent Cozic
7d68208cb4 Android release v1.2.1 2020-09-22 16:17:11 +01:00
Laurent Cozic
e9de9d9128 Electron release v1.2.1 2020-09-22 16:02:51 +01:00
Laurent Cozic
1af16d9f0b Tools: Update package locks 2020-09-22 16:02:22 +01:00
Laurent Cozic
8e11eababa Android: Fixes #3797: Disable beta editor even if it was already enabled 2020-09-22 16:01:00 +01:00
Laurent Cozic
4ec9faadd5 Desktop: Disable auto-update by default 2020-09-22 15:41:25 +01:00
Laurent Cozic
5cf462c885 Tools: Increase release version to 1.2 2020-09-22 15:31:32 +01:00
Laurent Cozic
f7ef0a2b1e Tools: Added script to automatically increase major and minor version numbers on new releases 2020-09-22 15:30:20 +01:00
Laurent Cozic
870f55a6c5 Merge branch 'release-1.2' of github.com:laurent22/joplin into release-1.2 2020-09-22 14:37:34 +01:00
Caleb John
7f7e38b434 Desktop, Mobile: Resolves #3740: Upgrade Mermaid to v8.8.0 (#3745)
Co-authored-by: Laurent <laurent22@users.noreply.github.com>
2020-09-22 13:21:35 +01:00
Caleb John
460a07b1a3 Desktop: Fix missed highlighting when using the global search (#3717) 2020-09-22 13:17:51 +01:00
Caleb John
48c9b86d2b Desktop: Fixes #3791: Add stricter rules for katex blocks (#3795) 2020-09-22 13:16:37 +01:00
Caleb John
7202066c1f Desktop: Fix bug where editor would scroll to focus global search (#3787) 2020-09-22 13:12:22 +01:00
Carlos Eduardo
5226f0019b Desktop: Add frequently used languages to markdown editor (#3786) 2020-09-22 13:11:12 +01:00
Laurent Cozic
26ac745419 Deskop, Cli: Fixes #3780: Fixed link generation when exporting to PDF or HTML 2020-09-22 12:56:56 +01:00
Laurent Cozic
b3f2bbee5b Desktop, Cli: Fixes #3760: Improved handling of special characters when exporting to Markdown 2020-09-22 12:06:19 +01:00
Laurent Cozic
56c6cfc785 Update website 2020-09-21 17:53:14 +01:00
Laurent Cozic
1db4932573 Merge branch 'release-1.2' into dev 2020-09-21 17:52:25 +01:00
Laurent Cozic
a2873ebbc5 Merge branch 'release-1.1' into dev 2020-09-21 17:52:19 +01:00
Laurent Cozic
f652011d59 Desktop: Fixes #3748: Fixed issue when switching from search to "All notes" 2020-09-21 17:50:59 +01:00
Laurent Cozic
27c572b2f5 Desktop: Fixes #3700: Disable editor shortcuts when a dialog, such as GotoAnything, is visible 2020-09-21 17:31:25 +01:00
Laurent Cozic
7a4c97618d Desktop: Improved menu enabled states when not in main screen 2020-09-21 17:09:57 +01:00
Laurent Cozic
3ac4fbeee5 Desktop, Mobile: Fixes #3698: Always use light theme for notes in HTML mode 2020-09-21 16:41:24 +01:00
Laurent Cozic
9e05fa553c Desktop: Fixes #3684: Allow Read Time label to be translated 2020-09-21 16:16:28 +01:00
Laurent Cozic
d4f0d2423d CLI v1.1.8 2020-09-21 13:03:33 +01:00
Laurent Cozic
abdd7e3256 Tools: Improved git changelog 2020-09-21 13:01:46 +01:00
Laurent Cozic
f3ea476f27 Merge branch 'release-1.1' of github.com:laurent22/joplin into release-1.1 2020-09-21 12:40:28 +01:00
Laurent Cozic
aa22af443c Tools: Clean up after spellfix build 2020-09-21 12:35:20 +01:00
Laurent Cozic
ce3bd2a47d Tools: Fixed Cli version handling 2020-09-21 12:16:05 +01:00
Laurent Cozic
a9b26246e6 Merge branch 'dev' into release-1.2 2020-09-21 11:56:32 +01:00
Laurent Cozic
cc1e941dd9 Merge branch 'release-1.1' into dev 2020-09-21 11:55:47 +01:00
Laurent Cozic
9610b7e6bd Electron release v1.1.4 2020-09-21 11:42:26 +01:00
Marc BOUVIER
ad85a12535 All: Translation: Update fr_FR.po (#3776) 2020-09-19 13:35:05 -04:00
Ji-Hyeon Gim
b825346829 All: Translation: Update ko.po (#3771)
Update ko.po translations.

Signed-off-by: Ji-Hyeon Gim <potatogim@potatogim.net>
2020-09-19 13:32:37 -04:00
Ettore Atalan
bd4cbaf93d All: Translation: Update de_DE.po (#3770)
This patch includes the translation of missing strings, the improvement of the existing translation and the replacement of Anglicisms by German words.
2020-09-19 13:31:25 -04:00
Laurent Cozic
9af2a19bdf Merge branch 'dev' into release-1.2 2020-09-19 14:22:02 +01:00
Laurent Cozic
d3fa906a9a Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-09-19 14:20:02 +01:00
Gen Neko
22679641ee All: Translation: Update ja_JP.po (#3761) 2020-09-18 01:56:18 -04:00
Laurent Cozic
0ca7457000 Electron release v1.1.3 2020-09-17 10:19:25 +01:00
Laurent Cozic
c84e49c71c All: Fixes #3696: Increased file extension limit to 20 to prevent issue when using external editors 2020-09-17 10:17:45 +01:00
Laurent Cozic
07ab0e986d Merge branch 'release-1.1' of github.com:laurent22/joplin into release-1.1 2020-09-17 10:00:30 +01:00
Laurent Cozic
17957f5da4 Desktop, Cli: Do not prevent export when one item is still encrypted 2020-09-17 10:00:13 +01:00
Naveen M V
a7b5d43e69 Desktop: Fix: Creating a note after backward redirection places it in a wrong notebook (#3759) 2020-09-17 09:32:52 +01:00
Caleb John
38eda3f151 Desktop: Fixes #3749: Use joplin list handling in emacs mode (#3758) 2020-09-17 09:29:19 +01:00
Laurent
056285deda Desktop: UI update (#3586) 2020-09-15 14:01:07 +01:00
Laurent Cozic
bdedf69439 Tools: Remove console statement 2020-09-15 12:13:29 +01:00
Laurent Cozic
c9451d8675 Electron release v1.1.2 2020-09-15 12:12:16 +01:00
Laurent Cozic
c38834b04c Updated French translation 2020-09-15 12:08:49 +01:00
Laurent Cozic
851eee1500 Fixed and simplified translations 2020-09-15 12:08:25 +01:00
216 changed files with 7285 additions and 4351 deletions

View File

@@ -69,8 +69,12 @@ ElectronClient/commands/focusElement.js
ElectronClient/commands/startExternalEditing.js ElectronClient/commands/startExternalEditing.js
ElectronClient/commands/stopExternalEditing.js ElectronClient/commands/stopExternalEditing.js
ElectronClient/global.d.js ElectronClient/global.d.js
ElectronClient/gui/Button/Button.js
ElectronClient/gui/ConfigScreen/ButtonBar.js
ElectronClient/gui/ConfigScreen/ConfigScreen.js
ElectronClient/gui/ConfigScreen/SideBar.js
ElectronClient/gui/DropboxLoginScreen.js
ElectronClient/gui/ErrorBoundary.js ElectronClient/gui/ErrorBoundary.js
ElectronClient/gui/Header/commands/focusSearch.js
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js
ElectronClient/gui/KeymapConfig/ShortcutRecorder.js ElectronClient/gui/KeymapConfig/ShortcutRecorder.js
ElectronClient/gui/KeymapConfig/styles/index.js ElectronClient/gui/KeymapConfig/styles/index.js
@@ -81,8 +85,8 @@ ElectronClient/gui/MainScreen/commands/editAlarm.js
ElectronClient/gui/MainScreen/commands/exportPdf.js ElectronClient/gui/MainScreen/commands/exportPdf.js
ElectronClient/gui/MainScreen/commands/hideModalMessage.js ElectronClient/gui/MainScreen/commands/hideModalMessage.js
ElectronClient/gui/MainScreen/commands/moveToFolder.js ElectronClient/gui/MainScreen/commands/moveToFolder.js
ElectronClient/gui/MainScreen/commands/newFolder.js
ElectronClient/gui/MainScreen/commands/newNote.js ElectronClient/gui/MainScreen/commands/newNote.js
ElectronClient/gui/MainScreen/commands/newNotebook.js
ElectronClient/gui/MainScreen/commands/newTodo.js ElectronClient/gui/MainScreen/commands/newTodo.js
ElectronClient/gui/MainScreen/commands/print.js ElectronClient/gui/MainScreen/commands/print.js
ElectronClient/gui/MainScreen/commands/renameFolder.js ElectronClient/gui/MainScreen/commands/renameFolder.js
@@ -94,9 +98,11 @@ ElectronClient/gui/MainScreen/commands/showModalMessage.js
ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js
ElectronClient/gui/MainScreen/commands/showNoteProperties.js ElectronClient/gui/MainScreen/commands/showNoteProperties.js
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
ElectronClient/gui/MainScreen/commands/toggleEditors.js
ElectronClient/gui/MainScreen/commands/toggleNoteList.js ElectronClient/gui/MainScreen/commands/toggleNoteList.js
ElectronClient/gui/MainScreen/commands/toggleSidebar.js ElectronClient/gui/MainScreen/commands/toggleSidebar.js
ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js
ElectronClient/gui/MainScreen/MainScreen.js
ElectronClient/gui/MultiNoteActions.js ElectronClient/gui/MultiNoteActions.js
ElectronClient/gui/NoteContentPropertiesDialog.js ElectronClient/gui/NoteContentPropertiesDialog.js
ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js
@@ -113,9 +119,11 @@ ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
ElectronClient/gui/NoteEditor/NoteEditor.js ElectronClient/gui/NoteEditor/NoteEditor.js
@@ -125,19 +133,42 @@ ElectronClient/gui/NoteEditor/utils/index.js
ElectronClient/gui/NoteEditor/utils/resourceHandling.js ElectronClient/gui/NoteEditor/utils/resourceHandling.js
ElectronClient/gui/NoteEditor/utils/types.js ElectronClient/gui/NoteEditor/utils/types.js
ElectronClient/gui/NoteEditor/utils/useDropHandler.js ElectronClient/gui/NoteEditor/utils/useDropHandler.js
ElectronClient/gui/NoteEditor/utils/useFolder.js
ElectronClient/gui/NoteEditor/utils/useFormNote.js ElectronClient/gui/NoteEditor/utils/useFormNote.js
ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.js ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.js
ElectronClient/gui/NoteEditor/utils/useMessageHandler.js ElectronClient/gui/NoteEditor/utils/useMessageHandler.js
ElectronClient/gui/NoteEditor/utils/useNoteSearchBar.js ElectronClient/gui/NoteEditor/utils/useNoteSearchBar.js
ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.js
ElectronClient/gui/NoteEditor/utils/useSearchMarkers.js ElectronClient/gui/NoteEditor/utils/useSearchMarkers.js
ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.js ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.js
ElectronClient/gui/NoteList/commands/focusElementNoteList.js ElectronClient/gui/NoteList/commands/focusElementNoteList.js
ElectronClient/gui/NoteList/NoteList.js
ElectronClient/gui/NoteListControls/commands/focusSearch.js
ElectronClient/gui/NoteListControls/NoteListControls.js
ElectronClient/gui/NoteListItem.js ElectronClient/gui/NoteListItem.js
ElectronClient/gui/NoteTextViewer.js
ElectronClient/gui/NoteToolbar/NoteToolbar.js ElectronClient/gui/NoteToolbar/NoteToolbar.js
ElectronClient/gui/OneDriveLoginScreen.js
ElectronClient/gui/ResizableLayout/hooks/useLayoutItemSizes.js
ElectronClient/gui/ResizableLayout/hooks/useWindowResizeEvent.js
ElectronClient/gui/ResizableLayout/ResizableLayout.js
ElectronClient/gui/ResourceScreen.js ElectronClient/gui/ResourceScreen.js
ElectronClient/gui/Root_UpgradeSyncTarget.js ElectronClient/gui/Root_UpgradeSyncTarget.js
ElectronClient/gui/SearchBar/hooks/useSearch.js
ElectronClient/gui/SearchBar/SearchBar.js
ElectronClient/gui/SearchBar/styles/index.js
ElectronClient/gui/ShareNoteDialog.js ElectronClient/gui/ShareNoteDialog.js
ElectronClient/gui/SideBar/commands/focusElementSideBar.js ElectronClient/gui/SideBar/commands/focusElementSideBar.js
ElectronClient/gui/SideBar/SideBar.js
ElectronClient/gui/SideBar/styles/index.js
ElectronClient/gui/StatusScreen/StatusScreen.js
ElectronClient/gui/style/StyledInput.js
ElectronClient/gui/style/StyledTextInput.js
ElectronClient/gui/ToggleEditorsButton/styles/index.js
ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.js
ElectronClient/gui/ToolbarBase.js
ElectronClient/gui/ToolbarButton/styles/index.js
ElectronClient/gui/ToolbarButton/ToolbarButton.js
ReactNativeClient/lib/AsyncActionQueue.js ReactNativeClient/lib/AsyncActionQueue.js
ReactNativeClient/lib/checkPermissions.js ReactNativeClient/lib/checkPermissions.js
ReactNativeClient/lib/commands/historyBackward.js ReactNativeClient/lib/commands/historyBackward.js
@@ -147,6 +178,7 @@ ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js
ReactNativeClient/lib/hooks/useEffectDebugger.js ReactNativeClient/lib/hooks/useEffectDebugger.js
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js
ReactNativeClient/lib/hooks/usePrevious.js ReactNativeClient/lib/hooks/usePrevious.js
ReactNativeClient/lib/hooks/usePropsDebugger.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
@@ -154,6 +186,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/lib/ntpDate.js ReactNativeClient/lib/ntpDate.js
ReactNativeClient/lib/services/CommandService.js ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/debug/populateDatabase.js
ReactNativeClient/lib/services/keychain/KeychainService.js ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
@@ -176,6 +209,16 @@ ReactNativeClient/lib/services/synchronizer/utils/types.js
ReactNativeClient/lib/services/UndoRedoService.js ReactNativeClient/lib/services/UndoRedoService.js
ReactNativeClient/lib/ShareExtension.js ReactNativeClient/lib/ShareExtension.js
ReactNativeClient/lib/shareHandler.js ReactNativeClient/lib/shareHandler.js
ReactNativeClient/lib/theme.js
ReactNativeClient/lib/themes/aritimDark.js
ReactNativeClient/lib/themes/dark.js
ReactNativeClient/lib/themes/dracula.js
ReactNativeClient/lib/themes/light.js
ReactNativeClient/lib/themes/nord.js
ReactNativeClient/lib/themes/oledDark.js
ReactNativeClient/lib/themes/solarizedDark.js
ReactNativeClient/lib/themes/solarizedLight.js
ReactNativeClient/lib/themes/type.js
ReactNativeClient/lib/versionInfo.js ReactNativeClient/lib/versionInfo.js
ReactNativeClient/PluginAssetsLoader.js ReactNativeClient/PluginAssetsLoader.js
ReactNativeClient/setUpQuickActions.js ReactNativeClient/setUpQuickActions.js

51
.gitignore vendored
View File

@@ -50,8 +50,8 @@ joplin-webclipper-source.zip
Tools/commit_hook.txt Tools/commit_hook.txt
.vscode/* .vscode/*
*.map *.map
ReactNativeClient/lib/sql-extensions/ ReactNativeClient/lib/sql-extensions/spellfix.so
!ReactNativeClient/lib/sql-extensions/spellfix.dll ReactNativeClient/lib/sql-extensions/spellfix.dylib
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
CliClient/app/LinkSelector.js CliClient/app/LinkSelector.js
@@ -62,8 +62,12 @@ ElectronClient/commands/focusElement.js
ElectronClient/commands/startExternalEditing.js ElectronClient/commands/startExternalEditing.js
ElectronClient/commands/stopExternalEditing.js ElectronClient/commands/stopExternalEditing.js
ElectronClient/global.d.js ElectronClient/global.d.js
ElectronClient/gui/Button/Button.js
ElectronClient/gui/ConfigScreen/ButtonBar.js
ElectronClient/gui/ConfigScreen/ConfigScreen.js
ElectronClient/gui/ConfigScreen/SideBar.js
ElectronClient/gui/DropboxLoginScreen.js
ElectronClient/gui/ErrorBoundary.js ElectronClient/gui/ErrorBoundary.js
ElectronClient/gui/Header/commands/focusSearch.js
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js
ElectronClient/gui/KeymapConfig/ShortcutRecorder.js ElectronClient/gui/KeymapConfig/ShortcutRecorder.js
ElectronClient/gui/KeymapConfig/styles/index.js ElectronClient/gui/KeymapConfig/styles/index.js
@@ -74,8 +78,8 @@ ElectronClient/gui/MainScreen/commands/editAlarm.js
ElectronClient/gui/MainScreen/commands/exportPdf.js ElectronClient/gui/MainScreen/commands/exportPdf.js
ElectronClient/gui/MainScreen/commands/hideModalMessage.js ElectronClient/gui/MainScreen/commands/hideModalMessage.js
ElectronClient/gui/MainScreen/commands/moveToFolder.js ElectronClient/gui/MainScreen/commands/moveToFolder.js
ElectronClient/gui/MainScreen/commands/newFolder.js
ElectronClient/gui/MainScreen/commands/newNote.js ElectronClient/gui/MainScreen/commands/newNote.js
ElectronClient/gui/MainScreen/commands/newNotebook.js
ElectronClient/gui/MainScreen/commands/newTodo.js ElectronClient/gui/MainScreen/commands/newTodo.js
ElectronClient/gui/MainScreen/commands/print.js ElectronClient/gui/MainScreen/commands/print.js
ElectronClient/gui/MainScreen/commands/renameFolder.js ElectronClient/gui/MainScreen/commands/renameFolder.js
@@ -87,9 +91,11 @@ ElectronClient/gui/MainScreen/commands/showModalMessage.js
ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js
ElectronClient/gui/MainScreen/commands/showNoteProperties.js ElectronClient/gui/MainScreen/commands/showNoteProperties.js
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
ElectronClient/gui/MainScreen/commands/toggleEditors.js
ElectronClient/gui/MainScreen/commands/toggleNoteList.js ElectronClient/gui/MainScreen/commands/toggleNoteList.js
ElectronClient/gui/MainScreen/commands/toggleSidebar.js ElectronClient/gui/MainScreen/commands/toggleSidebar.js
ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js
ElectronClient/gui/MainScreen/MainScreen.js
ElectronClient/gui/MultiNoteActions.js ElectronClient/gui/MultiNoteActions.js
ElectronClient/gui/NoteContentPropertiesDialog.js ElectronClient/gui/NoteContentPropertiesDialog.js
ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js
@@ -106,9 +112,11 @@ ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
ElectronClient/gui/NoteEditor/NoteEditor.js ElectronClient/gui/NoteEditor/NoteEditor.js
@@ -118,19 +126,42 @@ ElectronClient/gui/NoteEditor/utils/index.js
ElectronClient/gui/NoteEditor/utils/resourceHandling.js ElectronClient/gui/NoteEditor/utils/resourceHandling.js
ElectronClient/gui/NoteEditor/utils/types.js ElectronClient/gui/NoteEditor/utils/types.js
ElectronClient/gui/NoteEditor/utils/useDropHandler.js ElectronClient/gui/NoteEditor/utils/useDropHandler.js
ElectronClient/gui/NoteEditor/utils/useFolder.js
ElectronClient/gui/NoteEditor/utils/useFormNote.js ElectronClient/gui/NoteEditor/utils/useFormNote.js
ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.js ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.js
ElectronClient/gui/NoteEditor/utils/useMessageHandler.js ElectronClient/gui/NoteEditor/utils/useMessageHandler.js
ElectronClient/gui/NoteEditor/utils/useNoteSearchBar.js ElectronClient/gui/NoteEditor/utils/useNoteSearchBar.js
ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.js
ElectronClient/gui/NoteEditor/utils/useSearchMarkers.js ElectronClient/gui/NoteEditor/utils/useSearchMarkers.js
ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.js ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.js
ElectronClient/gui/NoteList/commands/focusElementNoteList.js ElectronClient/gui/NoteList/commands/focusElementNoteList.js
ElectronClient/gui/NoteList/NoteList.js
ElectronClient/gui/NoteListControls/commands/focusSearch.js
ElectronClient/gui/NoteListControls/NoteListControls.js
ElectronClient/gui/NoteListItem.js ElectronClient/gui/NoteListItem.js
ElectronClient/gui/NoteTextViewer.js
ElectronClient/gui/NoteToolbar/NoteToolbar.js ElectronClient/gui/NoteToolbar/NoteToolbar.js
ElectronClient/gui/OneDriveLoginScreen.js
ElectronClient/gui/ResizableLayout/hooks/useLayoutItemSizes.js
ElectronClient/gui/ResizableLayout/hooks/useWindowResizeEvent.js
ElectronClient/gui/ResizableLayout/ResizableLayout.js
ElectronClient/gui/ResourceScreen.js ElectronClient/gui/ResourceScreen.js
ElectronClient/gui/Root_UpgradeSyncTarget.js ElectronClient/gui/Root_UpgradeSyncTarget.js
ElectronClient/gui/SearchBar/hooks/useSearch.js
ElectronClient/gui/SearchBar/SearchBar.js
ElectronClient/gui/SearchBar/styles/index.js
ElectronClient/gui/ShareNoteDialog.js ElectronClient/gui/ShareNoteDialog.js
ElectronClient/gui/SideBar/commands/focusElementSideBar.js ElectronClient/gui/SideBar/commands/focusElementSideBar.js
ElectronClient/gui/SideBar/SideBar.js
ElectronClient/gui/SideBar/styles/index.js
ElectronClient/gui/StatusScreen/StatusScreen.js
ElectronClient/gui/style/StyledInput.js
ElectronClient/gui/style/StyledTextInput.js
ElectronClient/gui/ToggleEditorsButton/styles/index.js
ElectronClient/gui/ToggleEditorsButton/ToggleEditorsButton.js
ElectronClient/gui/ToolbarBase.js
ElectronClient/gui/ToolbarButton/styles/index.js
ElectronClient/gui/ToolbarButton/ToolbarButton.js
ReactNativeClient/lib/AsyncActionQueue.js ReactNativeClient/lib/AsyncActionQueue.js
ReactNativeClient/lib/checkPermissions.js ReactNativeClient/lib/checkPermissions.js
ReactNativeClient/lib/commands/historyBackward.js ReactNativeClient/lib/commands/historyBackward.js
@@ -140,6 +171,7 @@ ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js
ReactNativeClient/lib/hooks/useEffectDebugger.js ReactNativeClient/lib/hooks/useEffectDebugger.js
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js
ReactNativeClient/lib/hooks/usePrevious.js ReactNativeClient/lib/hooks/usePrevious.js
ReactNativeClient/lib/hooks/usePropsDebugger.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
@@ -147,6 +179,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/lib/ntpDate.js ReactNativeClient/lib/ntpDate.js
ReactNativeClient/lib/services/CommandService.js ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/debug/populateDatabase.js
ReactNativeClient/lib/services/keychain/KeychainService.js ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
@@ -169,6 +202,16 @@ ReactNativeClient/lib/services/synchronizer/utils/types.js
ReactNativeClient/lib/services/UndoRedoService.js ReactNativeClient/lib/services/UndoRedoService.js
ReactNativeClient/lib/ShareExtension.js ReactNativeClient/lib/ShareExtension.js
ReactNativeClient/lib/shareHandler.js ReactNativeClient/lib/shareHandler.js
ReactNativeClient/lib/theme.js
ReactNativeClient/lib/themes/aritimDark.js
ReactNativeClient/lib/themes/dark.js
ReactNativeClient/lib/themes/dracula.js
ReactNativeClient/lib/themes/light.js
ReactNativeClient/lib/themes/nord.js
ReactNativeClient/lib/themes/oledDark.js
ReactNativeClient/lib/themes/solarizedDark.js
ReactNativeClient/lib/themes/solarizedLight.js
ReactNativeClient/lib/themes/type.js
ReactNativeClient/lib/versionInfo.js ReactNativeClient/lib/versionInfo.js
ReactNativeClient/PluginAssetsLoader.js ReactNativeClient/PluginAssetsLoader.js
ReactNativeClient/setUpQuickActions.js ReactNativeClient/setUpQuickActions.js

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -11,6 +11,7 @@ Note that all the applications share the same library, which, for historical rea
- macOS, Linux: Install rsync - https://nodejs.org/en/ - macOS, Linux: Install rsync - https://nodejs.org/en/
- macOS: Install Cocoapods - `brew install cocoapods` - macOS: Install Cocoapods - `brew install cocoapods`
- Windows: Install Windows Build Tools - `npm install -g windows-build-tools` - Windows: Install Windows Build Tools - `npm install -g windows-build-tools`
- Linux: Install dependencies - `sudo apt install libnss3 libsecret-1-dev`
## Building ## Building
@@ -25,6 +26,8 @@ Then you can test the various applications:
cd ElectronClient cd ElectronClient
npm start npm start
You can also run it under WSL 2. To do so, [follow these instructions](https://www.beekeeperstudio.io/blog/building-electron-windows-ubuntu-wsl2) to setup your environment.
## Testing the Terminal application ## Testing the Terminal application
cd CliClient cd CliClient

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,10 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.3.1\n" "X-Generator: Poedit 2.4.1\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
#: CliClient/app/command-cp.js:13 #: CliClient/app/command-cp.js:13
msgid "" msgid ""
@@ -104,9 +106,9 @@ msgid "Do not ask for confirmation."
msgstr "Ne pas demander de confirmation." msgstr "Ne pas demander de confirmation."
#: CliClient/app/command-import.js:27 #: CliClient/app/command-import.js:27
#, fuzzy, javascript-format #, javascript-format
msgid "Output format: %s" msgid "Output format: %s"
msgstr "Format de la source : %s" msgstr "Format de la sortie : %s"
#: CliClient/app/command-import.js:47 ElectronClient/gui/ImportScreen.min.js:69 #: CliClient/app/command-import.js:47 ElectronClient/gui/ImportScreen.min.js:69
#, javascript-format #, javascript-format
@@ -317,7 +319,7 @@ msgstr ""
#: CliClient/app/command-sync.js:35 #: CliClient/app/command-sync.js:35
msgid "Upgrade the sync target to the latest version." msgid "Upgrade the sync target to the latest version."
msgstr "" msgstr "Mettre à jour la cible de synchronisation."
#: CliClient/app/command-sync.js:81 CliClient/app/command-sync.js:95 #: CliClient/app/command-sync.js:81 CliClient/app/command-sync.js:95
#: ElectronClient/gui/OneDriveLoginScreen.min.js:40 #: ElectronClient/gui/OneDriveLoginScreen.min.js:40
@@ -376,7 +378,6 @@ msgid "Synchronisation target: %s (%s)"
msgstr "Cible de la synchronisation : %s (%s)" msgstr "Cible de la synchronisation : %s (%s)"
#: CliClient/app/command-sync.js:177 #: CliClient/app/command-sync.js:177
#, fuzzy
msgid "Cannot initialise synchroniser." msgid "Cannot initialise synchroniser."
msgstr "Impossible d'initialiser la synchronisation." msgstr "Impossible d'initialiser la synchronisation."
@@ -813,14 +814,16 @@ msgstr "Annuler"
msgid "" msgid ""
"The app is now going to close. Please relaunch it to complete the process." "The app is now going to close. Please relaunch it to complete the process."
msgstr "" msgstr ""
"L'application va maintenance fermer. Veuillez la relancer pour terminer "
"l'opération."
#: ElectronClient/plugins/GotoAnything.min.js:446 #: ElectronClient/plugins/GotoAnything.min.js:446
msgid "" msgid ""
"Type a note title or part of its content to jump to it. Or type # followed " "Type a note title or part of its content to jump to it. Or type # followed "
"by a tag name, or @ followed by a notebook name." "by a tag name, or @ followed by a notebook name."
msgstr "" msgstr ""
"Entrez le titre d’une note, ou entrez # suivit du nom d’une étiquette, ou @ " "Entrez le titre d’une note, ou entrez # suivi du nom d’une étiquette, ou @ "
"suivit du nom d’un carnet." "suivi du nom d’un carnet."
#: ElectronClient/plugins/GotoAnything.min.js:486 #: ElectronClient/plugins/GotoAnything.min.js:486
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:20 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:20
@@ -1021,7 +1024,6 @@ msgid "strong text"
msgstr "texte en gras" msgstr "texte en gras"
#: ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:147 #: ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:147
#, fuzzy
msgid "emphasised text" msgid "emphasised text"
msgstr "texte en italique" msgstr "texte en italique"
@@ -1176,13 +1178,14 @@ msgstr "Création de %s..."
#: ElectronClient/gui/NoteEditor/NoteEditor.js:344 #: ElectronClient/gui/NoteEditor/NoteEditor.js:344
msgid "The following attachments are being watched for changes:" msgid "The following attachments are being watched for changes:"
msgstr "" msgstr "Les changements sur les éléments suivants sont monitorés :"
#: ElectronClient/gui/NoteEditor/NoteEditor.js:347 #: ElectronClient/gui/NoteEditor/NoteEditor.js:347
msgid "" msgid ""
"The attachments will no longer be watched when you switch to a different " "The attachments will no longer be watched when you switch to a different "
"note." "note."
msgstr "" msgstr ""
"Les pièces jointes ne seront plus monitorées lorsque vous changerez de note."
#: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:25 #: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:25
msgid "Select all" msgid "Select all"
@@ -1308,12 +1311,11 @@ msgstr "Importer"
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:125 #: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:125
msgid "Command" msgid "Command"
msgstr "" msgstr "Commande"
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:126 #: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:126
#, fuzzy
msgid "Keyboard Shortcut" msgid "Keyboard Shortcut"
msgstr "Mode de clavier" msgstr "Raccourci"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:14 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:14
#: ElectronClient/app.js:690 #: ElectronClient/app.js:690
@@ -1336,9 +1338,8 @@ msgid "Website and documentation"
msgstr "Documentation en ligne" msgstr "Documentation en ligne"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:24 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:24
#, fuzzy
msgid "Hide Joplin" msgid "Hide Joplin"
msgstr "A propos de Joplin" msgstr "Cacher Joplin"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:26 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:26
#: ElectronClient/app.js:703 #: ElectronClient/app.js:703
@@ -1346,9 +1347,8 @@ msgid "Close Window"
msgstr "Fermer la fenêtre" msgstr "Fermer la fenêtre"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28
#, fuzzy
msgid "Preferences" msgid "Preferences"
msgstr "Préférences" msgstr "Préférences"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28
#: ElectronClient/gui/Root.min.js:92 ElectronClient/app.js:572 #: ElectronClient/gui/Root.min.js:92 ElectronClient/app.js:572
@@ -1357,13 +1357,15 @@ msgstr "Options"
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48 #: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48
msgid "Press the shortcut" msgid "Press the shortcut"
msgstr "" msgstr "Taper le raccourci"
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48 #: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48
msgid "" msgid ""
"Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the " "Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the "
"shortcut." "shortcut."
msgstr "" msgstr ""
"Tapez le raccourci et appuyez sur ENTREE. Ou, appuyez sur la touche de "
"retour arrière pour supprimer le raccourci."
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:49 #: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:49
#: ElectronClient/gui/EncryptionConfigScreen.min.js:95 #: ElectronClient/gui/EncryptionConfigScreen.min.js:95
@@ -1377,11 +1379,13 @@ msgid ""
"may take a few minutes to complete and the app needs to be restarted. To " "may take a few minutes to complete and the app needs to be restarted. To "
"proceed please click on the link." "proceed please click on the link."
msgstr "" msgstr ""
"La cible de synchronisation doit être mise à jour. L'opération peut prendre "
"plusieurs minutes et l'application devra être re-démarrée. Pour continuer, "
"veuillez cliquer sur le lien."
#: ElectronClient/gui/MainScreen/MainScreen.min.js:306 #: ElectronClient/gui/MainScreen/MainScreen.min.js:306
#, fuzzy
msgid "Restart and upgrade" msgid "Restart and upgrade"
msgstr "Clefs maîtres qui peuvent être mise à niveau" msgstr "Redémarrer et mettre à jour"
#: ElectronClient/gui/MainScreen/MainScreen.min.js:313 #: ElectronClient/gui/MainScreen/MainScreen.min.js:313
msgid "Some items cannot be synchronised." msgid "Some items cannot be synchronised."
@@ -1406,12 +1410,10 @@ msgid "Set the password"
msgstr "Définir le mot de passe" msgstr "Définir le mot de passe"
#: ElectronClient/gui/MainScreen/MainScreen.min.js:349 #: ElectronClient/gui/MainScreen/MainScreen.min.js:349
#, fuzzy
msgid "One of your master keys use an obsolete encryption method." msgid "One of your master keys use an obsolete encryption method."
msgstr "L'une des clefs maîtres requiert un mot de passe." msgstr "L'une des clefs maîtres utilise une méthode de chiffrement obsolète."
#: ElectronClient/gui/MainScreen/MainScreen.min.js:361 #: ElectronClient/gui/MainScreen/MainScreen.min.js:361
#, fuzzy
msgid "" msgid ""
"The default encryption method has been changed, you should re-encrypt your " "The default encryption method has been changed, you should re-encrypt your "
"data." "data."
@@ -1420,7 +1422,6 @@ msgstr ""
"l'appliquer à vos données." "l'appliquer à vos données."
#: ElectronClient/gui/MainScreen/MainScreen.min.js:366 #: ElectronClient/gui/MainScreen/MainScreen.min.js:366
#, fuzzy
msgid "More info" msgid "More info"
msgstr "Plus d'information" msgstr "Plus d'information"
@@ -1761,9 +1762,8 @@ msgid "Icon"
msgstr "Icône" msgstr "Icône"
#: ElectronClient/gui/FolderPropertiesDialog.min.js:272 #: ElectronClient/gui/FolderPropertiesDialog.min.js:272
#, fuzzy
msgid "Notebook properties" msgid "Notebook properties"
msgstr "Propriétés de la note" msgstr "Propriétés du carnet"
#: ElectronClient/gui/NoteText.min.js:781 #: ElectronClient/gui/NoteText.min.js:781
#, javascript-format #, javascript-format
@@ -1936,7 +1936,7 @@ msgstr "Statistiques"
#: ElectronClient/gui/NoteContentPropertiesDialog.js:111 #: ElectronClient/gui/NoteContentPropertiesDialog.js:111
#, javascript-format #, javascript-format
msgid "Read time: %s min" msgid "Read time: %s min"
msgstr "" msgstr "Temps de lecture : %s min"
#: ElectronClient/gui/NoteContentPropertiesDialog.js:112 #: ElectronClient/gui/NoteContentPropertiesDialog.js:112
#: ElectronClient/gui/ShareNoteDialog.js:175 #: ElectronClient/gui/ShareNoteDialog.js:175
@@ -2151,9 +2151,8 @@ msgid "Templates"
msgstr "Modèles" msgstr "Modèles"
#: ElectronClient/app.js:668 #: ElectronClient/app.js:668
#, fuzzy
msgid "Export all" msgid "Export all"
msgstr "Exporter" msgstr "Tout exporter"
#: ElectronClient/app.js:683 #: ElectronClient/app.js:683
#, javascript-format #, javascript-format
@@ -2253,7 +2252,7 @@ msgstr "Paramètre inconnu : %s"
#: ReactNativeClient/lib/SyncTargetAmazonS3.js:28 #: ReactNativeClient/lib/SyncTargetAmazonS3.js:28
msgid "AWS S3" msgid "AWS S3"
msgstr "" msgstr "AWS S3"
#: ReactNativeClient/lib/SyncTargetDropbox.js:25 #: ReactNativeClient/lib/SyncTargetDropbox.js:25
msgid "Dropbox" msgid "Dropbox"
@@ -2397,15 +2396,15 @@ msgstr "WebDAV : Mot de passe"
#: ReactNativeClient/lib/models/Setting.js:195 #: ReactNativeClient/lib/models/Setting.js:195
msgid "AWS S3 bucket" msgid "AWS S3 bucket"
msgstr "" msgstr "AWS S3: bucket"
#: ReactNativeClient/lib/models/Setting.js:206 #: ReactNativeClient/lib/models/Setting.js:206
msgid "AWS key" msgid "AWS key"
msgstr "" msgstr "AWS : Clef"
#: ReactNativeClient/lib/models/Setting.js:216 #: ReactNativeClient/lib/models/Setting.js:216
msgid "AWS secret" msgid "AWS secret"
msgstr "" msgstr "AWS : Secret"
#: ReactNativeClient/lib/models/Setting.js:230 #: ReactNativeClient/lib/models/Setting.js:230
msgid "Attachment download behaviour" msgid "Attachment download behaviour"
@@ -2626,7 +2625,6 @@ msgid "Editor font family"
msgstr "Police de l'éditeur" msgstr "Police de l'éditeur"
#: ReactNativeClient/lib/models/Setting.js:553 #: ReactNativeClient/lib/models/Setting.js:553
#, fuzzy
msgid "" msgid ""
"This should be a *monospace* font or some elements will render incorrectly. " "This should be a *monospace* font or some elements will render incorrectly. "
"If the font is incorrect or empty, it will default to a generic monospace " "If the font is incorrect or empty, it will default to a generic monospace "
@@ -2867,9 +2865,8 @@ msgid "Web Clipper"
msgstr "Web Clipper" msgstr "Web Clipper"
#: ReactNativeClient/lib/models/Setting.js:1259 #: ReactNativeClient/lib/models/Setting.js:1259
#, fuzzy
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "Mode de clavier" msgstr "Raccourcis clavier"
#: ReactNativeClient/lib/models/Setting.js:1264 #: ReactNativeClient/lib/models/Setting.js:1264
msgid "" msgid ""
@@ -3025,6 +3022,8 @@ msgstr "Certains objets ne peuvent être synchronisés."
#: ReactNativeClient/lib/components/screen-header.js:453 #: ReactNativeClient/lib/components/screen-header.js:453
msgid "The sync target needs to be upgraded. Press this banner to proceed." msgid "The sync target needs to be upgraded. Press this banner to proceed."
msgstr "" msgstr ""
"La cible de synchronisation doit être mise à jour. Veuillez appuyer sur "
"cette bannière pour commencer."
#: ReactNativeClient/lib/components/side-menu-content.js:126 #: ReactNativeClient/lib/components/side-menu-content.js:126
#, javascript-format #, javascript-format
@@ -3151,7 +3150,7 @@ msgstr "Rafraîchir"
#: ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js:42 #: ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js:42
msgid "Sync Target Upgrade" msgid "Sync Target Upgrade"
msgstr "" msgstr "Mise à jour de la cible de synchro"
#: ReactNativeClient/lib/components/screens/NoteTagsDialog.js:163 #: ReactNativeClient/lib/components/screens/NoteTagsDialog.js:163
msgid "New tags:" msgid "New tags:"
@@ -3553,7 +3552,6 @@ msgid "Forward"
msgstr "Vers l'avant" msgstr "Vers l'avant"
#: ReactNativeClient/lib/commands/synchronize.js:17 #: ReactNativeClient/lib/commands/synchronize.js:17
#, fuzzy
msgid "Synchronize" msgid "Synchronize"
msgstr "Synchroniser" msgstr "Synchroniser"
@@ -3793,9 +3791,11 @@ msgid "Directory"
msgstr "Dossier" msgstr "Dossier"
#: ReactNativeClient/lib/services/InteropService.js:174 #: ReactNativeClient/lib/services/InteropService.js:174
#, fuzzy, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\" and output \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\" and output \"%s\""
msgstr "Impossible de charger module \"%s\" pour le format \"%s\"" msgstr ""
"Impossible de charger module \"%s\" pour le format d'entrée \"%s\" et de "
"sortie \"%s\""
#: ReactNativeClient/lib/services/InteropService.js:232 #: ReactNativeClient/lib/services/InteropService.js:232
#, javascript-format #, javascript-format

View File

@@ -15,6 +15,8 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.4\n" "X-Generator: Poedit 1.8.4\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
#: CliClient/app/command-cp.js:13 #: CliClient/app/command-cp.js:13
msgid "" msgid ""
@@ -312,7 +314,7 @@ msgstr "指定のターゲットと同期します。(標準: sync.targetの
#: CliClient/app/command-sync.js:35 #: CliClient/app/command-sync.js:35
msgid "Upgrade the sync target to the latest version." msgid "Upgrade the sync target to the latest version."
msgstr "" msgstr "同期先を最新バージョンにアップグレード。"
#: CliClient/app/command-sync.js:81 CliClient/app/command-sync.js:95 #: CliClient/app/command-sync.js:81 CliClient/app/command-sync.js:95
#: ElectronClient/gui/OneDriveLoginScreen.min.js:40 #: ElectronClient/gui/OneDriveLoginScreen.min.js:40
@@ -796,6 +798,8 @@ msgstr "キャンセル"
msgid "" msgid ""
"The app is now going to close. Please relaunch it to complete the process." "The app is now going to close. Please relaunch it to complete the process."
msgstr "" msgstr ""
"まもなくアプリケーションは終了します。もう一度起動して処理を完了させてくださ"
"い。"
#: ElectronClient/plugins/GotoAnything.min.js:446 #: ElectronClient/plugins/GotoAnything.min.js:446
msgid "" msgid ""
@@ -1157,13 +1161,13 @@ msgstr "新しい %s を作成中..."
#: ElectronClient/gui/NoteEditor/NoteEditor.js:344 #: ElectronClient/gui/NoteEditor/NoteEditor.js:344
msgid "The following attachments are being watched for changes:" msgid "The following attachments are being watched for changes:"
msgstr "" msgstr "下記の添付ファイルが変更されたかどうかを監視中です。"
#: ElectronClient/gui/NoteEditor/NoteEditor.js:347 #: ElectronClient/gui/NoteEditor/NoteEditor.js:347
msgid "" msgid ""
"The attachments will no longer be watched when you switch to a different " "The attachments will no longer be watched when you switch to a different "
"note." "note."
msgstr "" msgstr "添付ファイルの監視は他のノートに移動すると終了します。"
#: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:25 #: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:25
msgid "Select all" msgid "Select all"
@@ -1274,7 +1278,7 @@ msgstr "ノートのプロパティ"
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:62 #: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:62
msgid "An unexpected error occured while importing the keymap!" msgid "An unexpected error occured while importing the keymap!"
msgstr "" msgstr "キーマップのインポート中に予期しないエラーが発生しました!"
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:119 #: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:119
#: ElectronClient/gui/MainScreen/MainScreen.min.js:437 #: ElectronClient/gui/MainScreen/MainScreen.min.js:437
@@ -1289,12 +1293,11 @@ msgstr "インポート"
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:125 #: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:125
msgid "Command" msgid "Command"
msgstr "" msgstr "コマンド"
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:126 #: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:126
#, fuzzy
msgid "Keyboard Shortcut" msgid "Keyboard Shortcut"
msgstr "キーバインド" msgstr "ショートカットキー"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:14 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:14
#: ElectronClient/app.js:690 #: ElectronClient/app.js:690
@@ -1317,9 +1320,8 @@ msgid "Website and documentation"
msgstr "Webサイトとドキュメント" msgstr "Webサイトとドキュメント"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:24 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:24
#, fuzzy
msgid "Hide Joplin" msgid "Hide Joplin"
msgstr "Joplinについて" msgstr "Joplinを隠す"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:26 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:26
#: ElectronClient/app.js:703 #: ElectronClient/app.js:703
@@ -1327,7 +1329,6 @@ msgid "Close Window"
msgstr "ウィンドウを閉じる" msgstr "ウィンドウを閉じる"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28
#, fuzzy
msgid "Preferences" msgid "Preferences"
msgstr "環境設定" msgstr "環境設定"
@@ -1338,13 +1339,15 @@ msgstr "オプション"
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48 #: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48
msgid "Press the shortcut" msgid "Press the shortcut"
msgstr "" msgstr "ショートカットキーを押してください"
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48 #: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48
msgid "" msgid ""
"Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the " "Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the "
"shortcut." "shortcut."
msgstr "" msgstr ""
"ショートカットキーに続けてENTERを押すことで設定します。ショートカットを削除す"
"るにはBACKSPACEを押してください。"
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:49 #: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:49
#: ElectronClient/gui/EncryptionConfigScreen.min.js:95 #: ElectronClient/gui/EncryptionConfigScreen.min.js:95
@@ -1358,11 +1361,13 @@ msgid ""
"may take a few minutes to complete and the app needs to be restarted. To " "may take a few minutes to complete and the app needs to be restarted. To "
"proceed please click on the link." "proceed please click on the link."
msgstr "" msgstr ""
"同期するには同期先をアップグレードする必要があります。アップグレードには数分"
"かかるかもしれません。またアプリケーションの再起動が必要です。アップグレード"
"するにはリンクをクリックしてください。"
#: ElectronClient/gui/MainScreen/MainScreen.min.js:306 #: ElectronClient/gui/MainScreen/MainScreen.min.js:306
#, fuzzy
msgid "Restart and upgrade" msgid "Restart and upgrade"
msgstr "アップグレードが必要なマスターキー" msgstr "再起動してアップグレード"
#: ElectronClient/gui/MainScreen/MainScreen.min.js:313 #: ElectronClient/gui/MainScreen/MainScreen.min.js:313
msgid "Some items cannot be synchronised." msgid "Some items cannot be synchronised."
@@ -2114,9 +2119,8 @@ msgid "Templates"
msgstr "テンプレート" msgstr "テンプレート"
#: ElectronClient/app.js:668 #: ElectronClient/app.js:668
#, fuzzy
msgid "Export all" msgid "Export all"
msgstr "エクスポート" msgstr "すべてをエクスポート"
#: ElectronClient/app.js:683 #: ElectronClient/app.js:683
#, javascript-format #, javascript-format
@@ -2215,7 +2219,7 @@ msgstr "不明なレベルID: %s"
#: ReactNativeClient/lib/SyncTargetAmazonS3.js:28 #: ReactNativeClient/lib/SyncTargetAmazonS3.js:28
msgid "AWS S3" msgid "AWS S3"
msgstr "" msgstr "AWS S3"
#: ReactNativeClient/lib/SyncTargetDropbox.js:25 #: ReactNativeClient/lib/SyncTargetDropbox.js:25
msgid "Dropbox" msgid "Dropbox"
@@ -2360,15 +2364,15 @@ msgstr "WevDAV パスワード"
#: ReactNativeClient/lib/models/Setting.js:195 #: ReactNativeClient/lib/models/Setting.js:195
msgid "AWS S3 bucket" msgid "AWS S3 bucket"
msgstr "" msgstr "AWS S3 バケット"
#: ReactNativeClient/lib/models/Setting.js:206 #: ReactNativeClient/lib/models/Setting.js:206
msgid "AWS key" msgid "AWS key"
msgstr "" msgstr "AWS アクセスキーID"
#: ReactNativeClient/lib/models/Setting.js:216 #: ReactNativeClient/lib/models/Setting.js:216
msgid "AWS secret" msgid "AWS secret"
msgstr "" msgstr "AWS シークレットアクセスキー"
#: ReactNativeClient/lib/models/Setting.js:230 #: ReactNativeClient/lib/models/Setting.js:230
msgid "Attachment download behaviour" msgid "Attachment download behaviour"
@@ -2823,9 +2827,8 @@ msgid "Web Clipper"
msgstr "Webクリッパー" msgstr "Webクリッパー"
#: ReactNativeClient/lib/models/Setting.js:1259 #: ReactNativeClient/lib/models/Setting.js:1259
#, fuzzy
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "キーバインド" msgstr "キーボードショートカット"
#: ReactNativeClient/lib/models/Setting.js:1264 #: ReactNativeClient/lib/models/Setting.js:1264
msgid "" msgid ""
@@ -2980,6 +2983,8 @@ msgstr "いくつかの項目は同期されませんでした。詳細はクリ
#: ReactNativeClient/lib/components/screen-header.js:453 #: ReactNativeClient/lib/components/screen-header.js:453
msgid "The sync target needs to be upgraded. Press this banner to proceed." msgid "The sync target needs to be upgraded. Press this banner to proceed."
msgstr "" msgstr ""
"同期先をアップグレードする必要があります。アップグレードするにはバナーをク"
"リックしてください。"
#: ReactNativeClient/lib/components/side-menu-content.js:126 #: ReactNativeClient/lib/components/side-menu-content.js:126
#, javascript-format #, javascript-format
@@ -3024,8 +3029,8 @@ msgid ""
"Error. Please check that URL, username, password, etc. are correct and that " "Error. Please check that URL, username, password, etc. are correct and that "
"the sync target is accessible. The reported error was:" "the sync target is accessible. The reported error was:"
msgstr "" msgstr ""
"エラーです。URL、ユーザー名、パスワードなどを修正し、同期するターゲットにアク" "エラーです。URL、ユーザー名、パスワードなどを修正し、同期先にアクセスできるか"
"セスできるかを確認してください。次が報告されたエラーです:" "を確認してください。次が報告されたエラーです:"
#: ReactNativeClient/lib/components/shared/dropbox-login-shared.js:39 #: ReactNativeClient/lib/components/shared/dropbox-login-shared.js:39
msgid "The application has been authorised!" msgid "The application has been authorised!"
@@ -3102,7 +3107,7 @@ msgstr "更新"
#: ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js:42 #: ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js:42
msgid "Sync Target Upgrade" msgid "Sync Target Upgrade"
msgstr "" msgstr "同期先のアップグレード"
#: ReactNativeClient/lib/components/screens/NoteTagsDialog.js:163 #: ReactNativeClient/lib/components/screens/NoteTagsDialog.js:163
msgid "New tags:" msgid "New tags:"
@@ -3151,10 +3156,10 @@ msgid ""
"password as, for security purposes, this will be the *only* way to decrypt " "password as, for security purposes, this will be the *only* way to decrypt "
"the data! To enable encryption, please enter your password below." "the data! To enable encryption, please enter your password below."
msgstr "" msgstr ""
"暗号化を有効にするとは、*すべて*のノートや添付ファイルを再同期し、同期ター" "暗号化を有効にするとは、*すべて*のノートや添付ファイルを再同期し、同期先に暗"
"ゲットに暗号化した状態で送ることを意味します。パスワードはなくさないようにし" "号化した状態で送ることを意味します。パスワードはなくさないようにしてくださ"
"てください。セキュリティ上、このパスワードがデータを復号する*唯一*の方法にな" "い。セキュリティ上、このパスワードがデータを復号する*唯一*の方法になるためで"
"るためです! 暗号化を有効にするには、下にパスワードを入力してください。" "す! 暗号化を有効にするには、下にパスワードを入力してください。"
#: ReactNativeClient/lib/components/screens/encryption-config.js:177 #: ReactNativeClient/lib/components/screens/encryption-config.js:177
msgid "Enable" msgid "Enable"
@@ -3546,32 +3551,35 @@ msgstr "ノートをどのノートブックにインポートするのか指定
#: ReactNativeClient/lib/services/KeymapService.js:124 #: ReactNativeClient/lib/services/KeymapService.js:124
#, javascript-format #, javascript-format
msgid "Error loading the keymap from file: %s" msgid "Error loading the keymap from file: %s"
msgstr "" msgstr "キーマップ読み込みエラー(ファイル: %s)"
#: ReactNativeClient/lib/services/KeymapService.js:141 #: ReactNativeClient/lib/services/KeymapService.js:141
#, javascript-format #, javascript-format
msgid "Error saving the keymap to file: %s" msgid "Error saving the keymap to file: %s"
msgstr "" msgstr "キーマップ書き出しエラー(ファイル: %s)"
#: ReactNativeClient/lib/services/KeymapService.js:204 #: ReactNativeClient/lib/services/KeymapService.js:204
#, javascript-format #, javascript-format
msgid "Keymap item %s is missing the required \"command\" property." msgid "Keymap item %s is missing the required \"command\" property."
msgstr "" msgstr ""
"キーマップアイテム %s は必須の \"command\" プロパティを持っていません。"
#: ReactNativeClient/lib/services/KeymapService.js:207 #: ReactNativeClient/lib/services/KeymapService.js:207
#, javascript-format #, javascript-format
msgid "Keymap item %s is invalid because %s is not a valid command." msgid "Keymap item %s is invalid because %s is not a valid command."
msgstr "" msgstr "キーマップアイテム %s は %s が有効なコマンドでないため無効です。"
#: ReactNativeClient/lib/services/KeymapService.js:210 #: ReactNativeClient/lib/services/KeymapService.js:210
#, javascript-format #, javascript-format
msgid "Keymap item %s is missing the required \"accelerator\" property." msgid "Keymap item %s is missing the required \"accelerator\" property."
msgstr "" msgstr ""
"キーマップアイテム %s は必須の \"accelerator\" プロパティを持っていません。"
#: ReactNativeClient/lib/services/KeymapService.js:217 #: ReactNativeClient/lib/services/KeymapService.js:217
#, javascript-format #, javascript-format
msgid "Keymap item %s is invalid because %s is not a valid accelerator." msgid "Keymap item %s is invalid because %s is not a valid accelerator."
msgstr "" msgstr ""
"キーマップアイテム %s は %s が有効なショートカットキーでないため無効です。"
#: ReactNativeClient/lib/services/KeymapService.js:235 #: ReactNativeClient/lib/services/KeymapService.js:235
#, javascript-format #, javascript-format
@@ -3579,11 +3587,13 @@ msgid ""
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to " "Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to "
"unexpected behaviour." "unexpected behaviour."
msgstr "" msgstr ""
"ショートカットキー \"%s\" は \"%s\" コマンドと \"%s\" コマンドで使われていま"
"す。これにより予想外の動作が起こる可能性があります。"
#: ReactNativeClient/lib/services/KeymapService.js:260 #: ReactNativeClient/lib/services/KeymapService.js:260
#, javascript-format #, javascript-format
msgid "Accelerator \"%s\" is not valid." msgid "Accelerator \"%s\" is not valid."
msgstr "" msgstr "ショートカットキー \"%s\" は無効です。"
#: ReactNativeClient/lib/services/report.js:121 #: ReactNativeClient/lib/services/report.js:121
msgid "Items that cannot be synchronised" msgid "Items that cannot be synchronised"

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,9 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.3.1\n" "X-Generator: Poedit 2.4.1\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
#: CliClient/app/command-cp.js:13 #: CliClient/app/command-cp.js:13
msgid "" msgid ""
@@ -319,7 +321,7 @@ msgstr ""
#: CliClient/app/command-sync.js:35 #: CliClient/app/command-sync.js:35
msgid "Upgrade the sync target to the latest version." msgid "Upgrade the sync target to the latest version."
msgstr "" msgstr "Senkronizasyon hedefini en son sürüme yükseltin"
#: CliClient/app/command-sync.js:81 CliClient/app/command-sync.js:95 #: CliClient/app/command-sync.js:81 CliClient/app/command-sync.js:95
#: ElectronClient/gui/OneDriveLoginScreen.min.js:40 #: ElectronClient/gui/OneDriveLoginScreen.min.js:40
@@ -813,6 +815,8 @@ msgstr "İptal et"
msgid "" msgid ""
"The app is now going to close. Please relaunch it to complete the process." "The app is now going to close. Please relaunch it to complete the process."
msgstr "" msgstr ""
"Uygulama şimdi kapanacak. İşlemi tamamlamak için lütfen uygulamayı "
"kapandıktan sonar yeniden çalıştırın."
#: ElectronClient/plugins/GotoAnything.min.js:446 #: ElectronClient/plugins/GotoAnything.min.js:446
msgid "" msgid ""
@@ -1173,13 +1177,13 @@ msgstr "Yeni %s oluşturuluyor..."
#: ElectronClient/gui/NoteEditor/NoteEditor.js:344 #: ElectronClient/gui/NoteEditor/NoteEditor.js:344
msgid "The following attachments are being watched for changes:" msgid "The following attachments are being watched for changes:"
msgstr "" msgstr "Şu ek dosyaları değişiklikler için izlenmekte:"
#: ElectronClient/gui/NoteEditor/NoteEditor.js:347 #: ElectronClient/gui/NoteEditor/NoteEditor.js:347
msgid "" msgid ""
"The attachments will no longer be watched when you switch to a different " "The attachments will no longer be watched when you switch to a different "
"note." "note."
msgstr "" msgstr "Eğer başka bir not'a geçerseniz ek dosyalar artık izlenmeyecek."
#: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:25 #: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:25
msgid "Select all" msgid "Select all"
@@ -1290,7 +1294,7 @@ msgstr "Not özellikleri"
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:62 #: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:62
msgid "An unexpected error occured while importing the keymap!" msgid "An unexpected error occured while importing the keymap!"
msgstr "" msgstr "Tuş dizimini içe aktarırken bir hata oluştu!"
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:119 #: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:119
#: ElectronClient/gui/MainScreen/MainScreen.min.js:437 #: ElectronClient/gui/MainScreen/MainScreen.min.js:437
@@ -1305,12 +1309,11 @@ msgstr "İçe aktar"
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:125 #: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:125
msgid "Command" msgid "Command"
msgstr "" msgstr "Komut"
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:126 #: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:126
#, fuzzy
msgid "Keyboard Shortcut" msgid "Keyboard Shortcut"
msgstr "Klavye modu" msgstr "Klavye Kısayolu"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:14 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:14
#: ElectronClient/app.js:690 #: ElectronClient/app.js:690
@@ -1333,9 +1336,8 @@ msgid "Website and documentation"
msgstr "Web sitesi ve dökümanlar" msgstr "Web sitesi ve dökümanlar"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:24 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:24
#, fuzzy
msgid "Hide Joplin" msgid "Hide Joplin"
msgstr "Joplin hakkında" msgstr "Joplin'i Gizle"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:26 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:26
#: ElectronClient/app.js:703 #: ElectronClient/app.js:703
@@ -1343,9 +1345,8 @@ msgid "Close Window"
msgstr "Pencereyi Kapat" msgstr "Pencereyi Kapat"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28
#, fuzzy
msgid "Preferences" msgid "Preferences"
msgstr "Tercihler..." msgstr "Tercihler"
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28 #: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28
#: ElectronClient/gui/Root.min.js:92 ElectronClient/app.js:572 #: ElectronClient/gui/Root.min.js:92 ElectronClient/app.js:572
@@ -1354,13 +1355,15 @@ msgstr "Seçenekler"
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48 #: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48
msgid "Press the shortcut" msgid "Press the shortcut"
msgstr "" msgstr "Kısayolu girin"
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48 #: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48
msgid "" msgid ""
"Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the " "Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the "
"shortcut." "shortcut."
msgstr "" msgstr ""
"Kısayolu girin ve ardından ENTER tuşuna basın. Veya BACKSPACE tuşuna basarak "
"kısayolu temizleyin."
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:49 #: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:49
#: ElectronClient/gui/EncryptionConfigScreen.min.js:95 #: ElectronClient/gui/EncryptionConfigScreen.min.js:95
@@ -1374,11 +1377,14 @@ msgid ""
"may take a few minutes to complete and the app needs to be restarted. To " "may take a few minutes to complete and the app needs to be restarted. To "
"proceed please click on the link." "proceed please click on the link."
msgstr "" msgstr ""
"Senkronizasyon hedefi'nin Joplin senkronizasyona yeniden başlamadan once "
"güncellenmesi gerekir. Bu işlem notlarınızıın yoğunluğuna göre birkaç dakika "
"sürebilir, ve de bu işlem ardından uygulama yeniden başlatılacaktır. Bu "
"işlemi başlatmak için lütfen linke tıklayın."
#: ElectronClient/gui/MainScreen/MainScreen.min.js:306 #: ElectronClient/gui/MainScreen/MainScreen.min.js:306
#, fuzzy
msgid "Restart and upgrade" msgid "Restart and upgrade"
msgstr "Ana anahtarların güncellenmesi lazım" msgstr "Yeniden başlat ve güncelle"
#: ElectronClient/gui/MainScreen/MainScreen.min.js:313 #: ElectronClient/gui/MainScreen/MainScreen.min.js:313
msgid "Some items cannot be synchronised." msgid "Some items cannot be synchronised."
@@ -2138,9 +2144,8 @@ msgid "Templates"
msgstr "Şablonlar" msgstr "Şablonlar"
#: ElectronClient/app.js:668 #: ElectronClient/app.js:668
#, fuzzy
msgid "Export all" msgid "Export all"
msgstr "Dışa aktar" msgstr "Tümünü dışa aktar"
#: ElectronClient/app.js:683 #: ElectronClient/app.js:683
#, javascript-format #, javascript-format
@@ -2241,7 +2246,7 @@ msgstr "Bilinmeyen ID seviyesi: %s"
#: ReactNativeClient/lib/SyncTargetAmazonS3.js:28 #: ReactNativeClient/lib/SyncTargetAmazonS3.js:28
msgid "AWS S3" msgid "AWS S3"
msgstr "" msgstr "AWS S3"
#: ReactNativeClient/lib/SyncTargetDropbox.js:25 #: ReactNativeClient/lib/SyncTargetDropbox.js:25
msgid "Dropbox" msgid "Dropbox"
@@ -2384,15 +2389,15 @@ msgstr "WebDAV şifresi"
#: ReactNativeClient/lib/models/Setting.js:195 #: ReactNativeClient/lib/models/Setting.js:195
msgid "AWS S3 bucket" msgid "AWS S3 bucket"
msgstr "" msgstr "AWS S3 deposu"
#: ReactNativeClient/lib/models/Setting.js:206 #: ReactNativeClient/lib/models/Setting.js:206
msgid "AWS key" msgid "AWS key"
msgstr "" msgstr "AWS anahtarı"
#: ReactNativeClient/lib/models/Setting.js:216 #: ReactNativeClient/lib/models/Setting.js:216
msgid "AWS secret" msgid "AWS secret"
msgstr "" msgstr "AWS gizli anahtarı"
#: ReactNativeClient/lib/models/Setting.js:230 #: ReactNativeClient/lib/models/Setting.js:230
msgid "Attachment download behaviour" msgid "Attachment download behaviour"
@@ -2850,9 +2855,8 @@ msgid "Web Clipper"
msgstr "Web Alıntılayıcısı" msgstr "Web Alıntılayıcısı"
#: ReactNativeClient/lib/models/Setting.js:1259 #: ReactNativeClient/lib/models/Setting.js:1259
#, fuzzy
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "Klavye modu" msgstr "Klavye Kısayolları"
#: ReactNativeClient/lib/models/Setting.js:1264 #: ReactNativeClient/lib/models/Setting.js:1264
msgid "" msgid ""
@@ -3008,6 +3012,8 @@ msgstr "Bazı öğeler senkronize edilemiyor. Detayları için tıklayın."
#: ReactNativeClient/lib/components/screen-header.js:453 #: ReactNativeClient/lib/components/screen-header.js:453
msgid "The sync target needs to be upgraded. Press this banner to proceed." msgid "The sync target needs to be upgraded. Press this banner to proceed."
msgstr "" msgstr ""
"Senkronizasyon hedefinin güncellenmesi gerekmekte. Bu banner'a tıklayarak "
"devam edebilirsiniz."
#: ReactNativeClient/lib/components/side-menu-content.js:126 #: ReactNativeClient/lib/components/side-menu-content.js:126
#, javascript-format #, javascript-format
@@ -3129,7 +3135,7 @@ msgstr "Yenile"
#: ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js:42 #: ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js:42
msgid "Sync Target Upgrade" msgid "Sync Target Upgrade"
msgstr "" msgstr "Senkronizasyon Hedefi Güncellemesi"
#: ReactNativeClient/lib/components/screens/NoteTagsDialog.js:163 #: ReactNativeClient/lib/components/screens/NoteTagsDialog.js:163
msgid "New tags:" msgid "New tags:"
@@ -3578,32 +3584,37 @@ msgstr "Lütfen notların alınacağı not defterini belirtin."
#: ReactNativeClient/lib/services/KeymapService.js:124 #: ReactNativeClient/lib/services/KeymapService.js:124
#, javascript-format #, javascript-format
msgid "Error loading the keymap from file: %s" msgid "Error loading the keymap from file: %s"
msgstr "" msgstr "%s dosyasından tuş haritası yüklenemedi"
#: ReactNativeClient/lib/services/KeymapService.js:141 #: ReactNativeClient/lib/services/KeymapService.js:141
#, javascript-format #, javascript-format
msgid "Error saving the keymap to file: %s" msgid "Error saving the keymap to file: %s"
msgstr "" msgstr "%s dosyasına tuş haritası kaydedilemedi"
#: ReactNativeClient/lib/services/KeymapService.js:204 #: ReactNativeClient/lib/services/KeymapService.js:204
#, javascript-format #, javascript-format
msgid "Keymap item %s is missing the required \"command\" property." msgid "Keymap item %s is missing the required \"command\" property."
msgstr "" msgstr ""
"%s tuş haritası, işlemler için gerekli olan \"command\" özelliğini "
"barındırmıyor."
#: ReactNativeClient/lib/services/KeymapService.js:207 #: ReactNativeClient/lib/services/KeymapService.js:207
#, javascript-format #, javascript-format
msgid "Keymap item %s is invalid because %s is not a valid command." msgid "Keymap item %s is invalid because %s is not a valid command."
msgstr "" msgstr "%s tuş haritası geçersizdir, çünkü %s geçerli bir komut değildir."
#: ReactNativeClient/lib/services/KeymapService.js:210 #: ReactNativeClient/lib/services/KeymapService.js:210
#, javascript-format #, javascript-format
msgid "Keymap item %s is missing the required \"accelerator\" property." msgid "Keymap item %s is missing the required \"accelerator\" property."
msgstr "" msgstr ""
"%s tuş haritası, işlemler için gerekli olan \"accelerator\" özelliğini "
"barındırmıyor."
#: ReactNativeClient/lib/services/KeymapService.js:217 #: ReactNativeClient/lib/services/KeymapService.js:217
#, javascript-format #, javascript-format
msgid "Keymap item %s is invalid because %s is not a valid accelerator." msgid "Keymap item %s is invalid because %s is not a valid accelerator."
msgstr "" msgstr ""
"%s tuş haritası geçersizdir, çünkü %s geçerli bir accelerator değildir."
#: ReactNativeClient/lib/services/KeymapService.js:235 #: ReactNativeClient/lib/services/KeymapService.js:235
#, javascript-format #, javascript-format
@@ -3611,11 +3622,13 @@ msgid ""
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to " "Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to "
"unexpected behaviour." "unexpected behaviour."
msgstr "" msgstr ""
"Kısayol \"%s\", \"%s\" ve \"%s\" komutları için kullanılıyor. Bu beklenmeyen "
"sonuçlara sebep verebilir."
#: ReactNativeClient/lib/services/KeymapService.js:260 #: ReactNativeClient/lib/services/KeymapService.js:260
#, javascript-format #, javascript-format
msgid "Accelerator \"%s\" is not valid." msgid "Accelerator \"%s\" is not valid."
msgstr "" msgstr "Kısayol \"%s\" geçersiz."
#: ReactNativeClient/lib/services/report.js:121 #: ReactNativeClient/lib/services/report.js:121
msgid "Items that cannot be synchronised" msgid "Items that cannot be synchronised"
@@ -3949,7 +3962,7 @@ msgstr ""
#~ "Şu anda not defteriniz yok. (+) butonuna tıklayarak bir tane oluşturun." #~ "Şu anda not defteriniz yok. (+) butonuna tıklayarak bir tane oluşturun."
#~ msgid "Welcome" #~ msgid "Welcome"
#~ msgstr "Hoşgeldiniz" #~ msgstr "Hoş Geldiniz"
#~ msgid "Separate each tag by a comma." #~ msgid "Separate each tag by a comma."
#~ msgstr "Her etiketi virgülle ayırın." #~ msgstr "Her etiketi virgülle ayırın."

View File

@@ -1,6 +1,6 @@
{ {
"name": "joplin", "name": "joplin",
"version": "1.0.168", "version": "1.2.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -5880,6 +5880,11 @@
"is-fullwidth-code-point": "^2.0.0" "is-fullwidth-code-point": "^2.0.0"
} }
}, },
"slug": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/slug/-/slug-3.3.4.tgz",
"integrity": "sha512-VpHbtRCEWmgaZsrZcTsVl/Dhw98lcrOYDO17DNmJCNpppI6s3qJvnNu2Q3D4L84/2bi6vkW40mjNQI9oGQsflg=="
},
"snapdragon": { "snapdragon": {
"version": "0.8.2", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@@ -6742,11 +6747,6 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
}, },
"unorm": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
"integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA=="
},
"unpack-string": { "unpack-string": {
"version": "0.0.2", "version": "0.0.2",
"resolved": "https://registry.npmjs.org/unpack-string/-/unpack-string-0.0.2.tgz", "resolved": "https://registry.npmjs.org/unpack-string/-/unpack-string-0.0.2.tgz",
@@ -6849,14 +6849,6 @@
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
}, },
"uslug": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/uslug/-/uslug-1.0.4.tgz",
"integrity": "sha1-uaIvCRTgqGFAYz2swwLl9PpFBnc=",
"requires": {
"unorm": ">= 1.0.0"
}
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -28,7 +28,7 @@
], ],
"owner": "Laurent Cozic" "owner": "Laurent Cozic"
}, },
"version": "1.0.168", "version": "1.2.3",
"bin": { "bin": {
"joplin": "./main.js" "joplin": "./main.js"
}, },
@@ -109,7 +109,7 @@
"terminal-kit": "^1.30.0", "terminal-kit": "^1.30.0",
"tkwidgets": "^0.5.26", "tkwidgets": "^0.5.26",
"url-parse": "^1.4.7", "url-parse": "^1.4.7",
"uslug": "^1.0.4", "slug": "^3.3.4",
"uuid": "^3.0.1", "uuid": "^3.0.1",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
"word-wrap": "^1.2.3", "word-wrap": "^1.2.3",

View File

@@ -1,163 +1,163 @@
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
/* eslint prefer-const: 0*/ /* eslint prefer-const: 0*/
require('app-module-path').addPath(__dirname); // require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js'); // const { time } = require('lib/time-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, createNTestNotes, switchClient, createNTestFolders } = require('test-utils.js'); // const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, createNTestNotes, switchClient, createNTestFolders } = require('test-utils.js');
const SearchEngine = require('lib/services/searchengine/SearchEngine'); // const SearchEngine = require('lib/services/searchengine/SearchEngine');
const Note = require('lib/models/Note'); // const Note = require('lib/models/Note');
const Folder = require('lib/models/Folder'); // const Folder = require('lib/models/Folder');
const Tag = require('lib/models/Tag'); // const Tag = require('lib/models/Tag');
const ItemChange = require('lib/models/ItemChange'); // const ItemChange = require('lib/models/ItemChange');
const Setting = require('lib/models/Setting'); // const Setting = require('lib/models/Setting');
const Resource = require('lib/models/Resource.js'); // const Resource = require('lib/models/Resource.js');
const { shim } = require('lib/shim'); // const { shim } = require('lib/shim');
const ResourceService = require('lib/services/ResourceService.js'); // const ResourceService = require('lib/services/ResourceService.js');
process.on('unhandledRejection', (reason, p) => { // process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); // console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
}); // });
let engine = null; // let engine = null;
const ids = (array) => array.map(a => a.id); // const ids = (array) => array.map(a => a.id);
describe('services_SearchFuzzy', function() { // describe('services_SearchFuzzy', function() {
beforeEach(async (done) => { // beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1); // await setupDatabaseAndSynchronizer(1);
await switchClient(1); // await switchClient(1);
engine = new SearchEngine(); // engine = new SearchEngine();
engine.setDb(db()); // engine.setDb(db());
Setting.setValue('db.fuzzySearchEnabled', 1); // Setting.setValue('db.fuzzySearchEnabled', 1);
done(); // done();
}); // });
it('should return note almost matching title', asyncTest(async () => { // it('should return note almost matching title', asyncTest(async () => {
let rows; // let rows;
const n1 = await Note.save({ title: 'If It Ain\'t Baroque, Don\'t Fix It' }); // const n1 = await Note.save({ title: 'If It Ain\'t Baroque, Don\'t Fix It' });
const n2 = await Note.save({ title: 'Important note' }); // const n2 = await Note.save({ title: 'Important note' });
await engine.syncTables(); // await engine.syncTables();
rows = await engine.search('Broke', { fuzzy: false }); // rows = await engine.search('Broke', { fuzzy: false });
expect(rows.length).toBe(0); // expect(rows.length).toBe(0);
rows = await engine.search('Broke', { fuzzy: true }); // rows = await engine.search('Broke', { fuzzy: true });
expect(rows.length).toBe(1); // expect(rows.length).toBe(1);
expect(rows[0].id).toBe(n1.id); // expect(rows[0].id).toBe(n1.id);
rows = await engine.search('title:Broke', { fuzzy: true }); // rows = await engine.search('title:Broke', { fuzzy: true });
expect(rows.length).toBe(1); // expect(rows.length).toBe(1);
expect(rows[0].id).toBe(n1.id); // expect(rows[0].id).toBe(n1.id);
rows = await engine.search('title:"Broke"', { fuzzy: true }); // rows = await engine.search('title:"Broke"', { fuzzy: true });
expect(rows.length).toBe(1); // expect(rows.length).toBe(1);
expect(rows[0].id).toBe(n1.id); // expect(rows[0].id).toBe(n1.id);
rows = await engine.search('Imprtant', { fuzzy: true }); // rows = await engine.search('Imprtant', { fuzzy: true });
expect(rows.length).toBe(1); // expect(rows.length).toBe(1);
expect(rows[0].id).toBe(n2.id); // expect(rows[0].id).toBe(n2.id);
})); // }));
it('should order results by min fuzziness', asyncTest(async () => { // it('should order results by min fuzziness', asyncTest(async () => {
let rows; // let rows;
const n1 = await Note.save({ title: 'I demand you take me to him' }); // const n1 = await Note.save({ title: 'I demand you take me to him' });
const n2 = await Note.save({ title: 'He demanded an answer' }); // const n2 = await Note.save({ title: 'He demanded an answer' });
const n3 = await Note.save({ title: 'Don\'t you make demands of me' }); // const n3 = await Note.save({ title: 'Don\'t you make demands of me' });
const n4 = await Note.save({ title: 'No drama for me' }); // const n4 = await Note.save({ title: 'No drama for me' });
const n5 = await Note.save({ title: 'Just minding my own business' }); // const n5 = await Note.save({ title: 'Just minding my own business' });
await engine.syncTables(); // await engine.syncTables();
rows = await engine.search('demand', { fuzzy: false }); // rows = await engine.search('demand', { fuzzy: false });
expect(rows.length).toBe(1); // expect(rows.length).toBe(1);
expect(rows[0].id).toBe(n1.id); // expect(rows[0].id).toBe(n1.id);
rows = await engine.search('demand', { fuzzy: true }); // rows = await engine.search('demand', { fuzzy: true });
expect(rows.length).toBe(3); // expect(rows.length).toBe(3);
expect(rows[0].id).toBe(n1.id); // expect(rows[0].id).toBe(n1.id);
expect(rows[1].id).toBe(n3.id); // expect(rows[1].id).toBe(n3.id);
expect(rows[2].id).toBe(n2.id); // expect(rows[2].id).toBe(n2.id);
})); // }));
it('should consider any:1', asyncTest(async () => { // it('should consider any:1', asyncTest(async () => {
let rows; // let rows;
const n1 = await Note.save({ title: 'cat' }); // const n1 = await Note.save({ title: 'cat' });
const n2 = await Note.save({ title: 'cats' }); // const n2 = await Note.save({ title: 'cats' });
const n3 = await Note.save({ title: 'cot' }); // const n3 = await Note.save({ title: 'cot' });
const n4 = await Note.save({ title: 'defenestrate' }); // const n4 = await Note.save({ title: 'defenestrate' });
const n5 = await Note.save({ title: 'defenstrate' }); // const n5 = await Note.save({ title: 'defenstrate' });
const n6 = await Note.save({ title: 'defenestrated' }); // const n6 = await Note.save({ title: 'defenestrated' });
const n7 = await Note.save({ title: 'he defenestrated the cat' }); // const n7 = await Note.save({ title: 'he defenestrated the cat' });
await engine.syncTables(); // await engine.syncTables();
rows = await engine.search('defenestrated cat', { fuzzy: true }); // rows = await engine.search('defenestrated cat', { fuzzy: true });
expect(rows.length).toBe(1); // expect(rows.length).toBe(1);
rows = await engine.search('any:1 defenestrated cat', { fuzzy: true }); // rows = await engine.search('any:1 defenestrated cat', { fuzzy: true });
expect(rows.length).toBe(7); // expect(rows.length).toBe(7);
})); // }));
it('should leave phrase searches alone', asyncTest(async () => { // it('should leave phrase searches alone', asyncTest(async () => {
let rows; // let rows;
const n1 = await Note.save({ title: 'abc def' }); // const n1 = await Note.save({ title: 'abc def' });
const n2 = await Note.save({ title: 'def ghi' }); // const n2 = await Note.save({ title: 'def ghi' });
const n3 = await Note.save({ title: 'ghi jkl' }); // const n3 = await Note.save({ title: 'ghi jkl' });
const n4 = await Note.save({ title: 'def abc' }); // const n4 = await Note.save({ title: 'def abc' });
const n5 = await Note.save({ title: 'mno pqr ghi jkl' }); // const n5 = await Note.save({ title: 'mno pqr ghi jkl' });
await engine.syncTables(); // await engine.syncTables();
rows = await engine.search('abc def', { fuzzy: true }); // rows = await engine.search('abc def', { fuzzy: true });
expect(rows.length).toBe(2); // expect(rows.length).toBe(2);
expect(rows.map(r=>r.id)).toContain(n1.id); // expect(rows.map(r=>r.id)).toContain(n1.id);
expect(rows.map(r=>r.id)).toContain(n4.id); // expect(rows.map(r=>r.id)).toContain(n4.id);
rows = await engine.search('"abc def"', { fuzzy: true }); // rows = await engine.search('"abc def"', { fuzzy: true });
expect(rows.length).toBe(1); // expect(rows.length).toBe(1);
expect(rows.map(r=>r.id)).toContain(n1.id); // expect(rows.map(r=>r.id)).toContain(n1.id);
rows = await engine.search('"ghi jkl"', { fuzzy: true }); // rows = await engine.search('"ghi jkl"', { fuzzy: true });
expect(rows.length).toBe(2); // expect(rows.length).toBe(2);
expect(rows.map(r=>r.id)).toContain(n3.id); // expect(rows.map(r=>r.id)).toContain(n3.id);
expect(rows.map(r=>r.id)).toContain(n5.id); // expect(rows.map(r=>r.id)).toContain(n5.id);
rows = await engine.search('"ghi jkl" mno', { fuzzy: true }); // rows = await engine.search('"ghi jkl" mno', { fuzzy: true });
expect(rows.length).toBe(1); // expect(rows.length).toBe(1);
expect(rows.map(r=>r.id)).toContain(n5.id); // expect(rows.map(r=>r.id)).toContain(n5.id);
rows = await engine.search('any:1 "ghi jkl" mno', { fuzzy: true }); // rows = await engine.search('any:1 "ghi jkl" mno', { fuzzy: true });
expect(rows.length).toBe(2); // expect(rows.length).toBe(2);
expect(rows.map(r=>r.id)).toContain(n3.id); // expect(rows.map(r=>r.id)).toContain(n3.id);
expect(rows.map(r=>r.id)).toContain(n5.id); // expect(rows.map(r=>r.id)).toContain(n5.id);
})); // }));
it('should leave wild card searches alone', asyncTest(async () => { // it('should leave wild card searches alone', asyncTest(async () => {
let rows; // let rows;
const n1 = await Note.save({ title: 'abc def' }); // const n1 = await Note.save({ title: 'abc def' });
const n2 = await Note.save({ title: 'abcc ghi' }); // const n2 = await Note.save({ title: 'abcc ghi' });
const n3 = await Note.save({ title: 'abccc ghi' }); // const n3 = await Note.save({ title: 'abccc ghi' });
const n4 = await Note.save({ title: 'abcccc ghi' }); // const n4 = await Note.save({ title: 'abcccc ghi' });
const n5 = await Note.save({ title: 'wxy zzz' }); // const n5 = await Note.save({ title: 'wxy zzz' });
await engine.syncTables(); // await engine.syncTables();
rows = await engine.search('abc*', { fuzzy: true }); // rows = await engine.search('abc*', { fuzzy: true });
expect(rows.length).toBe(4); // expect(rows.length).toBe(4);
expect(rows.map(r=>r.id)).toContain(n1.id); // expect(rows.map(r=>r.id)).toContain(n1.id);
expect(rows.map(r=>r.id)).toContain(n2.id); // expect(rows.map(r=>r.id)).toContain(n2.id);
expect(rows.map(r=>r.id)).toContain(n3.id); // expect(rows.map(r=>r.id)).toContain(n3.id);
expect(rows.map(r=>r.id)).toContain(n4.id); // expect(rows.map(r=>r.id)).toContain(n4.id);
})); // }));
}); // });

View File

@@ -26,10 +26,11 @@ describe('timeUtils', function() {
startDate = new Date('3 Aug 2020 07:30:20'); startDate = new Date('3 Aug 2020 07:30:20');
expect(time.goBackInTime(startDate, 1, 'day')).toBe(endDate.getTime().toString()); expect(time.goBackInTime(startDate, 1, 'day')).toBe(endDate.getTime().toString());
// Note: this test randomly fails - https://github.com/laurent22/joplin/issues/3722
startDate = new Date('11 Aug 2020'); // startDate = new Date('11 Aug 2020');
endDate = new Date('9 Aug 2020'); // week start; // endDate = new Date('9 Aug 2020'); // week start;
expect(time.goBackInTime(startDate, 0, 'week')).toBe(endDate.getTime().toString()); // expect(time.goBackInTime(startDate, 0, 'week')).toBe(endDate.getTime().toString());
startDate = new Date('02 Feb 2020'); startDate = new Date('02 Feb 2020');
endDate = new Date('01 Jan 2020'); endDate = new Date('01 Jan 2020');
@@ -50,9 +51,9 @@ describe('timeUtils', function() {
expect(time.goForwardInTime(startDate, 1, 'day')).toBe(endDate.getTime().toString()); expect(time.goForwardInTime(startDate, 1, 'day')).toBe(endDate.getTime().toString());
startDate = new Date('9 Aug 2020'); // startDate = new Date('9 Aug 2020');
endDate = new Date('9 Aug 2020'); // week start; // endDate = new Date('9 Aug 2020'); // week start;
expect(time.goForwardInTime(startDate, 0, 'week')).toBe(endDate.getTime().toString()); // expect(time.goForwardInTime(startDate, 0, 'week')).toBe(endDate.getTime().toString());
startDate = new Date('02 Jan 2020'); startDate = new Date('02 Jan 2020');
endDate = new Date('01 Feb 2020'); endDate = new Date('01 Feb 2020');

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Joplin Web Clipper [DEV]", "name": "Joplin Web Clipper [DEV]",
"version": "1.0.25", "version": "1.2.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.", "description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org", "homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self'; object-src 'self'", "content_security_policy": "script-src 'self'; object-src 'self'",

View File

@@ -34,16 +34,17 @@ const KeymapService = require('lib/services/KeymapService').default;
const TemplateUtils = require('lib/TemplateUtils'); const TemplateUtils = require('lib/TemplateUtils');
const CssUtils = require('lib/CssUtils'); const CssUtils = require('lib/CssUtils');
const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/reducer').default; const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/reducer').default;
// const populateDatabase = require('lib/services/debug/populateDatabase').default;
const versionInfo = require('lib/versionInfo').default; const versionInfo = require('lib/versionInfo').default;
const commands = [ const commands = [
require('./gui/Header/commands/focusSearch'), require('./gui/NoteListControls/commands/focusSearch'),
require('./gui/MainScreen/commands/editAlarm'), require('./gui/MainScreen/commands/editAlarm'),
require('./gui/MainScreen/commands/exportPdf'), require('./gui/MainScreen/commands/exportPdf'),
require('./gui/MainScreen/commands/hideModalMessage'), require('./gui/MainScreen/commands/hideModalMessage'),
require('./gui/MainScreen/commands/moveToFolder'), require('./gui/MainScreen/commands/moveToFolder'),
require('./gui/MainScreen/commands/newNote'), require('./gui/MainScreen/commands/newNote'),
require('./gui/MainScreen/commands/newNotebook'), require('./gui/MainScreen/commands/newFolder'),
require('./gui/MainScreen/commands/newTodo'), require('./gui/MainScreen/commands/newTodo'),
require('./gui/MainScreen/commands/print'), require('./gui/MainScreen/commands/print'),
require('./gui/MainScreen/commands/renameFolder'), require('./gui/MainScreen/commands/renameFolder'),
@@ -58,6 +59,7 @@ const commands = [
require('./gui/MainScreen/commands/toggleNoteList'), require('./gui/MainScreen/commands/toggleNoteList'),
require('./gui/MainScreen/commands/toggleSidebar'), require('./gui/MainScreen/commands/toggleSidebar'),
require('./gui/MainScreen/commands/toggleVisiblePanes'), require('./gui/MainScreen/commands/toggleVisiblePanes'),
require('./gui/MainScreen/commands/toggleEditors'),
require('./gui/NoteEditor/commands/focusElementNoteBody'), require('./gui/NoteEditor/commands/focusElementNoteBody'),
require('./gui/NoteEditor/commands/focusElementNoteTitle'), require('./gui/NoteEditor/commands/focusElementNoteTitle'),
require('./gui/NoteEditor/commands/showLocalSearch'), require('./gui/NoteEditor/commands/showLocalSearch'),
@@ -98,6 +100,8 @@ const appDefaultState = Object.assign({}, defaultState, {
watchedNoteFiles: [], watchedNoteFiles: [],
lastEditorScrollPercents: {}, lastEditorScrollPercents: {},
devToolsVisible: false, devToolsVisible: false,
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
focusedField: null,
}); });
class Application extends BaseApplication { class Application extends BaseApplication {
@@ -279,6 +283,31 @@ class Application extends BaseApplication {
newState.devToolsVisible = action.value; newState.devToolsVisible = action.value;
break; break;
case 'VISIBLE_DIALOGS_ADD':
newState = Object.assign({}, state);
newState.visibleDialogs[state.name] = true;
break;
case 'VISIBLE_DIALOGS_REMOVE':
newState = Object.assign({}, state);
delete newState.visibleDialogs[state.name];
break;
case 'FOCUS_SET':
newState = Object.assign({}, state);
newState.focusedField = action.field;
break;
case 'FOCUS_CLEAR':
// A field can only clear its own state
if (action.field === state.focusedField) {
newState = Object.assign({}, state);
newState.focusedField = null;
}
break;
} }
} catch (error) { } catch (error) {
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`; error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
@@ -286,10 +315,11 @@ class Application extends BaseApplication {
} }
newState = resourceEditWatcherReducer(newState, action); newState = resourceEditWatcherReducer(newState, action);
newState = super.reducer(newState, action);
CommandService.instance().scheduleMapStateToProps(newState); CommandService.instance().scheduleMapStateToProps(newState);
return super.reducer(newState, action); return newState;
} }
toggleDevTools(visible) { toggleDevTools(visible) {
@@ -375,7 +405,7 @@ class Application extends BaseApplication {
await this.updateMenu(screen); await this.updateMenu(screen);
} }
async updateMenu(screen) { async updateMenu(screen, updateStates = true) {
if (this.lastMenuScreen_ === screen) return; if (this.lastMenuScreen_ === screen) return;
const cmdService = CommandService.instance(); const cmdService = CommandService.instance();
@@ -519,7 +549,7 @@ class Application extends BaseApplication {
const newNoteItem = cmdService.commandToMenuItem('newNote'); const newNoteItem = cmdService.commandToMenuItem('newNote');
const newTodoItem = cmdService.commandToMenuItem('newTodo'); const newTodoItem = cmdService.commandToMenuItem('newTodo');
const newNotebookItem = cmdService.commandToMenuItem('newNotebook'); const newFolderItem = cmdService.commandToMenuItem('newFolder');
const printItem = cmdService.commandToMenuItem('print'); const printItem = cmdService.commandToMenuItem('print');
toolsItemsFirst.push(syncStatusItem, { toolsItemsFirst.push(syncStatusItem, {
@@ -650,7 +680,7 @@ class Application extends BaseApplication {
}, },
shim.isMac() ? noItem : newNoteItem, shim.isMac() ? noItem : newNoteItem,
shim.isMac() ? noItem : newTodoItem, shim.isMac() ? noItem : newTodoItem,
shim.isMac() ? noItem : newNotebookItem, { shim.isMac() ? noItem : newFolderItem, {
type: 'separator', type: 'separator',
visible: shim.isMac() ? false : true, visible: shim.isMac() ? false : true,
}, { }, {
@@ -699,7 +729,7 @@ class Application extends BaseApplication {
submenu: [ submenu: [
newNoteItem, newNoteItem,
newTodoItem, newTodoItem,
newNotebookItem, { newFolderItem, {
label: _('Close Window'), label: _('Close Window'),
platforms: ['darwin'], platforms: ['darwin'],
accelerator: shim.isMac() && keymapService.getAccelerator('closeWindow'), accelerator: shim.isMac() && keymapService.getAccelerator('closeWindow'),
@@ -738,7 +768,6 @@ class Application extends BaseApplication {
const separator = () => { const separator = () => {
return { return {
type: 'separator', type: 'separator',
screens: ['Main'],
}; };
}; };
@@ -986,6 +1015,8 @@ class Application extends BaseApplication {
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
this.lastMenuScreen_ = screen; this.lastMenuScreen_ = screen;
if (updateStates) await this.updateMenuItemStates();
} }
async updateMenuItemStates(state = null) { async updateMenuItemStates(state = null) {
@@ -1095,7 +1126,7 @@ class Application extends BaseApplication {
try { try {
await keymapService.loadCustomKeymap(`${dir}/keymap-desktop.json`); await keymapService.loadCustomKeymap(`${dir}/keymap-desktop.json`);
} catch (err) { } catch (err) {
bridge().showErrorMessageBox(err.message); reg.logger().error(err.message);
} }
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId })); AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
@@ -1128,7 +1159,7 @@ class Application extends BaseApplication {
CommandService.instance().registerDeclaration(declaration); CommandService.instance().registerDeclaration(declaration);
} }
this.updateMenu('Main'); this.updateMenu('Main', false);
// Since the settings need to be loaded before the store is created, it will never // Since the settings need to be loaded before the store is created, it will never
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be // receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
@@ -1253,6 +1284,8 @@ class Application extends BaseApplication {
}; };
bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated); bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated);
// await populateDatabase(reg.db());
} }
} }

View File

@@ -11,7 +11,7 @@ interface Props {
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'startExternalEditing', name: 'startExternalEditing',
label: () => _('Edit in external editor'), label: () => _('Edit in external editor'),
iconName: 'fa-share-square', iconName: 'icon-share',
}; };
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
@@ -27,10 +27,14 @@ export const runtime = ():CommandRuntime => {
// await comp.saveNoteAndWait(comp.formNote); // await comp.saveNoteAndWait(comp.formNote);
}, },
isEnabled: (props:any) => { isEnabled: (props:any) => {
if (props.routeName !== 'Main') return false;
return !!props.noteId; return !!props.noteId;
}, },
mapStateToProps: (state:any) => { mapStateToProps: (state:any) => {
return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null }; return {
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
routeName: state.route.routeName,
};
}, },
}; };
}; };

View File

@@ -18,10 +18,11 @@ export const runtime = ():CommandRuntime => {
ExternalEditWatcher.instance().stopWatching(props.noteId); ExternalEditWatcher.instance().stopWatching(props.noteId);
}, },
isEnabled: (props:any) => { isEnabled: (props:any) => {
if (props.routeName !== 'Main') return false;
return !!props.noteId; return !!props.noteId;
}, },
mapStateToProps: (state:any) => { mapStateToProps: (state:any) => {
return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null }; return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null, routeName: state.route.routeName };
}, },
}; };
}; };

View File

@@ -0,0 +1,195 @@
import * as React from 'react';
const styled = require('styled-components').default;
const { space } = require('styled-system');
export enum ButtonLevel {
Primary = 'primary',
Secondary = 'secondary',
Tertiary = 'tertiary',
SideBarSecondary = 'sideBarSecondary',
}
interface Props {
title?: string,
iconName?: string,
level?: ButtonLevel,
className?:string,
onClick:Function,
color?: string,
iconAnimation?: string,
tooltip?: string,
disabled?: boolean,
style?:any,
}
const StyledTitle = styled.span`
`;
const StyledButtonBase = styled.button`
display: flex;
align-items: center;
flex-direction: row;
height: ${(props:any) => `${props.theme.toolbarHeight}px`};
min-height: ${(props:any) => `${props.theme.toolbarHeight}px`};
max-height: ${(props:any) => `${props.theme.toolbarHeight}px`};
width: ${(props:any) => props.iconOnly ? `${props.theme.toolbarHeight}px` : 'auto'};
${(props:any) => props.iconOnly ? `min-width: ${props.theme.toolbarHeight}px;` : ''}
${(props:any) => !props.iconOnly ? 'min-width: 100px;' : ''}
${(props:any) => props.iconOnly ? `max-width: ${props.theme.toolbarHeight}px;` : ''}
box-sizing: border-box;
border-radius: 3px;
border-style: solid;
border-width: 1px;
font-size: ${(props:any) => props.theme.fontSize}px;
padding: 0 ${(props:any) => props.iconOnly ? 4 : 8}px;
justify-content: center;
opacity: ${(props:any) => props.disabled ? 0.5 : 1};
user-select: none;
`;
const StyledIcon = styled(styled.span(space))`
font-size: ${(props:any) => props.theme.toolbarIconSize}px;
${(props:any) => props.animation ? `animation: ${props.animation}` : ''};
`;
const StyledButtonPrimary = styled(StyledButtonBase)`
border: none;
background-color: ${(props:any) => props.theme.backgroundColor5};
&:hover {
background-color: ${(props:any) => props.theme.backgroundColorHover5};
}
&:active {
background-color: ${(props:any) => props.theme.backgroundColorActive5};
}
${StyledIcon} {
color: ${(props:any) => props.theme.color5};
}
${StyledTitle} {
color: ${(props:any) => props.theme.color5};
}
`;
const StyledButtonSecondary = styled(StyledButtonBase)`
border: 1px solid ${(props:any) => props.theme.borderColor4};
background-color: ${(props:any) => props.theme.backgroundColor4};
&:hover {
background-color: ${(props:any) => props.theme.backgroundColorHover4};
}
&:active {
background-color: ${(props:any) => props.theme.backgroundColorActive4};
}
${StyledIcon} {
color: ${(props:any) => props.theme.color4};
}
${StyledTitle} {
color: ${(props:any) => props.theme.color4};
}
`;
const StyledButtonTertiary = styled(StyledButtonBase)`
border: 1px solid ${(props:any) => props.theme.color3};
background-color: ${(props:any) => props.theme.backgroundColor3};
&:hover {
background-color: ${(props:any) => props.theme.backgroundColorHoverDim3};
}
&:active {
background-color: ${(props:any) => props.theme.backgroundColorActive3};
}
${StyledIcon} {
color: ${(props:any) => props.theme.color};
}
${StyledTitle} {
color: ${(props:any) => props.theme.color};
opacity: 0.9;
}
`;
const StyledButtonSideBarSecondary = styled(StyledButtonBase)`
background: none;
border-color: ${(props:any) => props.theme.color2};
color: ${(props:any) => props.theme.color2};
&:hover {
color: ${(props:any) => props.theme.colorHover2};
border-color: ${(props:any) => props.theme.colorHover2};
background: none;
${StyledTitle} {
color: ${(props:any) => props.theme.colorHover2};
}
${StyledIcon} {
color: ${(props:any) => props.theme.colorHover2};
}
}
&:active {
color: ${(props:any) => props.theme.colorActive2};
border-color: ${(props:any) => props.theme.colorActive2};
background: none;
${StyledTitle} {
color: ${(props:any) => props.theme.colorActive2};
}
${StyledIcon} {
color: ${(props:any) => props.theme.colorActive2};
}
}
${StyledTitle} {
color: ${(props:any) => props.theme.color2};
}
${StyledIcon} {
color: ${(props:any) => props.theme.color2};
}
`;
function buttonClass(level:ButtonLevel) {
if (level === ButtonLevel.Primary) return StyledButtonPrimary;
if (level === ButtonLevel.Tertiary) return StyledButtonTertiary;
if (level === ButtonLevel.SideBarSecondary) return StyledButtonSideBarSecondary;
return StyledButtonSecondary;
}
export default function Button(props:Props) {
const iconOnly = props.iconName && !props.title;
const StyledButton = buttonClass(props.level);
function renderIcon() {
if (!props.iconName) return null;
return <StyledIcon animation={props.iconAnimation} mr={iconOnly ? '0' : '6px'} color={props.color} className={props.iconName}/>;
}
function renderTitle() {
if (!props.title) return null;
return <StyledTitle color={props.color}>{props.title}</StyledTitle>;
}
function onClick() {
if (props.disabled) return;
props.onClick();
}
return (
<StyledButton style={props.style} disabled={props.disabled} title={props.tooltip} className={props.className} iconOnly={iconOnly} onClick={onClick}>
{renderIcon()}
{renderTitle()}
</StyledButton>
);
}

View File

@@ -40,10 +40,12 @@ class ClipperConfigScreenComponent extends React.Component {
} }
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const containerStyle = Object.assign({}, theme.containerStyle, { const containerStyle = Object.assign({}, theme.containerStyle, {
overflowY: 'scroll', overflowY: 'scroll',
padding: theme.configScreenPadding,
backgroundColor: theme.backgroundColor3,
}); });
const buttonStyle = Object.assign({}, theme.buttonStyle, { marginRight: 10 }); const buttonStyle = Object.assign({}, theme.buttonStyle, { marginRight: 10 });
@@ -106,8 +108,8 @@ class ClipperConfigScreenComponent extends React.Component {
return ( return (
<div> <div>
<div style={containerStyle}> <div style={containerStyle}>
<div style={{ padding: theme.margin }}> <div>
<p style={theme.textStyle}>{_('Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.')}</p> <p style={Object.assign({}, theme.textStyle, { marginTop: 0 })}>{_('Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.')}</p>
<p style={theme.textStyle}>{_('In order to use the web clipper, you need to do the following:')}</p> <p style={theme.textStyle}>{_('In order to use the web clipper, you need to do the following:')}</p>
<div style={stepBoxStyle}> <div style={stepBoxStyle}>
@@ -120,8 +122,8 @@ class ClipperConfigScreenComponent extends React.Component {
<p style={theme.h1Style}>{_('Step 2: Install the extension')}</p> <p style={theme.h1Style}>{_('Step 2: Install the extension')}</p>
<p style={theme.textStyle}>{_('Download and install the relevant extension for your browser:')}</p> <p style={theme.textStyle}>{_('Download and install the relevant extension for your browser:')}</p>
<div style={{ display: 'flex', flexDirection: 'row' }}> <div style={{ display: 'flex', flexDirection: 'row' }}>
<ExtensionBadge theme={this.props.theme} type="firefox" url="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/"/> <ExtensionBadge themeId={this.props.themeId} type="firefox" url="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/"/>
<ExtensionBadge style={{ marginLeft: 10 }} theme={this.props.theme} type="chrome" url="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek"/> <ExtensionBadge style={{ marginLeft: 10 }} themeId={this.props.themeId} type="chrome" url="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek"/>
</div> </div>
</div> </div>
@@ -145,7 +147,7 @@ class ClipperConfigScreenComponent extends React.Component {
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
clipperServer: state.clipperServer, clipperServer: state.clipperServer,
clipperServerAutoStart: state.settings['clipperServer.autoStart'], clipperServerAutoStart: state.settings['clipperServer.autoStart'],
apiToken: state.settings['api.token'], apiToken: state.settings['api.token'],

View File

@@ -1,44 +0,0 @@
const React = require('react');
const styleSelector = require('./style/ConfigMenuBar');
const Setting = require('lib/models/Setting');
function ConfigMenuBarButton(props) {
const style = styleSelector(null, props);
const iconStyle = props.selected ? style.buttonIconSelected : style.buttonIcon;
const labelStyle = props.selected ? style.buttonLabelSelected : style.buttonLabel;
return (
<button style={style.button} onClick={props.onClick}>
<i style={iconStyle} className={props.iconName}></i>
<span style={labelStyle}>{props.label}</span>
</button>
);
}
function ConfigMenuBar(props) {
const buttons = [];
const style = styleSelector(null, props);
for (const section of props.sections) {
buttons.push(<ConfigMenuBarButton
selected={props.selection === section.name}
theme={props.theme}
key={section.name}
iconName={Setting.sectionNameToIcon(section.name)}
label={Setting.sectionNameToLabel(section.name)}
onClick={() => { props.onSelectionChange({ section: section }); }}
/>);
}
return (
<div style={style.root} className="config-menu-bar">
<div style={style.barButtons}>
{buttons}
</div>
</div>
);
}
module.exports = ConfigMenuBar;

View File

@@ -0,0 +1,52 @@
import * as React from 'react';
import Button, { ButtonLevel } from '../Button/Button';
const styled = require('styled-components').default;
const { _ } = require('lib/locale.js');
interface Props {
backButtonTitle?: string,
hasChanges?: boolean,
onCancelClick: Function,
onSaveClick?: Function,
onApplyClick?: Function,
}
export const StyledRoot = styled.div`
display: flex;
align-items: center;
padding: 10px;
background-color: ${(props:any) => props.theme.backgroundColor3};
padding-left: ${(props:any) => props.theme.configScreenPadding}px;
border-top-width: 1px;
border-top-style: solid;
border-top-color: ${(props:any) => props.theme.dividerColor};
`;
export default function ButtonBar(props:Props) {
function renderOkButton() {
if (!props.onSaveClick) return null;
return <Button style={{ marginRight: 10 }} level={ButtonLevel.Primary} disabled={!props.hasChanges} onClick={props.onSaveClick} title={_('OK')}/>;
}
function renderApplyButton() {
if (!props.onApplyClick) return null;
return <Button level={ButtonLevel.Primary} disabled={!props.hasChanges} onClick={props.onApplyClick} title={_('Apply')}/>;
}
return (
<StyledRoot>
<Button
onClick={props.onCancelClick}
level={ButtonLevel.Secondary}
iconName="fa fa-chevron-left"
title={props.backButtonTitle ? props.backButtonTitle : _('Back')}
/>
{ (props.onApplyClick || props.onSaveClick) && (
<div style={{ display: 'flex', flexDirection: 'row', marginLeft: 30 }}>
{renderOkButton()}
{renderApplyButton()}
</div>
)}
</StyledRoot>
);
}

View File

@@ -1,48 +1,63 @@
const React = require('react'); import * as React from 'react';
import SideBar from './SideBar';
import ButtonBar from './ButtonBar';
import Button, { ButtonLevel } from '../Button/Button';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');
const { bridge } = require('electron').remote.require('./bridge');
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const pathUtils = require('lib/path-utils.js'); const pathUtils = require('lib/path-utils.js');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry'); const SyncTargetRegistry = require('lib/SyncTargetRegistry');
const shared = require('lib/components/shared/config-shared.js'); const shared = require('lib/components/shared/config-shared.js');
const ConfigMenuBar = require('./ConfigMenuBar.min.js'); const { bridge } = require('electron').remote.require('./bridge');
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min'); const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min');
const { ClipperConfigScreen } = require('./ClipperConfigScreen.min'); const { ClipperConfigScreen } = require('../ClipperConfigScreen.min');
const { KeymapConfigScreen } = require('./KeymapConfig/KeymapConfigScreen'); const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
class ConfigScreenComponent extends React.Component { class ConfigScreenComponent extends React.Component<any, any> {
constructor() {
super(); rowStyle_:any = null;
constructor(props:any) {
super(props);
shared.init(this); shared.init(this);
this.state.selectedSectionName = 'general'; this.state = {
this.state.screenName = ''; selectedSectionName: 'general',
screenName: '',
this.checkSyncConfig_ = async () => { changedSettingKeys: [],
await shared.checkSyncConfig(this, this.state.settings);
};
this.checkNextcloudAppButton_click = async () => {
this.setState({ showNextcloudAppLog: true });
await shared.checkNextcloudApp(this, this.state.settings);
};
this.showLogButton_click = () => {
this.setState({ showNextcloudAppLog: true });
};
this.nextcloudAppHelpLink_click = () => {
bridge().openExternal('https://joplinapp.org/nextcloud_app');
}; };
this.rowStyle_ = { this.rowStyle_ = {
marginBottom: 10, marginBottom: 10,
}; };
this.configMenuBar_selectionChange = this.configMenuBar_selectionChange.bind(this); this.sideBar_selectionChange = this.sideBar_selectionChange.bind(this);
this.checkSyncConfig_ = this.checkSyncConfig_.bind(this);
this.checkNextcloudAppButton_click = this.checkNextcloudAppButton_click.bind(this);
this.showLogButton_click = this.showLogButton_click.bind(this);
this.nextcloudAppHelpLink_click = this.nextcloudAppHelpLink_click.bind(this);
this.onCancelClick = this.onCancelClick.bind(this);
this.onSaveClick = this.onSaveClick.bind(this);
this.onApplyClick = this.onApplyClick.bind(this);
}
async checkSyncConfig_() {
await shared.checkSyncConfig(this, this.state.settings);
}
async checkNextcloudAppButton_click() {
this.setState({ showNextcloudAppLog: true });
await shared.checkNextcloudApp(this, this.state.settings);
}
showLogButton_click() {
this.setState({ showNextcloudAppLog: true });
}
nextcloudAppHelpLink_click() {
bridge().openExternal('https://joplinapp.org/nextcloud_app');
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
@@ -57,7 +72,7 @@ class ConfigScreenComponent extends React.Component {
} }
} }
sectionByName(name) { sectionByName(name:string) {
const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings }); const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings });
for (const section of sections) { for (const section of sections) {
if (section.name === name) return section; if (section.name === name) return section;
@@ -66,15 +81,15 @@ class ConfigScreenComponent extends React.Component {
throw new Error(`Invalid section name: ${name}`); throw new Error(`Invalid section name: ${name}`);
} }
screenFromName(screenName) { screenFromName(screenName:string) {
if (screenName === 'encryption') return <EncryptionConfigScreen theme={this.props.theme}/>; if (screenName === 'encryption') return <EncryptionConfigScreen themeId={this.props.themeId}/>;
if (screenName === 'server') return <ClipperConfigScreen theme={this.props.theme}/>; if (screenName === 'server') return <ClipperConfigScreen themeId={this.props.themeId}/>;
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.theme}/>; if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.themeId}/>;
throw new Error(`Invalid screen name: ${screenName}`); throw new Error(`Invalid screen name: ${screenName}`);
} }
switchSection(name) { switchSection(name:string) {
const section = this.sectionByName(name); const section = this.sectionByName(name);
let screenName = ''; let screenName = '';
if (section.isScreen) { if (section.isScreen) {
@@ -89,11 +104,11 @@ class ConfigScreenComponent extends React.Component {
this.setState({ selectedSectionName: section.name, screenName: screenName }); this.setState({ selectedSectionName: section.name, screenName: screenName });
} }
configMenuBar_selectionChange(event) { sideBar_selectionChange(event:any) {
this.switchSection(event.section.name); this.switchSection(event.section.name);
} }
keyValueToArray(kv) { keyValueToArray(kv:any) {
const output = []; const output = [];
for (const k in kv) { for (const k in kv) {
if (!kv.hasOwnProperty(k)) continue; if (!kv.hasOwnProperty(k)) continue;
@@ -106,11 +121,11 @@ class ConfigScreenComponent extends React.Component {
return output; return output;
} }
renderSectionDescription(section) { renderSectionDescription(section:any) {
const description = Setting.sectionDescription(section.name); const description = Setting.sectionDescription(section.name);
if (!description) return null; if (!description) return null;
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
return ( return (
<div style={Object.assign({}, theme.textStyle, { marginBottom: 15 })}> <div style={Object.assign({}, theme.textStyle, { marginBottom: 15 })}>
{description} {description}
@@ -118,10 +133,10 @@ class ConfigScreenComponent extends React.Component {
); );
} }
sectionToComponent(key, section, settings, selected) { sectionToComponent(key:string, section:any, settings:any, selected:boolean) {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const createSettingComponents = (advanced) => { const createSettingComponents = (advanced:boolean) => {
const output = []; const output = [];
for (let i = 0; i < section.metadatas.length; i++) { for (let i = 0; i < section.metadatas.length; i++) {
const md = section.metadatas[i]; const md = section.metadatas[i];
@@ -135,9 +150,10 @@ class ConfigScreenComponent extends React.Component {
const settingComps = createSettingComponents(false); const settingComps = createSettingComponents(false);
const advancedSettingComps = createSettingComponents(true); const advancedSettingComps = createSettingComponents(true);
const sectionStyle = { const sectionStyle:any = {
marginTop: 20, marginTop: 20,
marginBottom: 20, marginBottom: 20,
maxWidth: 640,
}; };
if (!selected) sectionStyle.display = 'none'; if (!selected) sectionStyle.display = 'none';
@@ -161,9 +177,12 @@ class ConfigScreenComponent extends React.Component {
settingComps.push( settingComps.push(
<div key="check_sync_config_button" style={this.rowStyle_}> <div key="check_sync_config_button" style={this.rowStyle_}>
<button disabled={this.state.checkSyncConfigResult === 'checking'} style={theme.buttonStyle} onClick={this.checkSyncConfig_}> <Button
{_('Check synchronisation configuration')} title={_('Check synchronisation configuration')}
</button> level={ButtonLevel.Secondary}
disabled={this.state.checkSyncConfigResult === 'checking'}
onClick={this.checkSyncConfig_}
/>
{statusComp} {statusComp}
</div> </div>
); );
@@ -204,9 +223,7 @@ class ConfigScreenComponent extends React.Component {
&nbsp;&nbsp; &nbsp;&nbsp;
{showLogButton} {showLogButton}
&nbsp;&nbsp; &nbsp;&nbsp;
<button disabled={this.state.checkNextcloudAppResult === 'checking'} style={theme.buttonStyle} onClick={this.checkNextcloudAppButton_click}> <Button level={ButtonLevel.Secondary} style={{ display: 'inline-block' }} title={_('Check Status')} disabled={this.state.checkNextcloudAppResult === 'checking'} onClick={this.checkNextcloudAppButton_click}/>
{_('Check Status')}
</button>
&nbsp;&nbsp; &nbsp;&nbsp;
<a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a> <a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a>
{statusComp} {statusComp}
@@ -220,8 +237,17 @@ class ConfigScreenComponent extends React.Component {
if (advancedSettingComps.length) { if (advancedSettingComps.length) {
const iconName = this.state.showAdvancedSettings ? 'fa fa-angle-down' : 'fa fa-angle-right'; const iconName = this.state.showAdvancedSettings ? 'fa fa-angle-down' : 'fa fa-angle-right';
const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 }); // const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 });
advancedSettingsButton = <button onClick={() => shared.advancedSettingsButton_click(this)} style={advancedSettingsButtonStyle}><i style={{ fontSize: 14 }} className={iconName}></i> {_('Show Advanced Settings')}</button>; advancedSettingsButton = (
<div style={{ marginBottom: 10 }}>
<Button
level={ButtonLevel.Secondary}
onClick={() => shared.advancedSettingsButton_click(this)}
iconName={iconName}
title={_('Show Advanced Settings')}
/>
</div>
);
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none'; advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
} }
@@ -235,35 +261,39 @@ class ConfigScreenComponent extends React.Component {
); );
} }
settingToComponent(key, value) { settingToComponent(key:string, value:any) {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const output = null; const output:any = null;
const rowStyle = this.rowStyle_; const rowStyle = {
marginBottom: theme.mainPadding,
};
const labelStyle = Object.assign({}, theme.textStyle, { const labelStyle = Object.assign({}, theme.textStyle, {
display: 'inline-block', display: 'block',
marginRight: 10,
color: theme.color, color: theme.color,
fontSize: theme.fontSize * 1.083333,
fontWeight: 500,
marginBottom: theme.mainPadding / 4,
}); });
const subLabel = Object.assign({}, labelStyle, { const subLabel = Object.assign({}, labelStyle, {
display: 'block',
opacity: 0.7, opacity: 0.7,
marginBottom: Math.round(rowStyle.marginBottom * 0.7), marginBottom: labelStyle.marginBottom,
});
const invisibleLabel = Object.assign({}, labelStyle, {
opacity: 0,
}); });
const checkboxLabelStyle = Object.assign({}, labelStyle, { const checkboxLabelStyle = Object.assign({}, labelStyle, {
marginLeft: 8, marginLeft: 8,
display: 'inline',
backgroundColor: 'transparent',
}); });
const controlStyle = { const controlStyle = {
display: 'inline-block', display: 'inline-block',
color: theme.color, color: theme.color,
fontFamily: theme.fontFamily,
backgroundColor: theme.backgroundColor, backgroundColor: theme.backgroundColor,
}; };
@@ -275,13 +305,19 @@ class ConfigScreenComponent extends React.Component {
}); });
const textInputBaseStyle = Object.assign({}, controlStyle, { const textInputBaseStyle = Object.assign({}, controlStyle, {
fontFamily: theme.fontFamily,
border: '1px solid', border: '1px solid',
padding: '4px 6px', padding: '4px 6px',
borderColor: theme.dividerColor, boxSizing: 'border-box',
borderRadius: 4, borderColor: theme.borderColor4,
borderRadius: 3,
paddingLeft: 6,
paddingRight: 6,
paddingTop: 4,
paddingBottom: 4,
}); });
const updateSettingValue = (key, value) => { const updateSettingValue = (key:string, value:any) => {
// console.info(key + ' = ' + value); // console.info(key + ' = ' + value);
return shared.updateSettingValue(this, key, value); return shared.updateSettingValue(this, key, value);
}; };
@@ -306,7 +342,14 @@ class ConfigScreenComponent extends React.Component {
); );
} }
const selectStyle = Object.assign({}, controlStyle, { height: 22, borderColor: theme.dividerColor }); const selectStyle = Object.assign({}, controlStyle, {
paddingLeft: 6,
paddingRight: 6,
paddingTop: 4,
paddingBottom: 4,
borderColor: theme.borderColor4,
borderRadius: 3,
});
return ( return (
<div key={key} style={rowStyle}> <div key={key} style={rowStyle}>
@@ -316,7 +359,7 @@ class ConfigScreenComponent extends React.Component {
<select <select
value={value} value={value}
style={selectStyle} style={selectStyle}
onChange={event => { onChange={(event:any) => {
updateSettingValue(key, event.target.value); updateSettingValue(key, event.target.value);
}} }}
> >
@@ -330,35 +373,38 @@ class ConfigScreenComponent extends React.Component {
updateSettingValue(key, !value); updateSettingValue(key, !value);
}; };
const checkboxSize = theme.fontSize * 1.1666666666666;
// Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes. // Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes.
// There's probably a better way to do this but can't figure it out. // There's probably a better way to do this but can't figure it out.
return ( return (
<div key={key + value.toString()} style={rowStyle}> <div key={key + value.toString()} style={rowStyle}>
<div style={controlStyle}> <div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
<input <input
id={`setting_checkbox_${key}`} id={`setting_checkbox_${key}`}
type="checkbox" type="checkbox"
checked={!!value} checked={!!value}
onChange={event => { onChange={() => {
onCheckboxClick(event); onCheckboxClick();
}} }}
style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }}
/> />
<label <label
onClick={event => { onClick={() => {
onCheckboxClick(event); onCheckboxClick();
}} }}
style={checkboxLabelStyle} style={{ ...checkboxLabelStyle, marginLeft: 5, marginBottom: 0 }}
htmlFor={`setting_checkbox_${key}`} htmlFor={`setting_checkbox_${key}`}
> >
{md.label()} {md.label()}
</label> </label>
{descriptionComp}
</div> </div>
{descriptionComp}
</div> </div>
); );
} else if (md.type === Setting.TYPE_STRING) { } else if (md.type === Setting.TYPE_STRING) {
const inputStyle = Object.assign({}, textInputBaseStyle, { const inputStyle:any = Object.assign({}, textInputBaseStyle, {
width: '50%', width: '50%',
minWidth: '20em', minWidth: '20em',
}); });
@@ -367,13 +413,13 @@ class ConfigScreenComponent extends React.Component {
if (md.subType === 'file_path_and_args') { if (md.subType === 'file_path_and_args') {
inputStyle.marginBottom = subLabel.marginBottom; inputStyle.marginBottom = subLabel.marginBottom;
const splitCmd = cmdString => { const splitCmd = (cmdString:string) => {
const path = pathUtils.extractExecutablePath(cmdString); const path = pathUtils.extractExecutablePath(cmdString);
const args = cmdString.substr(path.length + 1); const args = cmdString.substr(path.length + 1);
return [pathUtils.unquotePath(path), args]; return [pathUtils.unquotePath(path), args];
}; };
const joinCmd = cmdArray => { const joinCmd = (cmdArray:string[]) => {
if (!cmdArray[0] && !cmdArray[1]) return ''; if (!cmdArray[0] && !cmdArray[1]) return '';
let cmdString = pathUtils.quotePath(cmdArray[0]); let cmdString = pathUtils.quotePath(cmdArray[0]);
if (!cmdString) cmdString = '""'; if (!cmdString) cmdString = '""';
@@ -381,13 +427,13 @@ class ConfigScreenComponent extends React.Component {
return cmdString; return cmdString;
}; };
const onPathChange = event => { const onPathChange = (event:any) => {
const cmd = splitCmd(this.state.settings[key]); const cmd = splitCmd(this.state.settings[key]);
cmd[0] = event.target.value; cmd[0] = event.target.value;
updateSettingValue(key, joinCmd(cmd)); updateSettingValue(key, joinCmd(cmd));
}; };
const onArgsChange = event => { const onArgsChange = (event:any) => {
const cmd = splitCmd(this.state.settings[key]); const cmd = splitCmd(this.state.settings[key]);
cmd[1] = event.target.value; cmd[1] = event.target.value;
updateSettingValue(key, joinCmd(cmd)); updateSettingValue(key, joinCmd(cmd));
@@ -405,53 +451,51 @@ class ConfigScreenComponent extends React.Component {
return ( return (
<div key={key} style={rowStyle}> <div key={key} style={rowStyle}>
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<div style={{ flex: 0, whiteSpace: 'nowrap' }}>
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
</div>
<div style={{ flex: 0 }}>
<div style={subLabel}>Path:</div>
<div style={subLabel}>Arguments:</div>
</div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom }}> <div style={{ ...rowStyle, marginBottom: 5 }}>
<div style={subLabel}>Path:</div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom }}>
<input
type={inputType}
style={Object.assign({}, inputStyle, { marginBottom: 0, marginRight: 5 })}
onChange={(event:any) => {
onPathChange(event);
}}
value={cmd[0]}
/>
<Button
level={ButtonLevel.Secondary}
title={_('Browse...')}
onClick={browseButtonClick}
/>
</div>
</div>
<div style={{ ...rowStyle, marginBottom: 5 }}>
<div style={subLabel}>Arguments:</div>
<input <input
type={inputType} type={inputType}
style={Object.assign({}, inputStyle, { marginBottom: 0 })} style={inputStyle}
onChange={event => { onChange={(event:any) => {
onPathChange(event); onArgsChange(event);
}} }}
value={cmd[0]} value={cmd[1]}
/> />
<button onClick={browseButtonClick} style={Object.assign({}, theme.buttonStyle, { marginLeft: 5 })}> <div style={{ width: inputStyle.width }}>
{_('Browse...')} {descriptionComp}
</button> </div>
</div> </div>
<input
type={inputType}
style={inputStyle}
onChange={event => {
onArgsChange(event);
}}
value={cmd[1]}
/>
</div> </div>
</div> </div>
<div style={{ display: 'flex' }}>
<div style={{ flex: 0, whiteSpace: 'nowrap' }}>
<div style={invisibleLabel}>
<label>{md.label()}</label>
</div>
</div>
<div style={{ flex: 1 }}>{descriptionComp}</div>
</div>
</div> </div>
); );
} else { } else {
const onTextChange = event => { const onTextChange = (event:any) => {
updateSettingValue(key, event.target.value); updateSettingValue(key, event.target.value);
}; };
@@ -464,23 +508,25 @@ class ConfigScreenComponent extends React.Component {
type={inputType} type={inputType}
style={inputStyle} style={inputStyle}
value={this.state.settings[key]} value={this.state.settings[key]}
onChange={event => { onChange={(event:any) => {
onTextChange(event); onTextChange(event);
}} }}
/> />
{descriptionComp} <div style={{ width: inputStyle.width }}>
{descriptionComp}
</div>
</div> </div>
); );
} }
} else if (md.type === Setting.TYPE_INT) { } else if (md.type === Setting.TYPE_INT) {
const onNumChange = event => { const onNumChange = (event:any) => {
updateSettingValue(key, event.target.value); updateSettingValue(key, event.target.value);
}; };
const label = [md.label()]; const label = [md.label()];
if (md.unitLabel) label.push(`(${md.unitLabel()})`); if (md.unitLabel) label.push(`(${md.unitLabel()})`);
const inputStyle = Object.assign({}, textInputBaseStyle); const inputStyle:any = Object.assign({}, textInputBaseStyle);
return ( return (
<div key={key} style={rowStyle}> <div key={key} style={rowStyle}>
@@ -491,7 +537,7 @@ class ConfigScreenComponent extends React.Component {
type="number" type="number"
style={inputStyle} style={inputStyle}
value={this.state.settings[key]} value={this.state.settings[key]}
onChange={event => { onChange={(event:any) => {
onNumChange(event); onNumChange(event);
}} }}
min={md.minimum} min={md.minimum}
@@ -502,20 +548,12 @@ class ConfigScreenComponent extends React.Component {
</div> </div>
); );
} else if (md.type === Setting.TYPE_BUTTON) { } else if (md.type === Setting.TYPE_BUTTON) {
const theme = themeStyle(this.props.theme);
const buttonStyle = Object.assign({}, theme.buttonStyle, {
display: 'inline-block',
marginRight: 10,
});
return ( return (
<div key={key} style={rowStyle}> <div key={key} style={rowStyle}>
<div style={labelStyle}> <div style={labelStyle}>
<label>{md.label()}</label> <label>{md.label()}</label>
</div> </div>
<button style={buttonStyle} onClick={md.onClick}> <Button level={ButtonLevel.Secondary} title={_('Edit')} onClick={md.onClick}/>
{_('Edit')}
</button>
{descriptionComp} {descriptionComp}
</div> </div>
); );
@@ -544,46 +582,35 @@ class ConfigScreenComponent extends React.Component {
} }
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = Object.assign( const style = Object.assign({},
{
backgroundColor: theme.backgroundColor,
},
this.props.style, this.props.style,
{ {
overflow: 'hidden', overflow: 'hidden',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
backgroundColor: theme.backgroundColor3,
} }
); );
const settings = this.state.settings; const settings = this.state.settings;
const containerStyle = Object.assign({}, theme.containerStyle, { padding: 10, paddingTop: 0, display: 'flex', flex: 1 }); const containerStyle = {
overflow: 'auto',
padding: theme.configScreenPadding,
paddingTop: 0,
display: 'flex',
flex: 1,
};
const hasChanges = this.hasChanges(); const hasChanges = this.hasChanges();
const buttonStyle = Object.assign({}, theme.buttonStyle, {
display: 'inline-block',
marginRight: 10,
});
const buttonStyleApprove = Object.assign({}, buttonStyle, {
opacity: hasChanges ? 1 : theme.disabledOpacity,
});
const settingComps = shared.settingsToComponents2(this, 'desktop', settings, this.state.selectedSectionName); const settingComps = shared.settingsToComponents2(this, 'desktop', settings, this.state.selectedSectionName);
const buttonBarStyle = { // screenComp is a custom config screen, such as the encryption config screen or keymap config screen.
display: 'flex', // These screens handle their own loading/saving of settings and have bespoke rendering.
alignItems: 'center', // When screenComp is null, it means we are viewing the regular settings.
padding: 10,
borderTopWidth: 1,
borderTopStyle: 'solid',
borderTopColor: theme.dividerColor,
};
const screenComp = this.state.screenName ? <div style={{ overflow: 'scroll', flex: 1 }}>{this.screenFromName(this.state.screenName)}</div> : null; const screenComp = this.state.screenName ? <div style={{ overflow: 'scroll', flex: 1 }}>{this.screenFromName(this.state.screenName)}</div> : null;
if (screenComp) containerStyle.display = 'none'; if (screenComp) containerStyle.display = 'none';
@@ -591,45 +618,35 @@ class ConfigScreenComponent extends React.Component {
const sections = shared.settingsSections({ device: 'desktop', settings }); const sections = shared.settingsSections({ device: 'desktop', settings });
return ( return (
<div style={style}> <div style={{ display: 'flex', flexDirection: 'row' }}>
<ConfigMenuBar <SideBar
selection={this.state.selectedSectionName} selection={this.state.selectedSectionName}
onSelectionChange={this.configMenuBar_selectionChange} onSelectionChange={this.sideBar_selectionChange}
sections={sections} sections={sections}
theme={this.props.theme}
/> />
{screenComp} <div style={style}>
<div style={containerStyle}>{settingComps}</div> {screenComp}
<div style={buttonBarStyle}> <div style={containerStyle}>{settingComps}</div>
<button <ButtonBar
onClick={() => { hasChanges={hasChanges}
this.onCancelClick(); backButtonTitle={hasChanges && !screenComp ? _('Cancel') : _('Back')}
}} onCancelClick={this.onCancelClick}
style={buttonStyle} onSaveClick={screenComp ? null : this.onSaveClick}
> onApplyClick={screenComp ? null : this.onApplyClick}
<i style={theme.buttonIconStyle} className={'fa fa-chevron-left'}></i> />
{hasChanges && !screenComp ? _('Cancel') : _('Back')}
</button>
{ !screenComp && (
<div>
<button disabled={!hasChanges} onClick={() => { this.onSaveClick(); }} style={buttonStyleApprove}>{_('OK')}</button>
<button disabled={!hasChanges} onClick={() => { this.onApplyClick(); }} style={buttonStyleApprove}>{_('Apply')}</button>
</div>
)}
</div> </div>
</div> </div>
); );
} }
} }
const mapStateToProps = state => { const mapStateToProps = (state:any) => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
settings: state.settings, settings: state.settings,
locale: state.settings.locale, locale: state.settings.locale,
}; };
}; };
const ConfigScreen = connect(mapStateToProps)(ConfigScreenComponent); export default connect(mapStateToProps)(ConfigScreenComponent);
module.exports = { ConfigScreen };

View File

@@ -0,0 +1,74 @@
import * as React from 'react';
const styled = require('styled-components').default;
const Setting = require('lib/models/Setting');
interface Props {
selection: string,
onSelectionChange: Function,
sections: any[],
}
export const StyledRoot = styled.div`
display: flex;
background-color: ${(props:any) => props.theme.backgroundColor2};
flex-direction: column;
`;
export const StyledListItem = styled.a`
box-sizing: border-box;
display: flex;
flex-direction: row;
padding: ${(props:any) => props.theme.mainPadding}px;
background: ${(props:any) => props.selected ? props.theme.selectedColor2 : 'none'};
transition: 0.1s;
text-decoration: none;
cursor: default;
opacity: ${(props:any) => props.selected ? 1 : 0.8};
&:hover {
background-color: ${(props:any) => props.theme.backgroundColorHover2};
}
`;
export const StyledListItemLabel = styled.span`
font-size: ${(props:any) => Math.round(props.theme.fontSize * 1.2)}px;
font-weight: 500;
color: ${(props:any) => props.theme.color2};
white-space: nowrap;
display: flex;
flex: 1;
align-items: center;
user-select: none;
`;
export const StyledListItemIcon = styled.i`
font-size: ${(props:any) => Math.round(props.theme.fontSize * 1.4)}px;
color: ${(props:any) => props.theme.color2};
margin-right: ${(props:any) => props.theme.mainPadding / 1.5}px;
`;
export default function SideBar(props:Props) {
const buttons:any[] = [];
function renderButton(section:any) {
const selected = props.selection === section.name;
return (
<StyledListItem key={section.name} selected={selected} onClick={() => { props.onSelectionChange({ section: section }); }}>
<StyledListItemIcon className={Setting.sectionNameToIcon(section.name)} />
<StyledListItemLabel>
{Setting.sectionNameToLabel(section.name)}
</StyledListItemLabel>
</StyledListItem>
);
}
for (const section of props.sections) {
buttons.push(renderButton(section));
}
return (
<StyledRoot>
{buttons}
</StyledRoot>
);
}

View File

@@ -3,7 +3,7 @@ const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
function DialogButtonRow(props) { function DialogButtonRow(props) {
const theme = themeStyle(props.theme); const theme = themeStyle(props.themeId);
const okButton_click = () => { const okButton_click = () => {
if (props.onClick) props.onClick({ buttonName: 'ok' }); if (props.onClick) props.onClick({ buttonName: 'ok' });

View File

@@ -1,16 +1,24 @@
const React = require('react'); import * as React from 'react';
import ButtonBar from './ConfigScreen/ButtonBar';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
const { Header } = require('./Header/Header.min.js');
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const Shared = require('lib/components/shared/dropbox-login-shared'); const Shared = require('lib/components/shared/dropbox-login-shared');
class DropboxLoginScreenComponent extends React.Component { interface Props {
constructor() { themeId: string,
super(); }
this.shared_ = new Shared(this, msg => bridge().showInfoMessageBox(msg), msg => bridge().showErrorMessageBox(msg)); class DropboxLoginScreenComponent extends React.Component<any, any> {
shared_:any;
constructor(props:Props) {
super(props);
this.shared_ = new Shared(this, (msg:string) => bridge().showInfoMessageBox(msg), (msg:string) => bridge().showErrorMessageBox(msg));
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
@@ -19,19 +27,18 @@ class DropboxLoginScreenComponent extends React.Component {
render() { render() {
const style = this.props.style; const style = this.props.style;
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width });
const containerStyle = Object.assign({}, theme.containerStyle, { const containerStyle = Object.assign({}, theme.containerStyle, {
padding: theme.margin, padding: theme.configScreenPadding,
height: style.height - theme.headerHeight - theme.margin * 2, height: style.height - theme.margin * 2,
flex: 1,
}); });
const inputStyle = Object.assign({}, theme.inputStyle, { width: 500 }); const inputStyle = Object.assign({}, theme.inputStyle, { width: 500 });
return ( return (
<div> <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Header style={headerStyle} />
<div style={containerStyle}> <div style={containerStyle}>
<p style={theme.textStyle}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</p> <p style={theme.textStyle}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</p>
<p style={theme.textStyle}>{_('Step 1: Open this URL in your browser to authorise the application:')}</p> <p style={theme.textStyle}>{_('Step 1: Open this URL in your browser to authorise the application:')}</p>
@@ -46,17 +53,18 @@ class DropboxLoginScreenComponent extends React.Component {
{_('Submit')} {_('Submit')}
</button> </button>
</div> </div>
<ButtonBar
onCancelClick={() => this.props.dispatch({ type: 'NAV_BACK' })}
/>
</div> </div>
); );
} }
} }
const mapStateToProps = state => { const mapStateToProps = (state:any) => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
}; };
}; };
const DropboxLoginScreen = connect(mapStateToProps)(DropboxLoginScreenComponent); export default connect(mapStateToProps)(DropboxLoginScreenComponent);
module.exports = { DropboxLoginScreen };

View File

@@ -35,7 +35,7 @@ class EncryptionConfigScreenComponent extends React.Component {
} }
renderMasterKey(mk) { renderMasterKey(mk) {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const passwordStyle = { const passwordStyle = {
color: theme.color, color: theme.color,
@@ -80,7 +80,7 @@ class EncryptionConfigScreenComponent extends React.Component {
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(this.props.masterKeys); const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(this.props.masterKeys);
if (!needUpgradeMasterKeys.length) return null; if (!needUpgradeMasterKeys.length) return null;
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const rows = []; const rows = [];
const comp = this; const comp = this;
@@ -114,7 +114,7 @@ class EncryptionConfigScreenComponent extends React.Component {
renderReencryptData() { renderReencryptData() {
if (!shim.isElectron()) return null; if (!shim.isElectron()) return null;
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const buttonLabel = _('Re-encrypt data'); const buttonLabel = _('Re-encrypt data');
const intro = this.props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.'); const intro = this.props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
@@ -139,13 +139,13 @@ class EncryptionConfigScreenComponent extends React.Component {
} }
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const masterKeys = this.props.masterKeys; const masterKeys = this.props.masterKeys;
const containerPadding = 10;
const containerStyle = Object.assign({}, theme.containerStyle, { const containerStyle = Object.assign({}, theme.containerStyle, {
padding: containerPadding, padding: theme.configScreenPadding,
overflow: 'auto', overflow: 'auto',
backgroundColor: theme.backgroundColor3,
}); });
const mkComps = []; const mkComps = [];
@@ -289,7 +289,7 @@ class EncryptionConfigScreenComponent extends React.Component {
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
masterKeys: state.masterKeys, masterKeys: state.masterKeys,
passwords: state.settings['encryption.passwordCache'], passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'], encryptionEnabled: state.settings['encryption.enabled'],

View File

@@ -1,329 +0,0 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
const CommandService = require('lib/services/CommandService').default;
const Setting = require('lib/models/Setting.js');
const commands = [
require('./commands/focusSearch'),
];
class HeaderComponent extends React.Component {
constructor() {
super();
this.state = {
searchQuery: '',
showSearchUsageLink: false,
showButtonLabels: true,
};
for (const command of commands) {
CommandService.instance().registerRuntime(command.declaration.name, command.runtime(this));
}
this.scheduleSearchChangeEventIid_ = null;
this.searchOnQuery_ = null;
this.searchElement_ = null;
const triggerOnQuery = query => {
clearTimeout(this.scheduleSearchChangeEventIid_);
if (this.searchOnQuery_) this.searchOnQuery_(query, Setting.value('db.fuzzySearchEnabled'));
this.scheduleSearchChangeEventIid_ = null;
};
this.search_onChange = event => {
this.setState({ searchQuery: event.target.value });
if (this.scheduleSearchChangeEventIid_) clearTimeout(this.scheduleSearchChangeEventIid_);
this.scheduleSearchChangeEventIid_ = setTimeout(() => {
triggerOnQuery(this.state.searchQuery);
}, 500);
};
this.search_onClear = () => {
this.resetSearch();
if (this.searchElement_) this.searchElement_.focus();
};
this.search_onFocus = () => {
if (this.hideSearchUsageLinkIID_) {
clearTimeout(this.hideSearchUsageLinkIID_);
this.hideSearchUsageLinkIID_ = null;
}
this.setState({ showSearchUsageLink: true });
};
this.search_onBlur = () => {
if (this.hideSearchUsageLinkIID_) return;
this.hideSearchUsageLinkIID_ = setTimeout(() => {
this.setState({ showSearchUsageLink: false });
}, 5000);
};
this.search_keyDown = event => {
if (event.keyCode === 27) {
// ESCAPE
this.resetSearch();
}
};
this.resetSearch = () => {
this.setState({ searchQuery: '' });
triggerOnQuery('');
};
this.searchUsageLink_click = () => {
bridge().openExternal('https://joplinapp.org/#searching');
};
}
componentDidUpdate(prevProps) {
if (prevProps.notesParentType !== this.props.notesParentType && this.props.notesParentType !== 'Search' && this.state.searchQuery) {
this.resetSearch();
}
if (this.props.zoomFactor !== prevProps.zoomFactor || this.props.size !== prevProps.size) {
this.determineButtonLabelState();
}
}
componentDidMount() {
this.determineButtonLabelState();
}
componentWillUnmount() {
if (this.hideSearchUsageLinkIID_) {
clearTimeout(this.hideSearchUsageLinkIID_);
this.hideSearchUsageLinkIID_ = null;
}
for (const command of commands) {
CommandService.instance().unregisterRuntime(command.declaration.name);
}
}
determineButtonLabelState() {
const mediaQuery = window.matchMedia(`(max-width: ${780 * this.props.zoomFactor}px)`);
const showButtonLabels = !mediaQuery.matches;
if (this.state.showButtonLabels !== showButtonLabels) {
this.setState({
showButtonLabels: !mediaQuery.matches,
});
}
}
back_click() {
this.props.dispatch({ type: 'NAV_BACK' });
}
makeButton(key, style, options) {
// TODO: "tab" type is not finished
if (options.type === 'tab') {
const buttons = [];
for (let i = 0; i < options.items.length; i++) {
const item = options.items[i];
buttons.push(this.makeButton(key + item.title, style, Object.assign({}, options, {
title: item.title,
type: 'button',
})));
}
return <span style={{ display: 'flex', flexDirection: 'row' }}>{buttons}</span>;
}
const theme = themeStyle(this.props.theme);
let icon = null;
if (options.iconName) {
const iconStyle = {
fontSize: Math.round(style.fontSize * 1.1),
color: theme.iconColor,
};
if (options.title) iconStyle.marginRight = 5;
if ('undefined' != typeof options.iconRotation) {
iconStyle.transition = 'transform 0.15s ease-in-out';
iconStyle.transform = `rotate(${options.iconRotation}deg)`;
}
icon = <i style={iconStyle} className={`fas ${options.iconName}`}></i>;
}
const isEnabled = !('enabled' in options) || options.enabled;
const classes = ['button'];
if (!isEnabled) classes.push('disabled');
const finalStyle = Object.assign({}, style, {
opacity: isEnabled ? 1 : 0.4,
});
const title = options.title ? options.title : '';
if (options.type === 'checkbox' && options.checked) {
finalStyle.backgroundColor = theme.selectedColor;
finalStyle.borderWidth = 1;
finalStyle.borderTopColor = theme.selectedDividerColor;
finalStyle.borderLeftColor = theme.selectedDividerColor;
finalStyle.borderTopStyle = 'solid';
finalStyle.borderLeftStyle = 'solid';
finalStyle.paddingLeft++;
finalStyle.paddingTop++;
finalStyle.paddingBottom--;
finalStyle.paddingRight--;
finalStyle.boxSizing = 'border-box';
}
return (
<a
className={classes.join(' ')}
style={finalStyle}
key={key}
href="#"
title={title}
onClick={() => {
if (isEnabled) options.onClick();
}}
>
{icon}
<span className="title" style={{
display: this.state.showButtonLabels ? 'inline-block' : 'none',
}}>{title}</span>
</a>
);
}
makeSearch(key, style, options, state) {
const theme = themeStyle(this.props.theme);
const inputStyle = {
display: 'flex',
flex: 1,
marginLeft: 10,
paddingLeft: 6,
paddingRight: 6,
paddingTop: 1, // vertical alignment with buttons
paddingBottom: 0, // vertical alignment with buttons
height: style.fontSize * 2,
maxWidth: 300,
color: style.color,
fontSize: style.fontSize,
fontFamily: style.fontFamily,
backgroundColor: style.searchColor,
border: '1px solid',
borderColor: style.dividerColor,
};
const searchButton = {
paddingLeft: 4,
paddingRight: 4,
paddingTop: 2,
paddingBottom: 2,
textDecoration: 'none',
};
const iconStyle = {
display: 'flex',
fontSize: Math.round(style.fontSize) * 1.2,
color: style.color,
};
const containerStyle = {
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
alignItems: 'center',
};
const iconName = state.searchQuery ? 'fa-times' : 'fa-search';
const icon = <i style={iconStyle} className={`fas ${iconName}`}></i>;
if (options.onQuery) this.searchOnQuery_ = options.onQuery;
const usageLink = !this.state.showSearchUsageLink ? null : (
<a onClick={this.searchUsageLink_click} style={theme.urlStyle} href="#">
{_('Usage')}
</a>
);
return (
<div key={key} style={containerStyle}>
<input type="text" style={inputStyle} placeholder={options.title} value={state.searchQuery} onChange={this.search_onChange} ref={elem => (this.searchElement_ = elem)} onFocus={this.search_onFocus} onBlur={this.search_onBlur} onKeyDown={this.search_keyDown} />
<a href="#" style={searchButton} onClick={this.search_onClear}>
{icon}
</a>
{usageLink}
</div>
);
}
render() {
const style = Object.assign({}, this.props.style);
const theme = themeStyle(this.props.theme);
const showBackButton = this.props.showBackButton === undefined || this.props.showBackButton === true;
style.height = theme.headerHeight;
style.display = 'flex';
style.flexDirection = 'row';
style.borderBottom = `1px solid ${theme.dividerColor}`;
style.boxSizing = 'border-box';
const items = [];
const itemStyle = {
height: theme.headerHeight,
display: 'flex',
alignItems: 'center',
paddingTop: 1,
paddingBottom: 1,
paddingLeft: theme.headerButtonHPadding,
paddingRight: theme.headerButtonHPadding,
color: theme.color,
searchColor: theme.backgroundColor,
dividerColor: theme.dividerColor,
textDecoration: 'none',
fontFamily: theme.fontFamily,
fontSize: theme.fontSize,
boxSizing: 'border-box',
cursor: 'default',
whiteSpace: 'nowrap',
userSelect: 'none',
};
if (showBackButton) {
items.push(this.makeButton('back', itemStyle, { title: _('Back'), onClick: () => this.back_click(), iconName: 'fa-chevron-left ' }));
}
if (this.props.items) {
for (let i = 0; i < this.props.items.length; i++) {
const item = this.props.items[i];
if (item.type === 'search') {
items.push(this.makeSearch(`item_${i}_search`, itemStyle, item, this.state));
} else {
items.push(this.makeButton(`item_${i}_${item.title}`, itemStyle, item));
}
}
}
return (
<div className="header" style={style}>
{items}
</div>
);
}
}
const mapStateToProps = state => {
return {
theme: state.settings.theme,
notesParentType: state.notesParentType,
size: state.windowContentSize,
zoomFactor: state.settings.windowContentZoomFactor / 100,
};
};
const Header = connect(mapStateToProps)(HeaderComponent);
module.exports = { Header };

View File

@@ -14,7 +14,7 @@ class HelpButtonComponent extends React.Component {
} }
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = Object.assign({}, this.props.style, { color: theme.color, textDecoration: 'none' }); const style = Object.assign({}, this.props.style, { color: theme.color, textDecoration: 'none' });
const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 }; const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 };
const extraProps = {}; const extraProps = {};
@@ -29,7 +29,7 @@ class HelpButtonComponent extends React.Component {
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
}; };
}; };

View File

@@ -4,7 +4,7 @@ const { themeStyle } = require('lib/theme');
class IconButton extends React.Component { class IconButton extends React.Component {
render() { render() {
const style = this.props.style; const style = this.props.style;
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const iconStyle = { const iconStyle = {
color: theme.color, color: theme.color,
fontSize: theme.fontSize * 1.4, fontSize: theme.fontSize * 1.4,

View File

@@ -1,7 +1,6 @@
const React = require('react'); const React = require('react');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const Folder = require('lib/models/Folder.js'); const Folder = require('lib/models/Folder.js');
const { Header } = require('./Header/Header.min.js');
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const { filename, basename } = require('lib/path-utils.js'); const { filename, basename } = require('lib/path-utils.js');
@@ -94,8 +93,7 @@ class ImportScreenComponent extends React.Component {
} }
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = this.props.style;
const messages = this.uniqueMessages(); const messages = this.uniqueMessages();
const messagesStyle = { const messagesStyle = {
@@ -105,10 +103,6 @@ class ImportScreenComponent extends React.Component {
backgroundColor: theme.backgroundColor, backgroundColor: theme.backgroundColor,
}; };
const headerStyle = {
width: style.width,
};
const messageComps = []; const messageComps = [];
for (let i = 0; i < messages.length; i++) { for (let i = 0; i < messages.length; i++) {
messageComps.push(<div key={messages[i].key}>{messages[i].text}</div>); messageComps.push(<div key={messages[i].key}>{messages[i].text}</div>);
@@ -116,7 +110,6 @@ class ImportScreenComponent extends React.Component {
return ( return (
<div style={{}}> <div style={{}}>
<Header style={headerStyle} />
<div style={messagesStyle}>{messageComps}</div> <div style={messagesStyle}>{messageComps}</div>
</div> </div>
); );
@@ -125,7 +118,7 @@ class ImportScreenComponent extends React.Component {
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
}; };
}; };

View File

@@ -36,6 +36,10 @@ class ItemList extends React.Component {
return this.listRef.current ? this.listRef.current.offsetTop : 0; return this.listRef.current ? this.listRef.current.offsetTop : 0;
} }
offsetScroll() {
return this.scrollTop_;
}
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.updateStateItemIndexes(); this.updateStateItemIndexes();
} }

View File

@@ -62,7 +62,7 @@ export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
const keymapFile = await shim.fsDriver().readFile(actualFilePath, 'utf-8'); const keymapFile = await shim.fsDriver().readFile(actualFilePath, 'utf-8');
overrideKeymapItems(JSON.parse(keymapFile)); overrideKeymapItems(JSON.parse(keymapFile));
} catch (err) { } catch (err) {
bridge().showErrorMessageBox(`${_('An unexpected error occured while importing the keymap!')}\n${err.message}`); bridge().showErrorMessageBox(_('Error: %s', err.message));
} }
} }
}; };

View File

@@ -5,7 +5,8 @@ export default function styles(themeId: number) {
return { return {
container: { container: {
...theme.containerStyle, ...theme.containerStyle,
padding: 16, padding: theme.configScreenPadding,
backgroundColor: theme.backgroundColor3,
}, },
actionsContainer: { actionsContainer: {
display: 'flex', display: 'flex',

View File

@@ -1,24 +1,26 @@
const React = require('react'); import * as React from 'react';
import ResizableLayout, { findItemByKey, LayoutItem, LayoutItemDirection } from '../ResizableLayout/ResizableLayout';
import NoteList from '../NoteList/NoteList.js';
import NoteEditor from '../NoteEditor/NoteEditor.js';
import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog.js';
import ShareNoteDialog from '../ShareNoteDialog.js';
import NoteListControls from '../NoteListControls/NoteListControls.js';
import CommandService from 'lib/services/CommandService';
const produce = require('immer').default;
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { Header } = require('../Header/Header.min.js'); const { SideBar } = require('../SideBar/SideBar.js');
const { SideBar } = require('../SideBar/SideBar.min.js');
const { NoteList } = require('../NoteList/NoteList.min.js');
const NoteEditor = require('../NoteEditor/NoteEditor.js').default;
const { stateUtils } = require('lib/reducer.js'); const { stateUtils } = require('lib/reducer.js');
const { PromptDialog } = require('../PromptDialog.min.js'); const { PromptDialog } = require('../PromptDialog.min.js');
const NoteContentPropertiesDialog = require('../NoteContentPropertiesDialog.js').default;
const NotePropertiesDialog = require('../NotePropertiesDialog.min.js'); const NotePropertiesDialog = require('../NotePropertiesDialog.min.js');
const ShareNoteDialog = require('../ShareNoteDialog.js').default;
const InteropServiceHelper = require('../../InteropServiceHelper.js'); const InteropServiceHelper = require('../../InteropServiceHelper.js');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');
const { shim } = require('lib/shim'); const { shim } = require('lib/shim');
const { themeStyle } = require('lib/theme.js'); const { themeStyle } = require('lib/theme.js');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
const VerticalResizer = require('../VerticalResizer.min');
const PluginManager = require('lib/services/PluginManager'); const PluginManager = require('lib/services/PluginManager');
const EncryptionService = require('lib/services/EncryptionService'); const EncryptionService = require('lib/services/EncryptionService');
const CommandService = require('lib/services/CommandService').default;
const ipcRenderer = require('electron').ipcRenderer; const ipcRenderer = require('electron').ipcRenderer;
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
@@ -28,7 +30,7 @@ const commands = [
require('./commands/hideModalMessage'), require('./commands/hideModalMessage'),
require('./commands/moveToFolder'), require('./commands/moveToFolder'),
require('./commands/newNote'), require('./commands/newNote'),
require('./commands/newNotebook'), require('./commands/newFolder'),
require('./commands/newTodo'), require('./commands/newTodo'),
require('./commands/print'), require('./commands/print'),
require('./commands/renameFolder'), require('./commands/renameFolder'),
@@ -40,14 +42,76 @@ const commands = [
require('./commands/showNoteContentProperties'), require('./commands/showNoteContentProperties'),
require('./commands/showNoteProperties'), require('./commands/showNoteProperties'),
require('./commands/showShareNoteDialog'), require('./commands/showShareNoteDialog'),
require('./commands/toggleEditors'),
require('./commands/toggleNoteList'), require('./commands/toggleNoteList'),
require('./commands/toggleSidebar'), require('./commands/toggleSidebar'),
require('./commands/toggleVisiblePanes'), require('./commands/toggleVisiblePanes'),
]; ];
class MainScreenComponent extends React.Component { class MainScreenComponent extends React.Component<any, any> {
constructor() {
super(); waitForNotesSavedIID_:any;
isPrinting_:boolean;
styleKey_:string;
styles_:any;
promptOnClose_:Function;
constructor(props:any) {
super(props);
const rootLayoutSize = this.rootLayoutSize();
const theme = themeStyle(props.themeId);
const sideBarMinWidth = 200;
const layout:LayoutItem = {
key: 'root',
direction: LayoutItemDirection.Row,
resizable: false,
width: rootLayoutSize.width,
height: rootLayoutSize.height,
children: [
{
key: 'sidebarColumn',
direction: LayoutItemDirection.Column,
resizable: true,
width: Setting.value('style.sidebar.width') < sideBarMinWidth ? sideBarMinWidth : Setting.value('style.sidebar.width'),
visible: Setting.value('sidebarVisibility'),
minWidth: sideBarMinWidth,
children: [
{
key: 'sideBar',
},
],
},
{
key: 'noteListColumn',
direction: LayoutItemDirection.Column,
resizable: true,
width: Setting.value('style.noteList.width') < sideBarMinWidth ? sideBarMinWidth : Setting.value('style.noteList.width'),
visible: Setting.value('noteListVisibility'),
minWidth: sideBarMinWidth,
children: [
{
height: theme.topRowHeight,
key: 'noteListControls',
},
{
key: 'noteList',
},
],
},
{
key: 'editorColumn',
direction: LayoutItemDirection.Column,
resizable: false,
children: [
{
key: 'editor',
},
],
},
],
};
this.state = { this.state = {
promptOptions: null, promptOptions: null,
@@ -58,6 +122,7 @@ class MainScreenComponent extends React.Component {
notePropertiesDialogOptions: {}, notePropertiesDialogOptions: {},
noteContentPropertiesDialogOptions: {}, noteContentPropertiesDialogOptions: {},
shareNoteDialogOptions: {}, shareNoteDialogOptions: {},
layout: layout,
}; };
this.registerCommands(); this.registerCommands();
@@ -70,6 +135,16 @@ class MainScreenComponent extends React.Component {
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this); this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
this.sidebar_onDrag = this.sidebar_onDrag.bind(this); this.sidebar_onDrag = this.sidebar_onDrag.bind(this);
this.noteList_onDrag = this.noteList_onDrag.bind(this); this.noteList_onDrag = this.noteList_onDrag.bind(this);
this.resizableLayout_resize = this.resizableLayout_resize.bind(this);
this.resizableLayout_renderItem = this.resizableLayout_renderItem.bind(this);
this.window_resize = this.window_resize.bind(this);
this.rowHeight = this.rowHeight.bind(this);
window.addEventListener('resize', this.window_resize);
}
window_resize() {
this.updateRootLayoutSize();
} }
setupAppCloseHandling() { setupAppCloseHandling() {
@@ -103,11 +178,11 @@ class MainScreenComponent extends React.Component {
}); });
} }
sidebar_onDrag(event) { sidebar_onDrag(event:any) {
Setting.setValue('style.sidebar.width', this.props.sidebarWidth + event.deltaX); Setting.setValue('style.sidebar.width', this.props.sidebarWidth + event.deltaX);
} }
noteList_onDrag(event) { noteList_onDrag(event:any) {
Setting.setValue('style.noteList.width', Setting.value('style.noteList.width') + event.deltaX); Setting.setValue('style.noteList.width', Setting.value('style.noteList.width') + event.deltaX);
} }
@@ -123,13 +198,13 @@ class MainScreenComponent extends React.Component {
this.setState({ shareNoteDialogOptions: {} }); this.setState({ shareNoteDialogOptions: {} });
} }
commandService_commandsEnabledStateChange(event) { commandService_commandsEnabledStateChange(event:any) {
const buttonCommandNames = [ const buttonCommandNames = [
'toggleSidebar', 'toggleSidebar',
'toggleNoteList', 'toggleNoteList',
'newNote', 'newNote',
'newTodo', 'newTodo',
'newNotebook', 'newFolder',
'toggleVisiblePanes', 'toggleVisiblePanes',
]; ];
@@ -141,13 +216,61 @@ class MainScreenComponent extends React.Component {
} }
} }
updateRootLayoutSize() {
this.setState({ layout: produce(this.state.layout, (draftState:any) => {
const s = this.rootLayoutSize();
draftState.width = s.width;
draftState.height = s.height;
}) });
}
componentDidUpdate(prevProps:any, prevState:any) {
if (this.props.noteListVisibility !== prevProps.noteListVisibility || this.props.sidebarVisibility !== prevProps.sidebarVisibility) {
this.setState({ layout: produce(this.state.layout, (draftState:any) => {
const noteListColumn = findItemByKey(draftState, 'noteListColumn');
noteListColumn.visible = this.props.noteListVisibility;
const sidebarColumn = findItemByKey(draftState, 'sidebarColumn');
sidebarColumn.visible = this.props.sidebarVisibility;
}) });
}
if (prevProps.style.width !== this.props.style.width || prevProps.style.height !== this.props.style.height) {
this.updateRootLayoutSize();
}
if (this.state.notePropertiesDialogOptions !== prevState.notePropertiesDialogOptions) {
this.props.dispatch({
type: this.state.notePropertiesDialogOptions && this.state.notePropertiesDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE',
name: 'noteProperties',
});
}
if (this.state.noteContentPropertiesDialogOptions !== prevState.noteContentPropertiesDialogOptions) {
this.props.dispatch({
type: this.state.noteContentPropertiesDialogOptions && this.state.noteContentPropertiesDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE',
name: 'noteContentProperties',
});
}
if (this.state.shareNoteDialogOptions !== prevState.shareNoteDialogOptions) {
this.props.dispatch({
type: this.state.shareNoteDialogOptions && this.state.shareNoteDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE',
name: 'shareNote',
});
}
}
componentDidMount() { componentDidMount() {
CommandService.instance().on('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange); CommandService.instance().on('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange);
this.updateRootLayoutSize();
} }
componentWillUnmount() { componentWillUnmount() {
CommandService.instance().off('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange); CommandService.instance().off('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange);
this.unregisterCommands(); this.unregisterCommands();
window.removeEventListener('resize', this.window_resize);
} }
toggleSidebar() { toggleSidebar() {
@@ -162,14 +285,14 @@ class MainScreenComponent extends React.Component {
}); });
} }
async waitForNoteToSaved(noteId) { async waitForNoteToSaved(noteId:string) {
while (noteId && this.props.editorNoteStatuses[noteId] === 'saving') { while (noteId && this.props.editorNoteStatuses[noteId] === 'saving') {
console.info('Waiting for note to be saved...', this.props.editorNoteStatuses); console.info('Waiting for note to be saved...', this.props.editorNoteStatuses);
await time.msleep(100); await time.msleep(100);
} }
} }
async printTo_(target, options) { async printTo_(target:string, options:any) {
// Concurrent print calls are disallowed to avoid incorrect settings being restored upon completion // Concurrent print calls are disallowed to avoid incorrect settings being restored upon completion
if (this.isPrinting_) { if (this.isPrinting_) {
console.info(`Printing ${options.path} to ${target} disallowed, already printing.`); console.info(`Printing ${options.path} to ${target} disallowed, already printing.`);
@@ -208,7 +331,23 @@ class MainScreenComponent extends React.Component {
this.isPrinting_ = false; this.isPrinting_ = false;
} }
styles(themeId, width, height, messageBoxVisible, isSidebarVisible, isNoteListVisible, sidebarWidth, noteListWidth) { rootLayoutSize() {
return {
width: window.innerWidth,
height: this.rowHeight(),
};
}
rowHeight() {
if (!this.props) return 0;
return this.props.style.height - (this.messageBoxVisible() ? this.messageBoxHeight() : 0);
}
messageBoxHeight() {
return 50;
}
styles(themeId:number, width:number, height:number, messageBoxVisible:boolean, isSidebarVisible:any, isNoteListVisible:any, sidebarWidth:number, noteListWidth:number) {
const styleKey = [themeId, width, height, messageBoxVisible, +isSidebarVisible, +isNoteListVisible, sidebarWidth, noteListWidth].join('_'); const styleKey = [themeId, width, height, messageBoxVisible, +isSidebarVisible, +isNoteListVisible, sidebarWidth, noteListWidth].join('_');
if (styleKey === this.styleKey_) return this.styles_; if (styleKey === this.styleKey_) return this.styles_;
@@ -224,14 +363,16 @@ class MainScreenComponent extends React.Component {
this.styles_.messageBox = { this.styles_.messageBox = {
width: width, width: width,
height: 50, height: this.messageBoxHeight(),
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
paddingLeft: 10, paddingLeft: 10,
backgroundColor: theme.warningBackgroundColor, backgroundColor: theme.warningBackgroundColor,
}; };
const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0); const rowHeight = height - (messageBoxVisible ? this.styles_.messageBox.height : 0);
this.styles_.rowHeight = rowHeight;
this.styles_.verticalResizerSidebar = { this.styles_.verticalResizerSidebar = {
width: 5, width: 5,
@@ -241,6 +382,10 @@ class MainScreenComponent extends React.Component {
display: 'inline-block', display: 'inline-block',
}; };
this.styles_.resizableLayout = {
height: rowHeight,
};
this.styles_.verticalResizerNotelist = Object.assign({}, this.styles_.verticalResizerSidebar); this.styles_.verticalResizerNotelist = Object.assign({}, this.styles_.verticalResizerSidebar);
this.styles_.sideBar = { this.styles_.sideBar = {
@@ -295,7 +440,7 @@ class MainScreenComponent extends React.Component {
return this.styles_; return this.styles_;
} }
renderNotification(theme, styles) { renderNotification(theme:any, styles:any) {
if (!this.messageBoxVisible()) return null; if (!this.messageBoxVisible()) return null;
const onViewStatusScreen = () => { const onViewStatusScreen = () => {
@@ -401,8 +546,34 @@ class MainScreenComponent extends React.Component {
} }
} }
resizableLayout_resize(event:any) {
this.setState({ layout: event.layout });
const col1 = findItemByKey(event.layout, 'sidebarColumn');
const col2 = findItemByKey(event.layout, 'noteListColumn');
Setting.setValue('style.sidebar.width', col1.width);
Setting.setValue('style.noteList.width', col2.width);
}
resizableLayout_renderItem(key:string, event:any) {
const eventEmitter = event.eventEmitter;
if (key === 'sideBar') {
return <SideBar key={key} />;
} else if (key === 'noteList') {
return <NoteList key={key} resizableLayoutEventEmitter={eventEmitter} size={event.size} visible={event.visible}/>;
} else if (key === 'editor') {
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
return <NoteEditor key={key} bodyEditor={bodyEditor} />;
} else if (key === 'noteListControls') {
return <NoteListControls key={key} showNewNoteButtons={this.props.focusedField !== 'globalSearch'} />;
}
throw new Error(`Invalid layout component: ${key}`);
}
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = Object.assign( const style = Object.assign(
{ {
color: theme.color, color: theme.color,
@@ -411,48 +582,12 @@ class MainScreenComponent extends React.Component {
this.props.style this.props.style
); );
const promptOptions = this.state.promptOptions; const promptOptions = this.state.promptOptions;
const notes = this.props.notes;
const sidebarVisibility = this.props.sidebarVisibility; const sidebarVisibility = this.props.sidebarVisibility;
const noteListVisibility = this.props.noteListVisibility; const noteListVisibility = this.props.noteListVisibility;
const styles = this.styles(this.props.theme, style.width, style.height, this.messageBoxVisible(), sidebarVisibility, noteListVisibility, this.props.sidebarWidth, this.props.noteListWidth); const styles = this.styles(this.props.themeId, style.width, style.height, this.messageBoxVisible(), sidebarVisibility, noteListVisibility, this.props.sidebarWidth, this.props.noteListWidth);
const headerItems = [];
headerItems.push(CommandService.instance().commandToToolbarButton('toggleSidebar', { iconRotation: sidebarVisibility ? 0 : 90 }));
headerItems.push(CommandService.instance().commandToToolbarButton('toggleNoteList', { iconRotation: noteListVisibility ? 0 : 90 }));
headerItems.push(CommandService.instance().commandToToolbarButton('newNote'));
headerItems.push(CommandService.instance().commandToToolbarButton('newTodo'));
headerItems.push(CommandService.instance().commandToToolbarButton('newNotebook'));
headerItems.push({
title: _('Code View'),
iconName: 'fa-file-code ',
enabled: !!notes.length,
type: 'checkbox',
checked: this.props.settingEditorCodeView,
onClick: () => {
// A bit of a hack, but for now don't allow changing code view
// while a note is being saved as it will cause a problem with
// TinyMCE because it won't have time to send its content before
// being switch to the Code Editor.
if (this.props.hasNotesBeingSaved) return;
Setting.toggle('editor.codeView');
},
});
headerItems.push(CommandService.instance().commandToToolbarButton('toggleVisiblePanes'));
headerItems.push({
title: _('Search...'),
iconName: 'fa-search',
onQuery: (query, fuzzy = false) => {
CommandService.instance().execute('search', { query, fuzzy });
},
type: 'search',
});
if (!this.promptOnClose_) { if (!this.promptOnClose_) {
this.promptOnClose_ = (answer, buttonType) => { this.promptOnClose_ = (answer:any, buttonType:any) => {
return this.state.promptOptions.onClose(answer, buttonType); return this.state.promptOptions.onClose(answer, buttonType);
}; };
} }
@@ -468,34 +603,33 @@ class MainScreenComponent extends React.Component {
const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions; const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions;
const shareNoteDialogOptions = this.state.shareNoteDialogOptions; const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
return ( return (
<div style={style}> <div style={style}>
<div style={modalLayerStyle}>{this.state.modalLayer.message}</div> <div style={modalLayerStyle}>{this.state.modalLayer.message}</div>
{noteContentPropertiesDialogOptions.visible && <NoteContentPropertiesDialog theme={this.props.theme} onClose={this.noteContentPropertiesDialog_close} text={noteContentPropertiesDialogOptions.text}/>} {noteContentPropertiesDialogOptions.visible && <NoteContentPropertiesDialog markupLanguage={noteContentPropertiesDialogOptions.markupLanguage} themeId={this.props.themeId} onClose={this.noteContentPropertiesDialog_close} text={noteContentPropertiesDialogOptions.text}/>}
{notePropertiesDialogOptions.visible && <NotePropertiesDialog theme={this.props.theme} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />} {notePropertiesDialogOptions.visible && <NotePropertiesDialog themeId={this.props.themeId} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
{shareNoteDialogOptions.visible && <ShareNoteDialog theme={this.props.theme} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />} {shareNoteDialogOptions.visible && <ShareNoteDialog themeId={this.props.themeId} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />}
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} theme={this.props.theme} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} /> <PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} themeId={this.props.themeId} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
<Header style={styles.header} showBackButton={false} items={headerItems} />
{messageComp} {messageComp}
<SideBar style={styles.sideBar} /> <ResizableLayout
<VerticalResizer style={styles.verticalResizerSidebar} onDrag={this.sidebar_onDrag} /> width={this.state.width}
<NoteList style={styles.noteList} /> height={styles.rowHeight}
<VerticalResizer style={styles.verticalResizerNotelist} onDrag={this.noteList_onDrag} /> layout={this.state.layout}
<NoteEditor bodyEditor={bodyEditor} style={styles.noteText} /> onResize={this.resizableLayout_resize}
renderItem={this.resizableLayout_renderItem}
/>
{pluginDialog} {pluginDialog}
</div> </div>
); );
} }
} }
const mapStateToProps = state => { const mapStateToProps = (state:any) => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
settingEditorCodeView: state.settings['editor.codeView'], settingEditorCodeView: state.settings['editor.codeView'],
sidebarVisibility: state.sidebarVisibility, sidebarVisibility: state.sidebarVisibility,
noteListVisibility: state.noteListVisibility, noteListVisibility: state.noteListVisibility,
@@ -516,9 +650,8 @@ const mapStateToProps = state => {
customCss: state.customCss, customCss: state.customCss,
editorNoteStatuses: state.editorNoteStatuses, editorNoteStatuses: state.editorNoteStatuses,
hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state), hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state),
focusedField: state.focusedField,
}; };
}; };
const MainScreen = connect(mapStateToProps)(MainScreenComponent); export default connect(mapStateToProps)(MainScreenComponent);
module.exports = { MainScreen };

View File

@@ -8,7 +8,7 @@ const { time } = require('lib/time-utils');
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'editAlarm', name: 'editAlarm',
label: () => _('Set alarm'), label: () => _('Set alarm'),
iconName: 'fa-clock', iconName: 'icon-alarm',
}; };
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {

View File

@@ -4,7 +4,7 @@ const Folder = require('lib/models/Folder');
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'newNotebook', name: 'newFolder',
label: () => _('New notebook'), label: () => _('New notebook'),
iconName: 'fa-book', iconName: 'fa-book',
}; };

View File

@@ -1,18 +1,15 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
const Note = require('lib/models/Note');
const BaseModel = require('lib/BaseModel'); const BaseModel = require('lib/BaseModel');
// const { _ } = require('lib/locale');
const { uuid } = require('lib/uuid.js'); const { uuid } = require('lib/uuid.js');
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'search', name: 'search',
iconName: 'icon-search',
}; };
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ query, fuzzy }:any) => { execute: async ({ query }:any) => {
console.info('RUNTIME', query);
if (!comp.searchId_) comp.searchId_ = uuid.create(); if (!comp.searchId_) comp.searchId_ = uuid.create();
comp.props.dispatch({ comp.props.dispatch({
@@ -23,7 +20,6 @@ export const runtime = (comp:any):CommandRuntime => {
query_pattern: query, query_pattern: query,
query_folder_id: null, query_folder_id: null,
type_: BaseModel.TYPE_SEARCH, type_: BaseModel.TYPE_SEARCH,
fuzzy: fuzzy,
}, },
}); });
@@ -33,14 +29,18 @@ export const runtime = (comp:any):CommandRuntime => {
id: comp.searchId_, id: comp.searchId_,
}); });
} else { } else {
const note = await Note.load(comp.props.selectedNoteId); // Note: Normally there's no need to do anything when the search query
if (note) { // is cleared as the reducer should handle all state changes.
comp.props.dispatch({ // https://github.com/laurent22/joplin/issues/3748
type: 'FOLDER_AND_NOTE_SELECT',
folderId: note.parent_id, // const note = await Note.load(comp.props.selectedNoteId);
noteId: note.id, // if (note) {
}); // comp.props.dispatch({
} // type: 'FOLDER_AND_NOTE_SELECT',
// folderId: note.parent_id,
// noteId: note.id,
// });
// }
} }
}, },
}; };

View File

@@ -5,7 +5,7 @@ const { _ } = require('lib/locale');
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'setTags', name: 'setTags',
label: () => _('Tags'), label: () => _('Tags'),
iconName: 'fa-tags', iconName: 'icon-tags',
}; };
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {

View File

@@ -4,7 +4,7 @@ const { _ } = require('lib/locale');
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'showNoteProperties', name: 'showNoteProperties',
label: () => _('Note properties'), label: () => _('Note properties'),
iconName: 'fa-info-circle', iconName: 'icon-info',
}; };
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {

View File

@@ -0,0 +1,32 @@
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
const { _ } = require('lib/locale');
const { stateUtils } = require('lib/reducer.js');
const Setting = require('lib/models/Setting');
export const declaration:CommandDeclaration = {
name: 'toggleEditors',
label: () => _('Toggle editors'),
iconName: 'fa-columns',
};
export const runtime = ():CommandRuntime => {
return {
execute: async (props:any) => {
// A bit of a hack, but for now don't allow changing code view
// while a note is being saved as it will cause a problem with
// TinyMCE because it won't have time to send its content before
// being switch to Ace Editor.
if (props.hasNotesBeingSaved) return;
Setting.toggle('editor.codeView');
},
isEnabled: (props:any):boolean => {
return !props.hasNotesBeingSaved && props.selectedNoteIds.length === 1;
},
mapStateToProps: (state:any):any => {
return {
hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state),
selectedNoteIds: state.selectedNoteIds,
};
},
};
};

View File

@@ -4,7 +4,7 @@ const { _ } = require('lib/locale');
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'toggleVisiblePanes', name: 'toggleVisiblePanes',
label: () => _('Toggle editor layout'), label: () => _('Toggle editor layout'),
iconName: 'fa-columns', iconName: 'icon-layout ',
}; };
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {

View File

@@ -5,22 +5,21 @@ const { bridge } = require('electron').remote.require('./bridge');
const NoteListUtils = require('./utils/NoteListUtils'); const NoteListUtils = require('./utils/NoteListUtils');
interface MultiNoteActionsProps { interface MultiNoteActionsProps {
theme: number, themeId: number,
selectedNoteIds: string[], selectedNoteIds: string[],
notes: any[], notes: any[],
dispatch: Function, dispatch: Function,
watchedNoteFiles: string[], watchedNoteFiles: string[],
style: any,
} }
function styles_(props:MultiNoteActionsProps) { function styles_(props:MultiNoteActionsProps) {
return buildStyle('MultiNoteActions', props.theme, (theme:any) => { return buildStyle('MultiNoteActions', props.themeId, (theme:any) => {
return { return {
root: { root: {
...props.style,
display: 'inline-flex', display: 'inline-flex',
justifyContent: 'center', justifyContent: 'center',
paddingTop: theme.marginTop, paddingTop: theme.marginTop,
width: '100%',
}, },
itemList: { itemList: {
display: 'flex', display: 'flex',

View File

@@ -7,7 +7,7 @@ const Countable = require('countable');
const markupLanguageUtils = require('lib/markupLanguageUtils'); const markupLanguageUtils = require('lib/markupLanguageUtils');
interface NoteContentPropertiesDialogProps { interface NoteContentPropertiesDialogProps {
theme: number, themeId: number,
text: string, text: string,
markupLanguage: number, markupLanguage: number,
onClose: Function, onClose: Function,
@@ -46,7 +46,9 @@ function formatReadTime(readTimeMinutes: number) {
} }
export default function NoteContentPropertiesDialog(props:NoteContentPropertiesDialogProps) { export default function NoteContentPropertiesDialog(props:NoteContentPropertiesDialogProps) {
const theme = themeStyle(props.theme);
console.info('MMMMMMMMMMMM', props.markupLanguage);
const theme = themeStyle(props.themeId);
const tableBodyComps: JSX.Element[] = []; const tableBodyComps: JSX.Element[] = [];
// For the source Markdown // For the source Markdown
const [lines, setLines] = useState<number>(0); const [lines, setLines] = useState<number>(0);
@@ -150,6 +152,8 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
textAlign: 'center', textAlign: 'center',
}; };
const readTimeLabel = _('Read time: %s min', formatReadTime(strippedReadTime));
return ( return (
<div style={theme.dialogModalLayer}> <div style={theme.dialogModalLayer}>
<div style={theme.dialogBox}> <div style={theme.dialogBox}>
@@ -162,10 +166,10 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
{tableBodyComps} {tableBodyComps}
</tbody> </tbody>
</table> </table>
<div style={labelCompStyle}> <div style={{ ...labelCompStyle, marginTop: 10 }}>
{_('Read time: %s min', formatReadTime(strippedReadTime))} {readTimeLabel}
</div> </div>
<DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/> <DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</div> </div>
</div> </div>
); );

View File

@@ -9,6 +9,7 @@ import { useScrollHandler, usePrevious, cursorPositionToTextOffset, useRootSize
import Toolbar from './Toolbar'; import Toolbar from './Toolbar';
import styles_ from './styles'; import styles_ from './styles';
import { RenderedBody, defaultRenderedBody } from './utils/types'; import { RenderedBody, defaultRenderedBody } from './utils/types';
import NoteTextViewer from '../../../NoteTextViewer';
import Editor from './Editor'; import Editor from './Editor';
// @ts-ignore // @ts-ignore
@@ -17,7 +18,6 @@ const { bridge } = require('electron').remote.require('./bridge');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
const { clipboard } = require('electron'); const { clipboard } = require('electron');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');
const NoteTextViewer = require('../../../NoteTextViewer.min');
const shared = require('lib/components/shared/note-screen-shared.js'); const shared = require('lib/components/shared/note-screen-shared.js');
const Menu = bridge().Menu; const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem; const MenuItem = bridge().MenuItem;
@@ -37,6 +37,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
const [webviewReady, setWebviewReady] = useState(false); const [webviewReady, setWebviewReady] = useState(false);
const previousContent = usePrevious(props.content);
const previousRenderedBody = usePrevious(renderedBody); const previousRenderedBody = usePrevious(renderedBody);
const previousSearchMarkers = usePrevious(props.searchMarkers); const previousSearchMarkers = usePrevious(props.searchMarkers);
const previousContentKey = usePrevious(props.contentKey); const previousContentKey = usePrevious(props.contentKey);
@@ -48,7 +49,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
props_onChangeRef.current = props.onChange; props_onChangeRef.current = props.onChange;
const contentKeyHasChangedRef = useRef(false); const contentKeyHasChangedRef = useRef(false);
contentKeyHasChangedRef.current = previousContentKey !== props.contentKey; contentKeyHasChangedRef.current = previousContentKey !== props.contentKey;
const theme = themeStyle(props.theme);
const rootSize = useRootSize({ rootRef }); const rootSize = useRootSize({ rootRef });
@@ -351,6 +351,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
}, [styles.editor.codeMirrorTheme]); }, [styles.editor.codeMirrorTheme]);
useEffect(() => { useEffect(() => {
const theme = themeStyle(props.themeId);
const element = document.createElement('style'); const element = document.createElement('style');
element.setAttribute('id', 'codemirrorStyle'); element.setAttribute('id', 'codemirrorStyle');
document.head.appendChild(element); document.head.appendChild(element);
@@ -358,6 +360,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
/* These must be important to prevent the codemirror defaults from taking over*/ /* These must be important to prevent the codemirror defaults from taking over*/
.CodeMirror { .CodeMirror {
font-family: monospace; font-family: monospace;
font-size: ${theme.editorFontSize}px;
height: 100% !important; height: 100% !important;
width: 100% !important; width: 100% !important;
color: inherit !important; color: inherit !important;
@@ -371,37 +374,37 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
/* be applied to the viewer. */ /* be applied to the viewer. */
padding-bottom: 400px !important; padding-bottom: 400px !important;
} }
.cm-header-1 { .cm-header-1 {
font-size: 1.5em; font-size: 1.5em;
} }
.cm-header-2 { .cm-header-2 {
font-size: 1.3em; font-size: 1.3em;
} }
.cm-header-3 { .cm-header-3 {
font-size: 1.1em; font-size: 1.1em;
} }
.cm-header-4, .cm-header-5, .cm-header-6 { .cm-header-4, .cm-header-5, .cm-header-6 {
font-size: 1em; font-size: 1em;
} }
.cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4, .cm-header-5, .cm-header-6 { .cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4, .cm-header-5, .cm-header-6 {
line-height: 1.5em; line-height: 1.5em;
} }
.cm-search-marker { .cm-search-marker {
background: ${theme.searchMarkerBackgroundColor}; background: ${theme.searchMarkerBackgroundColor};
color: ${theme.searchMarkerColor} !important; color: ${theme.searchMarkerColor} !important;
} }
.cm-search-marker-selected { .cm-search-marker-selected {
background: ${theme.selectedColor2}; background: ${theme.selectedColor2};
color: ${theme.color2} !important; color: ${theme.color2} !important;
} }
.cm-search-marker-scrollbar { .cm-search-marker-scrollbar {
background: ${theme.searchMarkerBackgroundColor}; background: ${theme.searchMarkerBackgroundColor};
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
@@ -415,12 +418,33 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
background-color: inherit !important; background-color: inherit !important;
border-bottom: 1px dotted #dc322f; border-bottom: 1px dotted #dc322f;
} }
/* The default dark theme colors don't have enough contrast with the background */
.cm-s-nord span.cm-comment {
color: #9aa4b6 !important;
}
.cm-s-dracula span.cm-comment {
color: #a1abc9 !important;
}
.cm-s-monokai span.cm-comment {
color: #908b74 !important;
}
.cm-s-material-darker span.cm-comment {
color: #878787 !important;
}
.cm-s-solarized.cm-s-dark span.cm-comment {
color: #8ba1a7 !important;
}
`)); `));
return () => { return () => {
document.head.removeChild(element); document.head.removeChild(element);
}; };
}, [props.theme]); }, [props.themeId]);
const webview_domReady = useCallback(() => { const webview_domReady = useCallback(() => {
setWebviewReady(true); setWebviewReady(true);
@@ -478,7 +502,18 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
}, [renderedBody, webviewReady]); }, [renderedBody, webviewReady]);
useEffect(() => { useEffect(() => {
if (props.searchMarkers !== previousSearchMarkers || renderedBody !== previousRenderedBody) { if (!props.searchMarkers) return;
// If there is a currently active search, it's important to re-search the text as the user
// types. However this is slow for performance so we ONLY want it to happen when there is
// a search
// Note that since the CodeMirror component also needs to handle the viewer pane, we need
// to check if the rendered body has changed too (it will be changed with a delay after
// props.content has been updated).
const textChanged = props.searchMarkers.keywords.length > 0 && (props.content !== previousContent || renderedBody !== previousRenderedBody);
if (props.searchMarkers !== previousSearchMarkers || textChanged) {
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options); webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
if (editorRef.current) { if (editorRef.current) {
@@ -487,7 +522,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
props.setLocalSearchResultCount(matches); props.setLocalSearchResultCount(matches);
} }
} }
}, [props.searchMarkers, props.setLocalSearchResultCount, renderedBody]); }, [props.searchMarkers, previousSearchMarkers, props.setLocalSearchResultCount, props.content, previousContent, renderedBody, previousRenderedBody, renderedBody]);
const cellEditorStyle = useMemo(() => { const cellEditorStyle = useMemo(() => {
const output = { ...styles.cellEditor }; const output = { ...styles.cellEditor };
@@ -539,17 +574,16 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
editorRef.current.refresh(); editorRef.current.refresh();
}, [rootSize, styles.editor, props.visiblePanes]); }, [rootSize, styles.editor, props.visiblePanes]);
const editorReadOnly = props.visiblePanes.indexOf('editor') < 0;
function renderEditor() { function renderEditor() {
return ( return (
<div style={cellEditorStyle}> <div style={cellEditorStyle}>
<Editor <Editor
value={props.content} value={props.content}
searchMarkers={props.searchMarkers}
ref={editorRef} ref={editorRef}
mode={props.contentMarkupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'xml' : 'joplin-markdown'} mode={props.contentMarkupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'xml' : 'joplin-markdown'}
theme={styles.editor.codeMirrorTheme} codeMirrorTheme={styles.editor.codeMirrorTheme}
style={styles.editor} style={styles.editor}
readOnly={props.visiblePanes.indexOf('editor') < 0} readOnly={props.visiblePanes.indexOf('editor') < 0}
autoMatchBraces={Setting.value('editor.autoMatchingBraces')} autoMatchBraces={Setting.value('editor.autoMatchingBraces')}
@@ -580,9 +614,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
<div style={styles.root} ref={rootRef}> <div style={styles.root} ref={rootRef}>
<div style={styles.rowToolbar}> <div style={styles.rowToolbar}>
<Toolbar <Toolbar
theme={props.theme} themeId={props.themeId}
dispatch={props.dispatch} dispatch={props.dispatch}
disabled={editorReadOnly}
/> />
{props.noteToolbar} {props.noteToolbar}
</div> </div>

View File

@@ -17,18 +17,17 @@ import useCursorUtils from './utils/useCursorUtils';
import useLineSorting from './utils/useLineSorting'; import useLineSorting from './utils/useLineSorting';
import useEditorSearch from './utils/useEditorSearch'; import useEditorSearch from './utils/useEditorSearch';
import useJoplinMode from './utils/useJoplinMode'; import useJoplinMode from './utils/useJoplinMode';
import useKeymap from './utils/useKeymap';
import 'codemirror/keymap/emacs'; import 'codemirror/keymap/emacs';
import 'codemirror/keymap/vim'; import 'codemirror/keymap/vim';
import 'codemirror/keymap/sublime'; // Used for swapLineUp and swapLineDown import 'codemirror/keymap/sublime'; // Used for swapLineUp and swapLineDown
import 'codemirror/mode/meta'; import 'codemirror/mode/meta';
const { shim } = require('lib/shim.js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
// Based on http://pypl.github.io/PYPL.html // Based on http://pypl.github.io/PYPL.html
// +XML (HTML) +CSS and Markdown added
const topLanguages = [ const topLanguages = [
'python', 'python',
'clike', 'clike',
@@ -51,8 +50,16 @@ const topLanguages = [
'haskell', 'haskell',
'pascal', 'pascal',
'css', 'css',
'xml',
// Additional languages, not in the PYPL list
'xml', // For HTML too
'markdown', 'markdown',
'yaml',
'shell',
'dockerfile',
'diff',
'erlang',
'sql',
]; ];
// Load Top Modes // Load Top Modes
for (let i = 0; i < topLanguages.length; i++) { for (let i = 0; i < topLanguages.length; i++) {
@@ -67,9 +74,10 @@ for (let i = 0; i < topLanguages.length; i++) {
export interface EditorProps { export interface EditorProps {
value: string, value: string,
searchMarkers: any,
mode: string, mode: string,
style: any, style: any,
theme: any, codeMirrorTheme: any,
readOnly: boolean, readOnly: boolean,
autoMatchBraces: boolean, autoMatchBraces: boolean,
keyMap: string, keyMap: string,
@@ -91,6 +99,7 @@ function Editor(props: EditorProps, ref: any) {
useLineSorting(CodeMirror); useLineSorting(CodeMirror);
useEditorSearch(CodeMirror); useEditorSearch(CodeMirror);
useJoplinMode(CodeMirror); useJoplinMode(CodeMirror);
useKeymap(CodeMirror);
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {
return editor; return editor;
@@ -133,90 +142,13 @@ function Editor(props: EditorProps, ref: any) {
} }
}, []); }, []);
useEffect(() => {
CodeMirror.keyMap.basic = {
'Left': 'goCharLeft',
'Right': 'goCharRight',
'Up': 'goLineUp',
'Down': 'goLineDown',
'End': 'goLineRight',
'Home': 'goLineLeftSmart',
'PageUp': 'goPageUp',
'PageDown': 'goPageDown',
'Delete': 'delCharAfter',
'Backspace': 'delCharBefore',
'Shift-Backspace': 'delCharBefore',
'Tab': 'smartListIndent',
'Shift-Tab': 'smartListUnindent',
'Enter': 'insertListElement',
'Insert': 'toggleOverwrite',
'Esc': 'singleSelection',
};
if (shim.isMac()) {
CodeMirror.keyMap.default = {
// MacOS
'Cmd-A': 'selectAll',
'Cmd-D': 'deleteLine',
'Cmd-Z': 'undo',
'Shift-Cmd-Z': 'redo',
'Cmd-Y': 'redo',
'Cmd-Home': 'goDocStart',
'Cmd-Up': 'goDocStart',
'Cmd-End': 'goDocEnd',
'Cmd-Down': 'goDocEnd',
'Cmd-Left': 'goLineLeft',
'Cmd-Right': 'goLineRight',
'Alt-Left': 'goGroupLeft',
'Alt-Right': 'goGroupRight',
'Alt-Backspace': 'delGroupBefore',
'Alt-Delete': 'delGroupAfter',
'Cmd-[': 'indentLess',
'Cmd-]': 'indentMore',
'Cmd-/': 'toggleComment',
'Cmd-Opt-S': 'sortSelectedLines',
'Opt-Up': 'swapLineUp',
'Opt-Down': 'swapLineDown',
'fallthrough': 'basic',
};
} else {
CodeMirror.keyMap.default = {
// Windows/linux
'Ctrl-A': 'selectAll',
'Ctrl-D': 'deleteLine',
'Ctrl-Z': 'undo',
'Shift-Ctrl-Z': 'redo',
'Ctrl-Y': 'redo',
'Ctrl-Home': 'goDocStart',
'Ctrl-End': 'goDocEnd',
'Ctrl-Up': 'goLineUp',
'Ctrl-Down': 'goLineDown',
'Ctrl-Left': 'goGroupLeft',
'Ctrl-Right': 'goGroupRight',
'Alt-Left': 'goLineStart',
'Alt-Right': 'goLineEnd',
'Ctrl-Backspace': 'delGroupBefore',
'Ctrl-Delete': 'delGroupAfter',
'Ctrl-[': 'indentLess',
'Ctrl-]': 'indentMore',
'Ctrl-/': 'toggleComment',
'Ctrl-Alt-S': 'sortSelectedLines',
'Alt-Up': 'swapLineUp',
'Alt-Down': 'swapLineDown',
'fallthrough': 'basic',
};
}
}, []);
useEffect(() => { useEffect(() => {
if (!editorParent.current) return () => {}; if (!editorParent.current) return () => {};
const cmOptions = { const cmOptions = {
value: props.value, value: props.value,
screenReaderLabel: props.value, screenReaderLabel: props.value,
theme: props.theme, theme: props.codeMirrorTheme,
mode: props.mode, mode: props.mode,
readOnly: props.readOnly, readOnly: props.readOnly,
autoCloseBrackets: props.autoMatchBraces, autoCloseBrackets: props.autoMatchBraces,
@@ -238,6 +170,11 @@ function Editor(props: EditorProps, ref: any) {
cm.on('drop', editor_drop); cm.on('drop', editor_drop);
cm.on('dragover', editor_drag); cm.on('dragover', editor_drag);
// It's possible for searchMarkers to be available before the editor
// In these cases we set the markers asap so the user can see them as
// soon as the editor is ready
if (props.searchMarkers) { cm.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options); }
return () => { return () => {
// Clean up codemirror // Clean up codemirror
cm.off('change', editor_change); cm.off('change', editor_change);
@@ -265,9 +202,9 @@ function Editor(props: EditorProps, ref: any) {
useEffect(() => { useEffect(() => {
if (editor) { if (editor) {
editor.setOption('theme', props.theme); editor.setOption('theme', props.codeMirrorTheme);
} }
}, [props.theme]); }, [props.codeMirrorTheme]);
useEffect(() => { useEffect(() => {
if (editor) { if (editor) {

View File

@@ -1,23 +1,19 @@
import * as React from 'react'; import * as React from 'react';
import CommandService from '../../../../lib/services/CommandService'; import CommandService from 'lib/services/CommandService';
import ToolbarBase from '../../../ToolbarBase';
const ToolbarBase = require('../../../Toolbar.min.js'); const { buildStyle } = require('lib/theme');
const { buildStyle, themeStyle } = require('lib/theme');
interface ToolbarProps { interface ToolbarProps {
theme: number, themeId: number,
dispatch: Function, dispatch: Function,
disabled: boolean,
} }
function styles_(props:ToolbarProps) { function styles_(props:ToolbarProps) {
return buildStyle('CodeMirrorToolbar', props.theme, (/* theme:any*/) => { return buildStyle('CodeMirrorToolbar', props.themeId, () => {
const theme = themeStyle(props.theme);
return { return {
root: { root: {
flex: 1, flex: 1,
marginBottom: 0, marginBottom: 0,
borderTop: `1px solid ${theme.dividerColor}`,
}, },
}; };
}); });
@@ -29,6 +25,11 @@ export default function Toolbar(props:ToolbarProps) {
const cmdService = CommandService.instance(); const cmdService = CommandService.instance();
const toolbarItems = [ const toolbarItems = [
cmdService.commandToToolbarButton('historyBackward'),
cmdService.commandToToolbarButton('historyForward'),
cmdService.commandToToolbarButton('startExternalEditing'),
{ type: 'separator' },
cmdService.commandToToolbarButton('textBold'), cmdService.commandToToolbarButton('textBold'),
cmdService.commandToToolbarButton('textItalic'), cmdService.commandToToolbarButton('textItalic'),
{ type: 'separator' }, { type: 'separator' },
@@ -42,7 +43,9 @@ export default function Toolbar(props:ToolbarProps) {
cmdService.commandToToolbarButton('textHeading'), cmdService.commandToToolbarButton('textHeading'),
cmdService.commandToToolbarButton('textHorizontalRule'), cmdService.commandToToolbarButton('textHorizontalRule'),
cmdService.commandToToolbarButton('insertDateTime'), cmdService.commandToToolbarButton('insertDateTime'),
cmdService.commandToToolbarButton('toggleEditors'),
]; ];
return <ToolbarBase disabled={props.disabled} style={styles.root} items={toolbarItems} />; return <ToolbarBase style={styles.root} items={toolbarItems} />;
} }

View File

@@ -2,7 +2,7 @@ import { NoteBodyEditorProps } from '../../../utils/types';
const { buildStyle } = require('lib/theme'); const { buildStyle } = require('lib/theme');
export default function styles(props: NoteBodyEditorProps) { export default function styles(props: NoteBodyEditorProps) {
return buildStyle('CodeMirror', props.theme, (theme: any) => { return buildStyle('CodeMirror', props.themeId, (theme: any) => {
return { return {
root: { root: {
position: 'relative', position: 'relative',

View File

@@ -119,7 +119,8 @@ export default function useEditorSearch(CodeMirror: any) {
// We only want to highlight all matches when there is only 1 search term // We only want to highlight all matches when there is only 1 search term
if (keywords.length !== 1 || keywords[0].value == '') { if (keywords.length !== 1 || keywords[0].value == '') {
clearOverlay(this); clearOverlay(this);
setPreviousKeywordValue(''); const prev = keywords.length > 1 ? keywords[0].value : '';
setPreviousKeywordValue(prev);
return 0; return 0;
} }

View File

@@ -21,7 +21,8 @@ export default function useJoplinMode(CodeMirror: any) {
const inlineKatexOpenRE = /(?<!\S)\$(?=[^\s$].*?[^\\\s$]\$(?!\S))/; const inlineKatexOpenRE = /(?<!\S)\$(?=[^\s$].*?[^\\\s$]\$(?!\S))/;
const inlineKatexCloseRE = /(?<![\\\s$])\$(?!\S)/; const inlineKatexCloseRE = /(?<![\\\s$])\$(?!\S)/;
const blockKatexRE = /(?<!\\)\$\$/; const blockKatexOpenRE = /(?<!\S)\$\$/;
const blockKatexCloseRE = /(?<![\\\s])\$\$/;
// Find token will search for a valid katex start or end token // Find token will search for a valid katex start or end token
// If found then it will return the index, otherwise -1 // If found then it will return the index, otherwise -1
@@ -55,19 +56,19 @@ export default function useJoplinMode(CodeMirror: any) {
let nextTokenPos = stream.string.length; let nextTokenPos = stream.string.length;
let closing = false; let closing = false;
const blockPos = findToken(stream, blockKatexRE);
if (state.openCharacter) { if (state.openCharacter) {
currentMode = stex; currentMode = stex;
currentState = state.inner; currentState = state.inner;
tokenLabel = 'katex-marker-close'; tokenLabel = 'katex-marker-close';
closing = true; closing = true;
const blockPos = findToken(stream, blockKatexCloseRE);
const inlinePos = findToken(stream, inlineKatexCloseRE); const inlinePos = findToken(stream, inlineKatexCloseRE);
if (state.openCharacter === '$$' && blockPos !== -1) nextTokenPos = blockPos; if (state.openCharacter === '$$' && blockPos !== -1) nextTokenPos = blockPos;
if (state.openCharacter === '$' && inlinePos !== -1) nextTokenPos = inlinePos; if (state.openCharacter === '$' && inlinePos !== -1) nextTokenPos = inlinePos;
} else { } else if (!currentState.code) {
const blockPos = findToken(stream, blockKatexOpenRE);
const inlinePos = findToken(stream, inlineKatexOpenRE); const inlinePos = findToken(stream, inlineKatexOpenRE);
if (blockPos !== -1) nextTokenPos = blockPos; if (blockPos !== -1) nextTokenPos = blockPos;

View File

@@ -0,0 +1,107 @@
import { useEffect } from 'react';
import CommandService from 'lib/services/CommandService';
const { shim } = require('lib/shim.js');
export default function useKeymap(CodeMirror: any) {
function save() {
CommandService.instance().execute('synchronize');
}
function setupEmacs() {
CodeMirror.keyMap.emacs['Tab'] = 'smartListIndent';
CodeMirror.keyMap.emacs['Enter'] = 'insertListElement';
CodeMirror.keyMap.emacs['Shift-Tab'] = 'smartListUnindent';
}
function setupVim() {
CodeMirror.Vim.defineAction('swapLineDown', CodeMirror.commands.swapLineDown);
CodeMirror.Vim.mapCommand('<A-j>', 'action', 'swapLineDown', {}, { context: 'normal', isEdit: true });
CodeMirror.Vim.defineAction('swapLineUp', CodeMirror.commands.swapLineUp);
CodeMirror.Vim.mapCommand('<A-k>', 'action', 'swapLineUp', {}, { context: 'normal', isEdit: true });
CodeMirror.Vim.defineAction('insertListElement', CodeMirror.commands.vimInsertListElement);
CodeMirror.Vim.mapCommand('o', 'action', 'insertListElement', { after: true }, { context: 'normal', isEdit: true, interlaceInsertRepeat: true });
}
useEffect(() => {
// This enables the special modes (emacs and vim) to initiate sync by the save action
CodeMirror.commands.save = save;
CodeMirror.keyMap.basic = {
'Left': 'goCharLeft',
'Right': 'goCharRight',
'Up': 'goLineUp',
'Down': 'goLineDown',
'End': 'goLineRight',
'Home': 'goLineLeftSmart',
'PageUp': 'goPageUp',
'PageDown': 'goPageDown',
'Delete': 'delCharAfter',
'Backspace': 'delCharBefore',
'Shift-Backspace': 'delCharBefore',
'Tab': 'smartListIndent',
'Shift-Tab': 'smartListUnindent',
'Enter': 'insertListElement',
'Insert': 'toggleOverwrite',
'Esc': 'singleSelection',
};
if (shim.isMac()) {
CodeMirror.keyMap.default = {
// MacOS
'Cmd-A': 'selectAll',
'Cmd-D': 'deleteLine',
'Cmd-Z': 'undo',
'Shift-Cmd-Z': 'redo',
'Cmd-Y': 'redo',
'Cmd-Home': 'goDocStart',
'Cmd-Up': 'goDocStart',
'Cmd-End': 'goDocEnd',
'Cmd-Down': 'goDocEnd',
'Cmd-Left': 'goLineLeft',
'Cmd-Right': 'goLineRight',
'Alt-Left': 'goGroupLeft',
'Alt-Right': 'goGroupRight',
'Alt-Backspace': 'delGroupBefore',
'Alt-Delete': 'delGroupAfter',
'Cmd-[': 'indentLess',
'Cmd-]': 'indentMore',
'Cmd-/': 'toggleComment',
'Cmd-Opt-S': 'sortSelectedLines',
'Opt-Up': 'swapLineUp',
'Opt-Down': 'swapLineDown',
'fallthrough': 'basic',
};
} else {
CodeMirror.keyMap.default = {
// Windows/linux
'Ctrl-A': 'selectAll',
'Ctrl-D': 'deleteLine',
'Ctrl-Z': 'undo',
'Shift-Ctrl-Z': 'redo',
'Ctrl-Y': 'redo',
'Ctrl-Home': 'goDocStart',
'Ctrl-End': 'goDocEnd',
'Ctrl-Up': 'goLineUp',
'Ctrl-Down': 'goLineDown',
'Ctrl-Left': 'goGroupLeft',
'Ctrl-Right': 'goGroupRight',
'Alt-Left': 'goLineStart',
'Alt-Right': 'goLineEnd',
'Ctrl-Backspace': 'delGroupBefore',
'Ctrl-Delete': 'delGroupAfter',
'Ctrl-[': 'indentLess',
'Ctrl-]': 'indentMore',
'Ctrl-/': 'toggleComment',
'Ctrl-Alt-S': 'sortSelectedLines',
'Alt-Up': 'swapLineUp',
'Alt-Down': 'swapLineDown',
'fallthrough': 'basic',
};
}
setupEmacs();
setupVim();
}, []);
}

View File

@@ -126,6 +126,24 @@ export default function useListIdent(CodeMirror: any) {
}); });
}; };
// This is a special case of insertList element because it happens when
// vim is in normal mode and input is disabled and the cursor is not
// necessarily at the end of line (but it should pretend it is
CodeMirror.commands.vimInsertListElement = function(cm: any) {
cm.setOption('disableInput', false);
const ranges = cm.listSelections();
if (ranges.length === 0) return;
const { anchor } = ranges[0];
// Need to move the cursor to end of line as this is the vim behavior
const line = cm.getLine(anchor.line);
cm.setCursor({ line: anchor.line, ch: line.length });
cm.execCommand('insertListElement');
cm.setOption('disableInput', true);
};
CodeMirror.commands.insertListElement = function(cm: any) { CodeMirror.commands.insertListElement = function(cm: any) {
if (cm.getOption('disableInput')) return CodeMirror.Pass; if (cm.getOption('disableInput')) return CodeMirror.Pass;

View File

@@ -3,15 +3,18 @@ import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHand
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps } from '../../utils/types'; import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps } from '../../utils/types';
import { resourcesStatus, commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling'; import { resourcesStatus, commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
import useScroll from './utils/useScroll'; import useScroll from './utils/useScroll';
import styles_ from './styles';
import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../utils/contextMenu'; import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../utils/contextMenu';
import CommandService from '../../../../lib/services/CommandService'; import CommandService, { ToolbarButtonInfo } from 'lib/services/CommandService';
import ToggleEditorsButton, { Value as ToggleEditorsButtonValue } from '../../../ToggleEditorsButton/ToggleEditorsButton';
import ToolbarButton from '../../../../gui/ToolbarButton/ToolbarButton';
const { MarkupToHtml } = require('lib/joplin-renderer'); const { MarkupToHtml } = require('lib/joplin-renderer');
const taboverride = require('taboverride'); const taboverride = require('taboverride');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const { _, closestSupportedLocale } = require('lib/locale'); const { _, closestSupportedLocale } = require('lib/locale');
const BaseItem = require('lib/models/BaseItem'); const BaseItem = require('lib/models/BaseItem');
const Resource = require('lib/models/Resource'); const Resource = require('lib/models/Resource');
const { themeStyle, buildStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const { clipboard } = require('electron'); const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales'); const supportedLocales = require('./supportedLocales');
@@ -112,31 +115,6 @@ const joplinCommandToTinyMceCommands:JoplinCommandToTinyMceCommands = {
'search': { name: 'SearchReplace' }, 'search': { name: 'SearchReplace' },
}; };
function styles_(props:NoteBodyEditorProps) {
return buildStyle('TinyMCE', props.theme, (/* theme:any */) => {
return {
disabledOverlay: {
zIndex: 10,
position: 'absolute',
backgroundColor: 'white',
opacity: 0.7,
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: 20,
paddingTop: 50,
textAlign: 'center',
width: '100%',
},
rootStyle: {
position: 'relative',
...props.style,
},
};
});
}
let loadedCssFiles_:string[] = []; let loadedCssFiles_:string[] = [];
let loadedJsFiles_:string[] = []; let loadedJsFiles_:string[] = [];
let dispatchDidUpdateIID_:any = null; let dispatchDidUpdateIID_:any = null;
@@ -170,7 +148,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
editorRef.current = editor; editorRef.current = editor;
const styles = styles_(props); const styles = styles_(props);
const theme = themeStyle(props.theme); // const theme = themeStyle(props.themeId);
const { scrollToPercent } = useScroll({ editor, onScroll: props.onScroll }); const { scrollToPercent } = useScroll({ editor, onScroll: props.onScroll });
@@ -368,10 +346,17 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
useEffect(() => { useEffect(() => {
if (!editorReady) return () => {}; if (!editorReady) return () => {};
const theme = themeStyle(props.themeId);
const element = document.createElement('style'); const element = document.createElement('style');
element.setAttribute('id', 'tinyMceStyle'); element.setAttribute('id', 'tinyMceStyle');
document.head.appendChild(element); document.head.appendChild(element);
element.appendChild(document.createTextNode(` element.appendChild(document.createTextNode(`
.joplin-tinymce .tox-editor-header {
padding-left: ${styles.leftExtraToolbarContainer.width + styles.leftExtraToolbarContainer.padding * 2}px;
padding-right: ${styles.rightExtraToolbarContainer.width + styles.rightExtraToolbarContainer.padding * 2}px;
}
.tox .tox-toolbar, .tox .tox-toolbar,
.tox .tox-toolbar__overflow, .tox .tox-toolbar__overflow,
.tox .tox-toolbar__primary, .tox .tox-toolbar__primary,
@@ -388,8 +373,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
} }
.tox .tox-editor-header { .tox .tox-editor-header {
border-top: 1px solid ${theme.dividerColor}; border: none;
border-bottom: 1px solid ${theme.dividerColor};
} }
.tox .tox-tbtn, .tox .tox-tbtn,
@@ -401,8 +385,8 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
.tox input, .tox input,
.tox .tox-label, .tox .tox-label,
.tox .tox-toolbar-label { .tox .tox-toolbar-label {
color: ${theme.iconColor} !important; color: ${theme.color3} !important;
fill: ${theme.iconColor} !important; fill: ${theme.color3} !important;
} }
.tox .tox-statusbar a, .tox .tox-statusbar a,
@@ -424,32 +408,59 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
} }
.tox .tox-tbtn:hover { .tox .tox-tbtn:hover {
background-color: ${theme.backgroundHover}; color: ${theme.colorHover3} !important;
color: ${theme.colorHover}; fill: ${theme.colorHover3} !important;
fill: ${theme.colorHover}; background-color: ${theme.backgroundColorHover3}
}
.tox .tox-tbtn {
width: ${theme.toolbarHeight}px;
height: ${theme.toolbarHeight}px;
min-width: ${theme.toolbarHeight}px;
min-height: ${theme.toolbarHeight}px;
margin: 0;
}
.tox .tox-tbtn[aria-haspopup=true] {
width: ${theme.toolbarHeight + 15}px;
min-width: ${theme.toolbarHeight + 15}px;
}
.tox .tox-tbtn > span,
.tox .tox-tbtn:active > span,
.tox .tox-tbtn:hover > span {
transform: scale(0.8);
} }
.tox .tox-toolbar__primary, .tox .tox-toolbar__primary,
.tox .tox-toolbar__overflow { .tox .tox-toolbar__overflow {
background: none; background: none;
background-color: ${theme.backgroundColor3} !important;
} }
.tox-tinymce, .tox-tinymce,
.tox .tox-toolbar__group, .tox .tox-toolbar__group,
.tox.tox-tinymce-aux .tox-toolbar__overflow, .tox.tox-tinymce-aux .tox-toolbar__overflow,
.tox .tox-dialog__footer { .tox .tox-dialog__footer {
border-color: ${theme.dividerColor} !important; border: none !important;
} }
.tox-tinymce { .tox-tinymce {
border-top: none !important; border-top: none !important;
} }
.joplin-tinymce .tox-toolbar__group {
background-color: ${theme.backgroundColor3};
padding-top: ${theme.toolbarPadding}px;
padding-bottom: ${theme.toolbarPadding}px;
}
`)); `));
return () => { return () => {
document.head.removeChild(element); document.head.removeChild(element);
}; };
}, [editorReady, props.theme]); }, [editorReady, props.themeId]);
// ----------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------
// Enable or disable the editor // Enable or disable the editor
@@ -499,6 +510,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
menubar: false, menubar: false,
relative_urls: false, relative_urls: false,
branding: false, branding: false,
statusbar: false,
target_list: false, target_list: false,
table_resize_bars: false, table_resize_bars: false,
language: ['en_US', 'en_GB'].includes(language) ? undefined : language, language: ['en_US', 'en_GB'].includes(language) ? undefined : language,
@@ -706,6 +718,8 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
// The fix would be to make allAssets() return a name and a version for each asset. Then the loading // The fix would be to make allAssets() return a name and a version for each asset. Then the loading
// code would check this and either append the CSS or replace. // code would check this and either append the CSS or replace.
const theme = themeStyle(props.themeId);
let docHead_:any = null; let docHead_:any = null;
function docHead() { function docHead() {
@@ -1039,12 +1053,64 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
}; };
}, []); }, []);
function renderExtraToolbarButton(key:string, info:ToolbarButtonInfo) {
return <ToolbarButton
key={key}
themeId={props.themeId}
toolbarButtonInfo={info}
/>;
}
const leftButtonCommandNames = ['historyBackward', 'historyForward', 'startExternalEditing'];
function renderLeftExtraToolbarButtons() {
const buttons = [];
for (const buttonName in props.noteToolbarButtonInfos) {
if (!leftButtonCommandNames.includes(buttonName)) continue;
const info = props.noteToolbarButtonInfos[buttonName];
buttons.push(renderExtraToolbarButton(buttonName, info));
}
return (
<div style={styles.leftExtraToolbarContainer}>
{buttons}
</div>
);
}
function renderRightExtraToolbarButtons() {
const buttons = [];
for (const buttonName in props.noteToolbarButtonInfos) {
if (leftButtonCommandNames.includes(buttonName)) continue;
const info = props.noteToolbarButtonInfos[buttonName];
if (buttonName === 'toggleEditors') {
buttons.push(<ToggleEditorsButton
key={buttonName}
value={ToggleEditorsButtonValue.RichText}
themeId={props.themeId}
toolbarButtonInfo={info}
/>);
} else {
buttons.push(renderExtraToolbarButton(buttonName, info));
}
}
return (
<div style={styles.rightExtraToolbarContainer}>
{buttons}
</div>
);
}
// Currently we don't handle resource "auto" and "manual" mode with TinyMCE // Currently we don't handle resource "auto" and "manual" mode with TinyMCE
// as it is quite complex and probably rarely used. // as it is quite complex and probably rarely used.
function renderDisabledOverlay() { function renderDisabledOverlay() {
const status = resourcesStatus(props.resourceInfos); const status = resourcesStatus(props.resourceInfos);
if (status === 'ready' && !draggingStarted) return null; if (status === 'ready' && !draggingStarted) return null;
const theme = themeStyle(props.themeId);
const message = draggingStarted ? _('Drop notes or files here') : _('Please wait for all attachments to be downloaded and decrypted. You may also switch to %s to edit the note.', _('Code View')); const message = draggingStarted ? _('Drop notes or files here') : _('Please wait for all attachments to be downloaded and decrypted. You may also switch to %s to edit the note.', _('Code View'));
const statusComp = draggingStarted ? null : <p style={theme.textStyleMinor}>{`Status: ${status}`}</p>; const statusComp = draggingStarted ? null : <p style={theme.textStyleMinor}>{`Status: ${status}`}</p>;
return ( return (
@@ -1056,8 +1122,10 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
} }
return ( return (
<div style={styles.rootStyle}> <div style={styles.rootStyle} className="joplin-tinymce">
{renderDisabledOverlay()} {renderDisabledOverlay()}
{renderLeftExtraToolbarButtons()}
{renderRightExtraToolbarButtons()}
<div style={{ width: '100%', height: '100%' }} id={rootIdRef.current}/> <div style={{ width: '100%', height: '100%' }} id={rootIdRef.current}/>
</div> </div>
); );

View File

@@ -0,0 +1,61 @@
import { NoteBodyEditorProps } from '../../../utils/types';
const { buildStyle } = require('lib/theme');
export default function styles(props:NoteBodyEditorProps) {
return buildStyle(['TinyMCE', props.style.width, props.style.height], props.themeId, (theme:any) => {
const extraToolbarContainer = {
backgroundColor: theme.backgroundColor3,
display: 'flex',
flexDirection: 'row',
position: 'absolute',
height: theme.toolbarHeight,
zIndex: 2,
top: 0,
padding: theme.toolbarPadding,
};
return {
disabledOverlay: {
zIndex: 10,
position: 'absolute',
backgroundColor: 'white',
opacity: 0.7,
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: 20,
paddingTop: 50,
textAlign: 'center',
width: '100%',
},
rootStyle: {
position: 'relative',
width: props.style.width,
height: props.style.height,
},
leftExtraToolbarContainer: {
...extraToolbarContainer,
width: 80,
left: 0,
},
rightExtraToolbarContainer: {
...extraToolbarContainer,
alignItems: 'center',
justifyContent: 'flex-end',
width: 70,
right: 0,
paddingRight: theme.mainPadding,
},
extraToolbarButton: {
display: 'flex',
border: 'none',
background: 'none',
},
extraToolbarButtonIcon: {
fontSize: theme.toolbarIconSize,
color: theme.color3,
},
};
});
}

View File

@@ -13,13 +13,18 @@ import useMessageHandler from './utils/useMessageHandler';
import useWindowCommandHandler from './utils/useWindowCommandHandler'; import useWindowCommandHandler from './utils/useWindowCommandHandler';
import useDropHandler from './utils/useDropHandler'; import useDropHandler from './utils/useDropHandler';
import useMarkupToHtml from './utils/useMarkupToHtml'; import useMarkupToHtml from './utils/useMarkupToHtml';
import useNoteToolbarButtons from './utils/useNoteToolbarButtons';
import useFormNote, { OnLoadEvent } from './utils/useFormNote'; import useFormNote, { OnLoadEvent } from './utils/useFormNote';
import useFolder from './utils/useFolder';
import styles_ from './styles'; import styles_ from './styles';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types'; import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types';
import ResourceEditWatcher from '../../lib/services/ResourceEditWatcher/index'; import ResourceEditWatcher from '../../lib/services/ResourceEditWatcher/index';
import CommandService from '../../lib/services/CommandService'; import CommandService from 'lib/services/CommandService';
import ToolbarButton from '../ToolbarButton/ToolbarButton';
import Button, { ButtonLevel } from '../Button/Button';
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const { substrWithEllipsis } = require('lib/string-utils');
const NoteSearchBar = require('../NoteSearchBar.min.js'); const NoteSearchBar = require('../NoteSearchBar.min.js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
@@ -70,6 +75,8 @@ function NoteEditor(props: NoteEditorProps) {
const formNoteRef = useRef<FormNote>(); const formNoteRef = useRef<FormNote>();
formNoteRef.current = { ...formNote }; formNoteRef.current = { ...formNote };
const formNoteFolder = useFolder({ folderId: formNote.parent_id });
const { const {
localSearch, localSearch,
onChange: localSearch_change, onChange: localSearch_change,
@@ -133,17 +140,17 @@ function NoteEditor(props: NoteEditorProps) {
return formNote.saveActionQueue.waitForAllDone(); return formNote.saveActionQueue.waitForAllDone();
} }
const markupToHtml = useMarkupToHtml({ themeId: props.theme, customCss: props.customCss }); const markupToHtml = useMarkupToHtml({ themeId: props.themeId, customCss: props.customCss });
const allAssets = useCallback(async (markupLanguage: number): Promise<any[]> => { const allAssets = useCallback(async (markupLanguage: number): Promise<any[]> => {
const theme = themeStyle(props.theme); const theme = themeStyle(props.themeId);
const markupToHtml = markupLanguageUtils.newMarkupToHtml({ const markupToHtml = markupLanguageUtils.newMarkupToHtml({
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`, resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
}); });
return markupToHtml.allAssets(markupLanguage, theme); return markupToHtml.allAssets(markupLanguage, theme);
}, [props.theme]); }, [props.themeId]);
const handleProvisionalFlag = useCallback(() => { const handleProvisionalFlag = useCallback(() => {
if (props.isProvisional) { if (props.isProvisional) {
@@ -336,20 +343,36 @@ function NoteEditor(props: NoteEditorProps) {
}; };
return <NoteToolbar return <NoteToolbar
theme={props.theme} themeId={props.themeId}
note={formNote} note={formNote}
style={toolbarStyle} style={toolbarStyle}
/>; />;
} }
function renderTagButton() {
const info = CommandService.instance().commandToToolbarButton('setTags');
return <ToolbarButton
themeId={props.themeId}
toolbarButtonInfo={info}
/>;
}
function renderTagBar() { function renderTagBar() {
return props.selectedNoteTags.length ? <TagList items={props.selectedNoteTags} /> : null; const theme = themeStyle(props.themeId);
const noteIds = [formNote.id];
const instructions = <span onClick={() => { CommandService.instance().execute('setTags', { noteIds }); }} style={{ ...theme.clickableTextStyle, whiteSpace: 'nowrap' }}>Click to add tags...</span>;
const tagList = props.selectedNoteTags.length ? <TagList items={props.selectedNoteTags} /> : null;
return (
<div style={{ paddingLeft: 8, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>{tagList}{instructions}</div>
);
} }
function renderTitleBar() { function renderTitleBar() {
const theme = themeStyle(props.themeId);
const titleBarDate = <span style={styles.titleDate}>{time.formatMsToLocal(formNote.user_updated_time)}</span>; const titleBarDate = <span style={styles.titleDate}>{time.formatMsToLocal(formNote.user_updated_time)}</span>;
return ( return (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', height: theme.topRowHeight }}>
<input <input
type="text" type="text"
ref={titleInputRef} ref={titleInputRef}
@@ -360,6 +383,7 @@ function NoteEditor(props: NoteEditorProps) {
value={formNote.title} value={formNote.title}
/> />
{titleBarDate} {titleBarDate}
{renderNoteToolbar()}
</div> </div>
); );
} }
@@ -381,7 +405,7 @@ function NoteEditor(props: NoteEditorProps) {
markupToHtml: markupToHtml, markupToHtml: markupToHtml,
allAssets: allAssets, allAssets: allAssets,
disabled: false, disabled: false,
theme: props.theme, themeId: props.themeId,
dispatch: props.dispatch, dispatch: props.dispatch,
noteToolbar: null,// renderNoteToolbar(), noteToolbar: null,// renderNoteToolbar(),
onScroll: onScroll, onScroll: onScroll,
@@ -391,6 +415,7 @@ function NoteEditor(props: NoteEditorProps) {
keyboardMode: Setting.value('editor.keyboardMode'), keyboardMode: Setting.value('editor.keyboardMode'),
locale: Setting.value('locale'), locale: Setting.value('locale'),
onDrop: onDrop, onDrop: onDrop,
noteToolbarButtonInfos: useNoteToolbarButtons(),
}; };
let editor = null; let editor = null;
@@ -414,10 +439,10 @@ function NoteEditor(props: NoteEditorProps) {
}, []); }, []);
if (showRevisions) { if (showRevisions) {
const theme = themeStyle(props.theme); const theme = themeStyle(props.themeId);
const revStyle = { const revStyle:any = {
...props.style, // ...props.style,
display: 'inline-flex', display: 'inline-flex',
padding: theme.margin, padding: theme.margin,
verticalAlign: 'top', verticalAlign: 'top',
@@ -433,19 +458,18 @@ function NoteEditor(props: NoteEditorProps) {
if (props.selectedNoteIds.length > 1) { if (props.selectedNoteIds.length > 1) {
return <MultiNoteActions return <MultiNoteActions
theme={props.theme} themeId={props.themeId}
selectedNoteIds={props.selectedNoteIds} selectedNoteIds={props.selectedNoteIds}
notes={props.notes} notes={props.notes}
dispatch={props.dispatch} dispatch={props.dispatch}
watchedNoteFiles={props.watchedNoteFiles} watchedNoteFiles={props.watchedNoteFiles}
style={props.style}
/>; />;
} }
function renderSearchBar() { function renderSearchBar() {
if (!showLocalSearch) return false; if (!showLocalSearch) return false;
const theme = themeStyle(props.theme); const theme = themeStyle(props.themeId);
return ( return (
<NoteSearchBar <NoteSearchBar
@@ -479,6 +503,30 @@ function NoteEditor(props: NoteEditorProps) {
); );
} }
function renderSearchInfo() {
if (formNoteFolder && ['Search', 'Tag', 'SmartFilter'].includes(props.notesParentType)) {
return (
<div style={{ paddingTop: 10, paddingBottom: 10 }}>
<Button
iconName="icon-notebooks"
level={ButtonLevel.Primary}
title={_('In: %s', substrWithEllipsis(formNoteFolder.title, 0, 100))}
onClick={() => {
props.dispatch({
type: 'FOLDER_AND_NOTE_SELECT',
folderId: formNoteFolder.id,
noteId: formNote.id,
});
}}
/>
<div style={{ flex: 1 }}></div>
</div>
);
} else {
return null;
}
}
if (formNote.encryption_applied || !formNote.id || !props.noteId) { if (formNote.encryption_applied || !formNote.id || !props.noteId) {
return renderNoNotes(styles.root); return renderNoNotes(styles.root);
} }
@@ -488,15 +536,17 @@ function NoteEditor(props: NoteEditorProps) {
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{renderResourceWatchingNotification()} {renderResourceWatchingNotification()}
{renderTitleBar()} {renderTitleBar()}
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> {renderSearchInfo()}
{renderNoteToolbar()}{renderTagBar()}
</div>
<div style={{ display: 'flex', flex: 1 }}> <div style={{ display: 'flex', flex: 1 }}>
{editor} {editor}
</div> </div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
{renderSearchBar()} {renderSearchBar()}
</div> </div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', height: 40 }}>
{renderTagButton()}
{renderTagBar()}
</div>
{wysiwygBanner} {wysiwygBanner}
</div> </div>
</div> </div>
@@ -518,7 +568,7 @@ const mapStateToProps = (state: any) => {
isProvisional: state.provisionalNoteIds.includes(noteId), isProvisional: state.provisionalNoteIds.includes(noteId),
editorNoteStatuses: state.editorNoteStatuses, editorNoteStatuses: state.editorNoteStatuses,
syncStarted: state.syncStarted, syncStarted: state.syncStarted,
theme: state.settings.theme, themeId: state.settings.theme,
watchedNoteFiles: state.watchedNoteFiles, watchedNoteFiles: state.watchedNoteFiles,
notesParentType: state.notesParentType, notesParentType: state.notesParentType,
selectedNoteTags: state.selectedNoteTags, selectedNoteTags: state.selectedNoteTags,

View File

@@ -28,57 +28,57 @@ const declarations:CommandDeclaration[] = [
{ {
name: 'textBold', name: 'textBold',
label: () => _('Bold'), label: () => _('Bold'),
iconName: 'fa-bold', iconName: 'icon-bold',
}, },
{ {
name: 'textItalic', name: 'textItalic',
label: () => _('Italic'), label: () => _('Italic'),
iconName: 'fa-italic', iconName: 'icon-italic',
}, },
{ {
name: 'textLink', name: 'textLink',
label: () => _('Hyperlink'), label: () => _('Hyperlink'),
iconName: 'fa-link', iconName: 'icon-link',
}, },
{ {
name: 'textCode', name: 'textCode',
label: () => _('Code'), label: () => _('Code'),
iconName: 'fa-code', iconName: 'icon-code',
}, },
{ {
name: 'attachFile', name: 'attachFile',
label: () => _('Attach file'), label: () => _('Attach file'),
iconName: 'fa-paperclip', iconName: 'icon-attachment',
}, },
{ {
name: 'textNumberedList', name: 'textNumberedList',
label: () => _('Numbered List'), label: () => _('Numbered List'),
iconName: 'fa-list-ol', iconName: 'icon-numbered-list',
}, },
{ {
name: 'textBulletedList', name: 'textBulletedList',
label: () => _('Bulleted List'), label: () => _('Bulleted List'),
iconName: 'fa-list-ul', iconName: 'icon-bulleted-list',
}, },
{ {
name: 'textCheckbox', name: 'textCheckbox',
label: () => _('Checkbox'), label: () => _('Checkbox'),
iconName: 'fa-check-square', iconName: 'icon-to-do-list',
}, },
{ {
name: 'textHeading', name: 'textHeading',
label: () => _('Heading'), label: () => _('Heading'),
iconName: 'fa-heading', iconName: 'icon-heading',
}, },
{ {
name: 'textHorizontalRule', name: 'textHorizontalRule',
label: () => _('Horizontal Rule'), label: () => _('Horizontal Rule'),
iconName: 'fa-ellipsis-h', iconName: 'fas fa-ellipsis-h',
}, },
{ {
name: 'insertDateTime', name: 'insertDateTime',
label: () => _('Insert Date Time'), label: () => _('Insert Date Time'),
iconName: 'fa-calendar-plus', iconName: 'icon-add-date',
}, },
]; ];

View File

@@ -3,16 +3,18 @@ import { NoteEditorProps } from '../utils/types';
const { buildStyle } = require('lib/theme'); const { buildStyle } = require('lib/theme');
export default function styles(props: NoteEditorProps) { export default function styles(props: NoteEditorProps) {
return buildStyle(['NoteEditor', props.style.width, props.style.height], props.theme, (theme: any) => { return buildStyle(['NoteEditor'], props.themeId, (theme: any) => {
return { return {
root: { root: {
...props.style, // ...props.style,
boxSizing: 'border-box', boxSizing: 'border-box',
paddingLeft: 10, paddingLeft: theme.mainPadding,
paddingTop: 5, paddingTop: 0,
borderLeftWidth: 1, borderLeftWidth: 1,
borderLeftColor: theme.dividerColor, borderLeftColor: theme.dividerColor,
borderLeftStyle: 'solid', borderLeftStyle: 'solid',
width: '100%',
height: '100%',
}, },
titleInput: { titleInput: {
flex: 1, flex: 1,
@@ -20,16 +22,15 @@ export default function styles(props: NoteEditorProps) {
paddingTop: 5, paddingTop: 5,
minHeight: 35, minHeight: 35,
boxSizing: 'border-box', boxSizing: 'border-box',
fontWeight: 'bold',
paddingBottom: 5, paddingBottom: 5,
paddingLeft: 8, paddingLeft: 0,
paddingRight: 8, paddingRight: 8,
marginLeft: 5, marginLeft: 5,
// marginRight: theme.paddingLeft,
color: theme.textStyle.color, color: theme.textStyle.color,
fontSize: theme.textStyle.fontSize * 1.25, fontSize: Math.round(theme.textStyle.fontSize * 1.5),
backgroundColor: theme.backgroundColor, backgroundColor: theme.backgroundColor,
border: '1px solid', border: 'none',
borderColor: theme.dividerColor,
}, },
warningBanner: { warningBanner: {
background: theme.warningBackgroundColor, background: theme.warningBackgroundColor,

View File

@@ -1,10 +1,15 @@
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import AsyncActionQueue from '../../../lib/AsyncActionQueue'; import AsyncActionQueue from '../../../lib/AsyncActionQueue';
import { ToolbarButtonInfo } from 'lib/services/CommandService';
export interface ToolbarButtonInfos {
[key:string]: ToolbarButtonInfo;
}
export interface NoteEditorProps { export interface NoteEditorProps {
style: any; // style: any;
noteId: string; noteId: string;
theme: number; themeId: number;
dispatch: Function; dispatch: Function;
selectedNoteIds: string[]; selectedNoteIds: string[];
notes: any[]; notes: any[];
@@ -29,7 +34,7 @@ export interface NoteEditorProps {
export interface NoteBodyEditorProps { export interface NoteBodyEditorProps {
style: any; style: any;
ref: any, ref: any,
theme: number; themeId: number;
content: string, content: string,
contentKey: string, contentKey: string,
contentMarkupLanguage: number, contentMarkupLanguage: number,
@@ -51,6 +56,7 @@ export interface NoteBodyEditorProps {
resourceInfos: ResourceInfos, resourceInfos: ResourceInfos,
locale: string, locale: string,
onDrop: Function, onDrop: Function,
noteToolbarButtonInfos: ToolbarButtonInfos,
} }
export interface FormNote { export interface FormNote {

View File

@@ -0,0 +1,29 @@
import { useState, useEffect } from 'react';
const Folder = require('lib/models/Folder');
interface HookDependencies {
folderId: string,
}
export default function(dependencies:HookDependencies) {
const { folderId } = dependencies;
const [folder, setFolder] = useState(null);
useEffect(function() {
let cancelled = false;
async function loadFolder() {
const f = await Folder.load(folderId);
if (cancelled) return;
setFolder(f);
}
loadFolder();
return function() {
cancelled = true;
};
}, [folderId]);
return folder;
}

View File

@@ -0,0 +1,33 @@
import { useState, useEffect } from 'react';
import CommandService, { ToolbarButtonInfo } from 'lib/services/CommandService';
interface ToolbarButtonInfos {
[key:string]: ToolbarButtonInfo;
}
export default function useNoteToolbarButtons():ToolbarButtonInfos {
const [noteToolbarButtons, setNoteToolbarButtons] = useState<ToolbarButtonInfos>({});
function update() {
const buttonNames = ['historyBackward', 'historyForward', 'toggleEditors', 'startExternalEditing'];
const output:ToolbarButtonInfos = {};
for (const buttonName of buttonNames) {
output[buttonName] = CommandService.instance().commandToToolbarButton(buttonName);
}
setNoteToolbarButtons(output);
}
useEffect(() => {
update();
CommandService.instance().on('commandsEnabledStateChange', update);
return () => {
CommandService.instance().off('commandsEnabledStateChange', update);
};
}, []);
return noteToolbarButtons;
}

View File

@@ -44,6 +44,7 @@ function editorCommandRuntime(declaration:CommandDeclaration, editorRef:any):Com
} }
}, },
isEnabled: (props:any) => { isEnabled: (props:any) => {
if (props.routeName !== 'Main' || props.isDialogVisible) return false;
if (props.markdownEditorViewerOnly) return false; if (props.markdownEditorViewerOnly) return false;
if (!props.noteId) return false; if (!props.noteId) return false;
const note = BaseModel.byId(props.notes, props.noteId); const note = BaseModel.byId(props.notes, props.noteId);
@@ -58,6 +59,8 @@ function editorCommandRuntime(declaration:CommandDeclaration, editorRef:any):Com
noteVisiblePanes: state.noteVisiblePanes, noteVisiblePanes: state.noteVisiblePanes,
notes: state.notes, notes: state.notes,
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null, noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
routeName: state.route.routeName,
isDialogVisible: !!Object.keys(state.visibleDialogs).length,
}; };
}, },
}; };

View File

@@ -12,11 +12,18 @@ const Setting = require('lib/models/Setting');
const NoteListUtils = require('../utils/NoteListUtils'); const NoteListUtils = require('../utils/NoteListUtils');
const NoteListItem = require('../NoteListItem').default; const NoteListItem = require('../NoteListItem').default;
const CommandService = require('lib/services/CommandService.js').default; const CommandService = require('lib/services/CommandService.js').default;
const styled = require('styled-components').default;
const commands = [ const commands = [
require('./commands/focusElementNoteList'), require('./commands/focusElementNoteList'),
]; ];
const StyledRoot = styled.div`
width: 100%;
height: 100%;
background-color: ${(props:any) => props.theme.backgroundColor3};
`;
class NoteListComponent extends React.Component { class NoteListComponent extends React.Component {
constructor() { constructor() {
super(); super();
@@ -27,12 +34,15 @@ class NoteListComponent extends React.Component {
this.state = { this.state = {
dragOverTargetNoteIndex: null, dragOverTargetNoteIndex: null,
width: 0,
height: 0,
}; };
this.noteListRef = React.createRef();
this.itemListRef = React.createRef(); this.itemListRef = React.createRef();
this.itemAnchorRefs_ = {}; this.itemAnchorRefs_ = {};
this.itemRenderer = this.itemRenderer.bind(this); this.renderItem = this.renderItem.bind(this);
this.onKeyDown = this.onKeyDown.bind(this); this.onKeyDown = this.onKeyDown.bind(this);
this.noteItem_titleClick = this.noteItem_titleClick.bind(this); this.noteItem_titleClick = this.noteItem_titleClick.bind(this);
this.noteItem_noteDragOver = this.noteItem_noteDragOver.bind(this); this.noteItem_noteDragOver = this.noteItem_noteDragOver.bind(this);
@@ -43,12 +53,13 @@ class NoteListComponent extends React.Component {
this.registerGlobalDragEndEvent_ = this.registerGlobalDragEndEvent_.bind(this); this.registerGlobalDragEndEvent_ = this.registerGlobalDragEndEvent_.bind(this);
this.unregisterGlobalDragEndEvent_ = this.unregisterGlobalDragEndEvent_.bind(this); this.unregisterGlobalDragEndEvent_ = this.unregisterGlobalDragEndEvent_.bind(this);
this.itemContextMenu = this.itemContextMenu.bind(this); this.itemContextMenu = this.itemContextMenu.bind(this);
this.resizableLayout_resize = this.resizableLayout_resize.bind(this);
} }
style() { style() {
if (this.styleCache_ && this.styleCache_[this.props.theme]) return this.styleCache_[this.props.theme]; if (this.styleCache_ && this.styleCache_[this.props.themeId]) return this.styleCache_[this.props.themeId];
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = { const style = {
root: { root: {
@@ -85,12 +96,12 @@ class NoteListComponent extends React.Component {
}; };
this.styleCache_ = {}; this.styleCache_ = {};
this.styleCache_[this.props.theme] = style; this.styleCache_[this.props.themeId] = style;
return style; return style;
} }
itemContextMenu(event) { itemContextMenu(event:any) {
const currentItemId = event.currentTarget.getAttribute('data-id'); const currentItemId = event.currentTarget.getAttribute('data-id');
if (!currentItemId) return; if (!currentItemId) return;
@@ -128,11 +139,11 @@ class NoteListComponent extends React.Component {
document.removeEventListener('dragend', this.onGlobalDrop_); document.removeEventListener('dragend', this.onGlobalDrop_);
} }
dragTargetNoteIndex_(event) { dragTargetNoteIndex_(event:any) {
return Math.abs(Math.round((event.clientY - this.itemListRef.current.offsetTop()) / this.itemHeight)); return Math.abs(Math.round((event.clientY - this.itemListRef.current.offsetTop() + this.itemListRef.current.offsetScroll()) / this.itemHeight));
} }
noteItem_noteDragOver(event) { noteItem_noteDragOver(event:any) {
if (this.props.notesParentType !== 'Folder') return; if (this.props.notesParentType !== 'Folder') return;
const dt = event.dataTransfer; const dt = event.dataTransfer;
@@ -146,7 +157,7 @@ class NoteListComponent extends React.Component {
} }
} }
async noteItem_noteDrop(event) { async noteItem_noteDrop(event:any) {
if (this.props.notesParentType !== 'Folder') return; if (this.props.notesParentType !== 'Folder') return;
if (this.props.noteSortOrder !== 'order') { if (this.props.noteSortOrder !== 'order') {
@@ -172,7 +183,7 @@ class NoteListComponent extends React.Component {
} }
async noteItem_checkboxClick(event, item) { async noteItem_checkboxClick(event:any, item:any) {
const checked = event.target.checked; const checked = event.target.checked;
const newNote = { const newNote = {
id: item.id, id: item.id,
@@ -182,7 +193,7 @@ class NoteListComponent extends React.Component {
eventManager.emit('todoToggle', { noteId: item.id, note: newNote }); eventManager.emit('todoToggle', { noteId: item.id, note: newNote });
} }
async noteItem_titleClick(event, item) { async noteItem_titleClick(event:any, item:any) {
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
event.preventDefault(); event.preventDefault();
this.props.dispatch({ this.props.dispatch({
@@ -203,7 +214,7 @@ class NoteListComponent extends React.Component {
} }
} }
noteItem_dragStart(event) { noteItem_dragStart(event:any) {
let noteIds = []; let noteIds = [];
// Here there is two cases: // Here there is two cases:
@@ -223,7 +234,7 @@ class NoteListComponent extends React.Component {
event.dataTransfer.setData('text/x-jop-note-ids', JSON.stringify(noteIds)); event.dataTransfer.setData('text/x-jop-note-ids', JSON.stringify(noteIds));
} }
itemRenderer(item, index) { renderItem(item:any, index:number) {
const highlightedWords = () => { const highlightedWords = () => {
if (this.props.notesParentType === 'Search') { if (this.props.notesParentType === 'Search') {
const query = BaseModel.byId(this.props.searches, this.props.selectedSearchId); const query = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
@@ -240,11 +251,12 @@ class NoteListComponent extends React.Component {
return <NoteListItem return <NoteListItem
ref={ref} ref={ref}
key={item.id} key={item.id}
style={this.style(this.props.theme)} style={this.style()}
item={item} item={item}
index={index} index={index}
theme={this.props.theme} themeId={this.props.themeId}
width={this.props.style.width} width={this.state.width}
height={this.itemHeight}
dragItemIndex={this.state.dragOverTargetNoteIndex} dragItemIndex={this.state.dragOverTargetNoteIndex}
highlightedWords={highlightedWords()} highlightedWords={highlightedWords()}
isProvisional={this.props.provisionalNoteIds.includes(item.id)} isProvisional={this.props.provisionalNoteIds.includes(item.id)}
@@ -260,12 +272,12 @@ class NoteListComponent extends React.Component {
/>; />;
} }
itemAnchorRef(itemId) { itemAnchorRef(itemId:string) {
if (this.itemAnchorRefs_[itemId] && this.itemAnchorRefs_[itemId].current) return this.itemAnchorRefs_[itemId].current; if (this.itemAnchorRefs_[itemId] && this.itemAnchorRefs_[itemId].current) return this.itemAnchorRefs_[itemId].current;
return null; return null;
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps:any) {
if (prevProps.selectedNoteIds !== this.props.selectedNoteIds && this.props.selectedNoteIds.length === 1) { if (prevProps.selectedNoteIds !== this.props.selectedNoteIds && this.props.selectedNoteIds.length === 1) {
const id = this.props.selectedNoteIds[0]; const id = this.props.selectedNoteIds[0];
const doRefocus = this.props.notes.length < prevProps.notes.length; const doRefocus = this.props.notes.length < prevProps.notes.length;
@@ -281,9 +293,13 @@ class NoteListComponent extends React.Component {
} }
} }
} }
if (prevProps.visible !== this.props.visible) {
this.updateSizeState();
}
} }
scrollNoteIndex_(keyCode, ctrlKey, metaKey, noteIndex) { scrollNoteIndex_(keyCode:any, ctrlKey:any, metaKey:any, noteIndex:any) {
if (keyCode === 33) { if (keyCode === 33) {
// Page Up // Page Up
@@ -314,7 +330,7 @@ class NoteListComponent extends React.Component {
return noteIndex; return noteIndex;
} }
async onKeyDown(event) { async onKeyDown(event:any) {
const keyCode = event.keyCode; const keyCode = event.keyCode;
const noteIds = this.props.selectedNoteIds; const noteIds = this.props.selectedNoteIds;
@@ -350,7 +366,7 @@ class NoteListComponent extends React.Component {
event.preventDefault(); event.preventDefault();
const notes = BaseModel.modelsByIds(this.props.notes, noteIds); const notes = BaseModel.modelsByIds(this.props.notes, noteIds);
const todos = notes.filter(n => !!n.is_todo); const todos = notes.filter((n:any) => !!n.is_todo);
if (!todos.length) return; if (!todos.length) return;
for (let i = 0; i < todos.length; i++) { for (let i = 0; i < todos.length; i++) {
@@ -382,7 +398,7 @@ class NoteListComponent extends React.Component {
} }
} }
focusNoteId_(noteId) { focusNoteId_(noteId:string) {
// - We need to focus the item manually otherwise focus might be lost when the // - We need to focus the item manually otherwise focus might be lost when the
// list is scrolled and items within it are being rebuilt. // list is scrolled and items within it are being rebuilt.
// - We need to use an interval because when leaving the arrow pressed, the rendering // - We need to use an interval because when leaving the arrow pressed, the rendering
@@ -401,56 +417,86 @@ class NoteListComponent extends React.Component {
} }
} }
updateSizeState() {
this.setState({
width: this.noteListRef.current.clientWidth,
height: this.noteListRef.current.clientHeight,
});
}
resizableLayout_resize() {
this.updateSizeState();
}
componentDidMount() {
this.props.resizableLayoutEventEmitter.on('resize', this.resizableLayout_resize);
this.updateSizeState();
}
componentWillUnmount() { componentWillUnmount() {
if (this.focusItemIID_) { if (this.focusItemIID_) {
clearInterval(this.focusItemIID_); clearInterval(this.focusItemIID_);
this.focusItemIID_ = null; this.focusItemIID_ = null;
} }
this.props.resizableLayoutEventEmitter.off('resize', this.resizableLayout_resize);
CommandService.instance().componentUnregisterCommands(commands); CommandService.instance().componentUnregisterCommands(commands);
} }
renderEmptyList() {
if (this.props.notes.length) return null;
const theme = themeStyle(this.props.themeId);
const padding = 10;
const emptyDivStyle = {
padding: `${padding}px`,
fontSize: theme.fontSize,
color: theme.color,
backgroundColor: theme.backgroundColor,
fontFamily: theme.fontFamily,
};
// emptyDivStyle.width = emptyDivStyle.width - padding * 2;
// emptyDivStyle.height = emptyDivStyle.height - padding * 2;
return <div style={emptyDivStyle}>{this.props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}</div>;
}
renderItemList(style:any) {
if (!this.props.notes.length) return null;
return (
<ItemList
ref={this.itemListRef}
disabled={this.props.isInsertingNotes}
itemHeight={this.style().listItem.height}
className={'note-list'}
items={this.props.notes}
style={style}
itemRenderer={this.renderItem}
onKeyDown={this.onKeyDown}
/>
);
}
render() { render() {
const theme = themeStyle(this.props.theme); if (!this.props.size) throw new Error('props.size is required');
const style = this.props.style;
if (!this.props.notes.length) { return (
const padding = 10; <StyledRoot ref={this.noteListRef}>
const emptyDivStyle = Object.assign( {this.renderEmptyList()}
{ {this.renderItemList(this.props.size)}
padding: `${padding}px`, </StyledRoot>
fontSize: theme.fontSize, );
color: theme.color,
backgroundColor: theme.backgroundColor,
fontFamily: theme.fontFamily,
},
style
);
emptyDivStyle.width = emptyDivStyle.width - padding * 2;
emptyDivStyle.height = emptyDivStyle.height - padding * 2;
return <div style={emptyDivStyle}>{this.props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}</div>;
}
return <ItemList
ref={this.itemListRef}
disabled={this.props.isInsertingNotes}
itemHeight={this.style(this.props.theme).listItem.height}
className={'note-list'}
items={this.props.notes}
style={style}
itemRenderer={this.itemRenderer}
onKeyDown={this.onKeyDown}
/>;
} }
} }
const mapStateToProps = state => { const mapStateToProps = (state:any) => {
return { return {
notes: state.notes, notes: state.notes,
folders: state.folders, folders: state.folders,
selectedNoteIds: state.selectedNoteIds, selectedNoteIds: state.selectedNoteIds,
selectedFolderId: state.selectedFolderId, selectedFolderId: state.selectedFolderId,
theme: state.settings.theme, themeId: state.settings.theme,
notesParentType: state.notesParentType, notesParentType: state.notesParentType,
searches: state.searches, searches: state.searches,
selectedSearchId: state.selectedSearchId, selectedSearchId: state.selectedSearchId,
@@ -462,6 +508,4 @@ const mapStateToProps = state => {
}; };
}; };
const NoteList = connect(mapStateToProps)(NoteListComponent); export default connect(mapStateToProps)(NoteListComponent);
module.exports = { NoteList };

View File

@@ -0,0 +1,76 @@
import * as React from 'react';
import { useEffect, useRef } from 'react';
import SearchBar from '../SearchBar/SearchBar';
import Button, { ButtonLevel } from '../Button/Button';
import CommandService from 'lib/services/CommandService';
import { runtime as focusSearchRuntime } from './commands/focusSearch';
const styled = require('styled-components').default;
interface Props {
showNewNoteButtons: boolean,
}
const StyledRoot = styled.div`
width: 100%;
display: flex;
flex-direction: row;
padding: ${(props:any) => props.theme.mainPadding}px;
background-color: ${(props:any) => props.theme.backgroundColor3};
`;
const StyledButton = styled(Button)`
margin-left: 8px;
`;
const ButtonContainer = styled.div`
display: flex;
flex-direction: row;
`;
export default function NoteListControls(props:Props) {
const searchBarRef = useRef(null);
useEffect(function() {
CommandService.instance().registerRuntime('focusSearch', focusSearchRuntime(searchBarRef));
return function() {
CommandService.instance().unregisterRuntime('focusSearch');
};
}, []);
function onNewTodoButtonClick() {
CommandService.instance().execute('newTodo');
}
function onNewNoteButtonClick() {
CommandService.instance().execute('newNote');
}
function renderNewNoteButtons() {
if (!props.showNewNoteButtons) return null;
return (
<ButtonContainer>
<StyledButton
tooltip={CommandService.instance().title('newTodo')}
iconName="far fa-check-square"
level={ButtonLevel.Primary}
onClick={onNewTodoButtonClick}
/>
<StyledButton
tooltip={CommandService.instance().title('newNote')}
iconName="icon-note"
level={ButtonLevel.Primary}
onClick={onNewNoteButtonClick}
/>
</ButtonContainer>
);
}
return (
<StyledRoot>
<SearchBar inputRef={searchBarRef}/>
{renderNewNoteButtons()}
</StyledRoot>
);
}

View File

@@ -6,10 +6,10 @@ export const declaration:CommandDeclaration = {
label: () => _('Search in all the notes'), label: () => _('Search in all the notes'),
}; };
export const runtime = (comp:any):CommandRuntime => { export const runtime = (searchBarRef:any):CommandRuntime => {
return { return {
execute: async () => { execute: async () => {
if (comp.searchElement_) comp.searchElement_.focus(); if (searchBarRef.current) searchBarRef.current.focus();
}, },
}; };
}; };

View File

@@ -5,10 +5,45 @@ const Mark = require('mark.js/dist/mark.min.js');
const markJsUtils = require('lib/markJsUtils'); const markJsUtils = require('lib/markJsUtils');
const Note = require('lib/models/Note'); const Note = require('lib/models/Note');
const { replaceRegexDiacritics, pregQuote } = require('lib/string-utils'); const { replaceRegexDiacritics, pregQuote } = require('lib/string-utils');
const styled = require('styled-components').default;
const StyledRoot = styled.div`
width: ${(props:any) => props.width}px;
height: ${(props:any) => props.height}px;
opacity: ${(props:any) => props.isProvisional ? '0.5' : '1'};
max-width: 100%;
box-sizing: border-box;
display: flex;
align-items: stretch;
position: relative;
background-color: ${(props:any) => props.selected ? props.theme.selectedColor : 'none'};
border-style: solid;
border-color: ${(props:any) => props.theme.color};
border-top-width: ${(props:any) => props.dragItemPosition === 'top' ? 2 : 0}px;
border-bottom-width: ${(props:any) => props.dragItemPosition === 'bottom' ? 2 : 0}px;
border-right: none;
border-left: none;
// https://stackoverflow.com/questions/50174448/css-how-to-add-white-space-before-elements-border
&::before {
content: '';
border-bottom: 1px solid ${(props:any) => props.theme.dividerColor};
width: ${(props:any) => props.width - 32}px;
position: absolute;
bottom: 0;
left: 16px;
}
&:hover {
background-color: ${(props:any) => props.theme.backgroundColorHover3};
}
`;
interface NoteListItemProps { interface NoteListItemProps {
theme: number, themeId: number,
width: number, width: number,
height: number,
style: any, style: any,
dragItemIndex: number, dragItemIndex: number,
highlightedWords: string[], highlightedWords: string[],
@@ -28,8 +63,8 @@ interface NoteListItemProps {
function NoteListItem(props:NoteListItemProps, ref:any) { function NoteListItem(props:NoteListItemProps, ref:any) {
const item = props.item; const item = props.item;
const theme = themeStyle(props.theme); const theme = themeStyle(props.themeId);
const hPadding = 10; const hPadding = 16;
const anchorRef = useRef(null); const anchorRef = useRef(null);
@@ -41,14 +76,11 @@ function NoteListItem(props:NoteListItemProps, ref:any) {
}; };
}); });
let rootStyle = Object.assign({ width: props.width, opacity: props.isProvisional ? 0.5 : 1 }, props.style.listItem); let dragItemPosition = '';
if (props.isSelected) rootStyle = Object.assign(rootStyle, props.style.listItemSelected);
if (props.dragItemIndex === props.index) { if (props.dragItemIndex === props.index) {
rootStyle.borderTop = `2px solid ${theme.color}`; dragItemPosition = 'top';
} else if (props.index === props.itemCount - 1 && props.dragItemIndex >= props.itemCount) { } else if (props.index === props.itemCount - 1 && props.dragItemIndex >= props.itemCount) {
rootStyle.borderBottom = `2px solid ${theme.color}`; dragItemPosition = 'bottom';
} }
const onTitleClick = useCallback((event) => { const onTitleClick = useCallback((event) => {
@@ -65,7 +97,7 @@ function NoteListItem(props:NoteListItemProps, ref:any) {
if (!item.is_todo) return null; if (!item.is_todo) return null;
return ( return (
<div style={{ display: 'flex', height: rootStyle.height, alignItems: 'center', paddingLeft: hPadding }}> <div style={{ display: 'flex', height: props.height, alignItems: 'center', paddingLeft: hPadding }}>
<input <input
style={{ margin: 0, marginBottom: 1, marginRight: 5 }} style={{ margin: 0, marginBottom: 1, marginRight: 5 }}
type="checkbox" type="checkbox"
@@ -118,12 +150,19 @@ function NoteListItem(props:NoteListItemProps, ref:any) {
}; };
const watchedIcon = props.isWatched ? null : <i style={watchedIconStyle} className={'fa fa-share-square'}></i>; const watchedIcon = props.isWatched ? null : <i style={watchedIconStyle} className={'fa fa-share-square'}></i>;
// key={`${item.id}_${item.todo_completed}`}
// Need to include "todo_completed" in key so that checkbox is updated when // Need to include "todo_completed" in key so that checkbox is updated when
// item is changed via sync. // item is changed via sync.
return ( return (
<div className="list-item-container" style={rootStyle} onDragOver={props.onNoteDragOver} onDrop={props.onNoteDrop}> <StyledRoot
className="list-item-container"
onDragOver={props.onNoteDragOver}
onDrop={props.onNoteDrop}
width={props.width}
height={props.height}
isProvisional={props.isProvisional}
selected={props.isSelected}
dragItemPosition={dragItemPosition}
>
{renderCheckbox()} {renderCheckbox()}
<a <a
ref={anchorRef} ref={anchorRef}
@@ -138,7 +177,7 @@ function NoteListItem(props:NoteListItemProps, ref:any) {
{watchedIcon} {watchedIcon}
{titleComp} {titleComp}
</a> </a>
</div> </StyledRoot>
); );
} }

View File

@@ -224,8 +224,8 @@ class NotePropertiesDialog extends React.Component {
} }
createNoteField(key, value) { createNoteField(key, value) {
const styles = this.styles(this.props.theme); const styles = this.styles(this.props.themeId);
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const labelComp = <label style={Object.assign({}, theme.textStyle, theme.controlBoxLabel)}>{this.formatLabel(key)}</label>; const labelComp = <label style={Object.assign({}, theme.textStyle, theme.controlBoxLabel)}>{this.formatLabel(key)}</label>;
let controlComp = null; let controlComp = null;
let editComp = null; let editComp = null;
@@ -356,7 +356,7 @@ class NotePropertiesDialog extends React.Component {
} }
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const formNote = this.state.formNote; const formNote = this.state.formNote;
const noteComps = []; const noteComps = [];
@@ -374,7 +374,7 @@ class NotePropertiesDialog extends React.Component {
<div style={theme.dialogBox}> <div style={theme.dialogBox}>
<div style={theme.dialogTitle}>{_('Note properties')}</div> <div style={theme.dialogTitle}>{_('Note properties')}</div>
<div>{noteComps}</div> <div>{noteComps}</div>
<DialogButtonRow theme={this.props.theme} okButtonRef={this.okButton} onClick={this.buttonRow_click}/> <DialogButtonRow themeId={this.props.themeId} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
</div> </div>
</div> </div>
); );

View File

@@ -2,7 +2,7 @@ const React = require('react');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const NoteTextViewer = require('./NoteTextViewer.min'); const NoteTextViewer = require('./NoteTextViewer').default;
const HelpButton = require('./HelpButton.min'); const HelpButton = require('./HelpButton.min');
const BaseModel = require('lib/BaseModel'); const BaseModel = require('lib/BaseModel');
const Revision = require('lib/models/Revision'); const Revision = require('lib/models/Revision');
@@ -38,7 +38,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
} }
style() { style() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = { const style = {
root: { root: {
@@ -114,7 +114,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
this.setState({ note: note }); this.setState({ note: note });
} }
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const markupToHtml = markupLanguageUtils.newMarkupToHtml({ const markupToHtml = markupLanguageUtils.newMarkupToHtml({
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`, resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
@@ -164,7 +164,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
} }
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = this.style(); const style = this.style();
const revisionListItems = []; const revisionListItems = [];
@@ -213,7 +213,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
}; };
}; };

View File

@@ -17,7 +17,7 @@ class NoteSearchBarComponent extends React.Component {
} }
style() { style() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = { const style = {
root: Object.assign({}, theme.textStyle, { root: Object.assign({}, theme.textStyle, {
@@ -34,7 +34,7 @@ class NoteSearchBarComponent extends React.Component {
} }
buttonIconComponent(iconName, clickHandler, isEnabled) { buttonIconComponent(iconName, clickHandler, isEnabled) {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const searchButton = { const searchButton = {
paddingLeft: 4, paddingLeft: 4,
@@ -119,7 +119,7 @@ class NoteSearchBarComponent extends React.Component {
// backgroundColor needs to cached to a local variable to prevent the // backgroundColor needs to cached to a local variable to prevent the
// colour from blinking. // colour from blinking.
// For more info: https://github.com/laurent22/joplin/pull/2329#issuecomment-578376835 // For more info: https://github.com/laurent22/joplin/pull/2329#issuecomment-578376835
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
if (!this.props.searching) { if (!this.props.searching) {
if (this.props.resultCount === 0 && query.length > 0) { if (this.props.resultCount === 0 && query.length > 0) {
this.backgroundColor = theme.warningBackgroundColor; this.backgroundColor = theme.warningBackgroundColor;
@@ -181,7 +181,7 @@ class NoteSearchBarComponent extends React.Component {
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
}; };
}; };

View File

@@ -5,7 +5,7 @@ const { themeStyle } = require('lib/theme');
class NoteStatusBarComponent extends React.Component { class NoteStatusBarComponent extends React.Component {
style() { style() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = { const style = {
root: Object.assign({}, theme.textStyle, { root: Object.assign({}, theme.textStyle, {
@@ -28,7 +28,7 @@ const mapStateToProps = state => {
// notes: state.notes, // notes: state.notes,
// folders: state.folders, // folders: state.folders,
// selectedNoteIds: state.selectedNoteIds, // selectedNoteIds: state.selectedNoteIds,
theme: state.settings.theme, themeId: state.settings.theme,
}; };
}; };

View File

@@ -1,15 +1,24 @@
const React = require('react'); import * as React from 'react';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
class NoteTextViewerComponent extends React.Component { interface Props {
constructor() { onDomReady: Function,
super(); onIpcMessage: Function,
viewerStyle: any,
}
this.initialized_ = false; class NoteTextViewerComponent extends React.Component<Props, any> {
this.domReady_ = false;
private initialized_:boolean = false;
private domReady_:boolean = false;
private webviewRef_:any;
private webviewListeners_:any = null;
constructor(props:any) {
super(props);
this.webviewRef_ = React.createRef(); this.webviewRef_ = React.createRef();
this.webviewListeners_ = null;
this.webview_domReady = this.webview_domReady.bind(this); this.webview_domReady = this.webview_domReady.bind(this);
this.webview_ipcMessage = this.webview_ipcMessage.bind(this); this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
@@ -17,20 +26,20 @@ class NoteTextViewerComponent extends React.Component {
this.webview_message = this.webview_message.bind(this); this.webview_message = this.webview_message.bind(this);
} }
webview_domReady(event) { webview_domReady(event:any) {
this.domReady_ = true; this.domReady_ = true;
if (this.props.onDomReady) this.props.onDomReady(event); if (this.props.onDomReady) this.props.onDomReady(event);
} }
webview_ipcMessage(event) { webview_ipcMessage(event:any) {
if (this.props.onIpcMessage) this.props.onIpcMessage(event); if (this.props.onIpcMessage) this.props.onIpcMessage(event);
} }
webview_load() { webview_load() {
this.webview_domReady(); this.webview_domReady({});
} }
webview_message(event) { webview_message(event:any) {
if (!event.data || event.data.target !== 'main') return; if (!event.data || event.data.target !== 'main') return;
const callName = event.data.name; const callName = event.data.name;
@@ -78,7 +87,14 @@ class NoteTextViewerComponent extends React.Component {
wv.removeEventListener(n, fn); wv.removeEventListener(n, fn);
} }
this.webviewRef_.current.contentWindow.removeEventListener('message', this.webview_message); try {
// It seems this can throw a cross-origin error in a way that is hard to replicate so just wrap
// it in try/catch since it's not critical.
// https://github.com/laurent22/joplin/issues/3835
this.webviewRef_.current.contentWindow.removeEventListener('message', this.webview_message);
} catch (error) {
reg.logger().warn('Error destroying note viewer', error);
}
this.initialized_ = false; this.initialized_ = false;
this.domReady_ = false; this.domReady_ = false;
@@ -107,7 +123,7 @@ class NoteTextViewerComponent extends React.Component {
// Wrap WebView functions // Wrap WebView functions
// ---------------------------------------------------------------- // ----------------------------------------------------------------
send(channel, arg0 = null, arg1 = null) { send(channel:string, arg0:any = null, arg1:any = null) {
const win = this.webviewRef_.current.contentWindow; const win = this.webviewRef_.current.contentWindow;
if (channel === 'setHtml') { if (channel === 'setHtml') {
@@ -127,36 +143,6 @@ class NoteTextViewerComponent extends React.Component {
} }
} }
printToPDF() { // options, callback) {
// In Electron 4x, printToPDF is broken so need to use this hack:
// https://github.com/electron/electron/issues/16171#issuecomment-451090245
// return this.webviewRef_.current.printToPDF(options, callback);
// return this.webviewRef_.current.getWebContents().printToPDF(options, callback);
}
print() {
// In Electron 4x, print is broken so need to use this hack:
// https://github.com/electron/electron/issues/16219#issuecomment-451454948
// Note that this is not a perfect workaround since it means the options are ignored
// In particular it means that background images and colours won't be printed (printBackground property will be ignored)
// return this.webviewRef_.current.getWebContents().print({});
return this.webviewRef_.current.getWebContents().executeJavaScript('window.print()');
}
openDevTools() {
// return this.webviewRef_.current.openDevTools();
}
closeDevTools() {
// return this.webviewRef_.current.closeDevTools();
}
isDevToolsOpened() {
// return this.webviewRef_.current.isDevToolsOpened();
}
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// Wrap WebView functions (END) // Wrap WebView functions (END)
// ---------------------------------------------------------------- // ----------------------------------------------------------------
@@ -167,9 +153,9 @@ class NoteTextViewerComponent extends React.Component {
} }
} }
const mapStateToProps = state => { const mapStateToProps = (state:any) => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
}; };
}; };
@@ -180,4 +166,4 @@ const NoteTextViewer = connect(
{ withRef: true } { withRef: true }
)(NoteTextViewerComponent); )(NoteTextViewerComponent);
module.exports = NoteTextViewer; export default NoteTextViewer;

View File

@@ -1,19 +1,19 @@
import * as React from 'react'; import * as React from 'react';
import { useEffect, useCallback, useState } from 'react'; import { useEffect, useState } from 'react';
import CommandService from '../../lib/services/CommandService'; import CommandService from '../../lib/services/CommandService';
import ToolbarBase from '../ToolbarBase';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { buildStyle } = require('lib/theme'); const { buildStyle } = require('lib/theme');
const Toolbar = require('../Toolbar.min.js'); // const Folder = require('lib/models/Folder');
const Folder = require('lib/models/Folder'); // const { _ } = require('lib/locale');
const { _ } = require('lib/locale'); // const { substrWithEllipsis } = require('lib/string-utils');
const { substrWithEllipsis } = require('lib/string-utils');
interface ButtonClickEvent { interface ButtonClickEvent {
name: string, name: string,
} }
interface NoteToolbarProps { interface NoteToolbarProps {
theme: number, themeId: number,
style: any, style: any,
folders: any[], folders: any[],
watchedNoteFiles: string[], watchedNoteFiles: string[],
@@ -26,11 +26,12 @@ interface NoteToolbarProps {
} }
function styles_(props:NoteToolbarProps) { function styles_(props:NoteToolbarProps) {
return buildStyle('NoteToolbar', props.theme, (/* theme:any*/) => { return buildStyle('NoteToolbar', props.themeId, (theme:any) => {
return { return {
root: { root: {
...props.style, ...props.style,
borderBottom: 'none', borderBottom: 'none',
backgroundColor: theme.backgroundColor,
}, },
}; };
}); });
@@ -39,52 +40,18 @@ function styles_(props:NoteToolbarProps) {
function NoteToolbar(props:NoteToolbarProps) { function NoteToolbar(props:NoteToolbarProps) {
const styles = styles_(props); const styles = styles_(props);
const [toolbarItems, setToolbarItems] = useState([]); const [toolbarItems, setToolbarItems] = useState([]);
const selectedNoteFolder = Folder.byId(props.folders, props.note.parent_id);
const folderId = selectedNoteFolder ? selectedNoteFolder.id : '';
const folderTitle = selectedNoteFolder && selectedNoteFolder.title ? selectedNoteFolder.title : '';
const cmdService = CommandService.instance(); const cmdService = CommandService.instance();
const updateToolbarItems = useCallback(() => { function updateToolbarItems() {
const output = []; const output = [];
output.push( output.push(cmdService.commandToToolbarButton('editAlarm'));
cmdService.commandToToolbarButton('historyBackward') output.push(cmdService.commandToToolbarButton('toggleVisiblePanes'));
);
output.push(
cmdService.commandToToolbarButton('historyForward')
);
if (folderId && ['Search', 'Tag', 'SmartFilter'].includes(props.notesParentType)) {
output.push({
title: _('In: %s', substrWithEllipsis(folderTitle, 0, 16)),
tooltip: folderTitle,
iconName: 'fa-book',
onClick: () => {
props.dispatch({
type: 'FOLDER_AND_NOTE_SELECT',
folderId: folderId,
noteId: props.note.id,
});
},
});
}
output.push(cmdService.commandToToolbarButton('showNoteProperties')); output.push(cmdService.commandToToolbarButton('showNoteProperties'));
if (props.watchedNoteFiles.indexOf(props.note.id) >= 0) {
output.push(cmdService.commandToToolbarButton('stopExternalEditing'));
} else {
output.push(cmdService.commandToToolbarButton('startExternalEditing'));
}
output.push(cmdService.commandToToolbarButton('editAlarm'));
output.push(cmdService.commandToToolbarButton('setTags'));
setToolbarItems(output); setToolbarItems(output);
}, [props.note.id, folderId, folderTitle, props.watchedNoteFiles, props.notesParentType]); }
useEffect(() => { useEffect(() => {
updateToolbarItems(); updateToolbarItems();
@@ -92,9 +59,9 @@ function NoteToolbar(props:NoteToolbarProps) {
return () => { return () => {
cmdService.off('commandsEnabledStateChange', updateToolbarItems); cmdService.off('commandsEnabledStateChange', updateToolbarItems);
}; };
}, [updateToolbarItems]); }, []);
return <Toolbar style={styles.root} items={toolbarItems} />; return <ToolbarBase style={styles.root} items={toolbarItems} />;
} }
const mapStateToProps = (state:any) => { const mapStateToProps = (state:any) => {

View File

@@ -1,16 +1,21 @@
const React = require('react'); import * as React from 'react';
import ButtonBar from './ConfigScreen/ButtonBar';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const Setting = require('lib/models/Setting'); const Setting = require('lib/models/Setting');
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
const { Header } = require('./Header/Header.min.js');
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js'); const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js');
class OneDriveLoginScreenComponent extends React.Component { interface Props {
constructor() { themeId: string,
super(); }
class OneDriveLoginScreenComponent extends React.Component<any, any> {
constructor(props:Props) {
super(props);
this.state = { this.state = {
authLog: [], authLog: [],
@@ -18,8 +23,8 @@ class OneDriveLoginScreenComponent extends React.Component {
} }
async componentDidMount() { async componentDidMount() {
const log = (s) => { const log = (s:any) => {
this.setState(state => { this.setState((state:any) => {
const authLog = state.authLog.slice(); const authLog = state.authLog.slice();
authLog.push({ key: (Date.now() + Math.random()).toString(), text: s }); authLog.push({ key: (Date.now() + Math.random()).toString(), text: s });
return { authLog: authLog }; return { authLog: authLog };
@@ -30,7 +35,7 @@ class OneDriveLoginScreenComponent extends React.Component {
const syncTarget = reg.syncTarget(syncTargetId); const syncTarget = reg.syncTarget(syncTargetId);
const oneDriveApiUtils = new OneDriveApiNodeUtils(syncTarget.api()); const oneDriveApiUtils = new OneDriveApiNodeUtils(syncTarget.api());
const auth = await oneDriveApiUtils.oauthDance({ const auth = await oneDriveApiUtils.oauthDance({
log: (s) => log(s), log: (s:any) => log(s),
}); });
Setting.setValue(`sync.${syncTargetId}.auth`, auth ? JSON.stringify(auth) : null); Setting.setValue(`sync.${syncTargetId}.auth`, auth ? JSON.stringify(auth) : null);
@@ -52,9 +57,7 @@ class OneDriveLoginScreenComponent extends React.Component {
} }
render() { render() {
const style = this.props.style; const theme = themeStyle(this.props.themeId);
const theme = themeStyle(this.props.theme);
const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width });
const logComps = []; const logComps = [];
for (const l of this.state.authLog) { for (const l of this.state.authLog) {
@@ -66,22 +69,23 @@ class OneDriveLoginScreenComponent extends React.Component {
} }
return ( return (
<div> <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Header style={headerStyle}/> <div style={{ padding: theme.configScreenPadding, flex: 1 }}>
<div style={{ padding: 10 }}>
{logComps} {logComps}
</div> </div>
<ButtonBar
onCancelClick={() => this.props.dispatch({ type: 'NAV_BACK' })}
/>
</div> </div>
); );
} }
} }
const mapStateToProps = state => { const mapStateToProps = (state:any) => {
return { return {
theme: state.settings.theme, themeId: state.settings.theme,
}; };
}; };
const OneDriveLoginScreen = connect(mapStateToProps)(OneDriveLoginScreenComponent); export default connect(mapStateToProps)(OneDriveLoginScreenComponent);
module.exports = { OneDriveLoginScreen };

View File

@@ -164,10 +164,10 @@ class PromptDialog extends React.Component {
render() { render() {
const style = this.props.style; const style = this.props.style;
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel']; const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel'];
const styles = this.styles(this.props.theme, style.width, style.height, this.state.visible); const styles = this.styles(this.props.themeId, style.width, style.height, this.state.visible);
const onClose = (accept, buttonType) => { const onClose = (accept, buttonType) => {
if (this.props.onClose) { if (this.props.onClose) {

View File

@@ -0,0 +1,173 @@
import * as React from 'react';
import { useRef, useState } from 'react';
import produce from 'immer';
import useWindowResizeEvent from './hooks/useWindowResizeEvent';
import useLayoutItemSizes, { LayoutItemSizes, itemSize } from './hooks/useLayoutItemSizes';
const { Resizable } = require('re-resizable');
const EventEmitter = require('events');
export enum LayoutItemDirection {
Row = 'row',
Column = 'column',
}
export interface Size {
width: number,
height: number,
}
export interface LayoutItem {
key: string,
width?: number,
height?: number,
minWidth?: number,
minHeight?: number,
children?: LayoutItem[]
direction?: LayoutItemDirection,
resizable?: boolean,
visible?: boolean,
}
interface onResizeEvent {
layout: LayoutItem
}
interface Props {
layout: LayoutItem,
renderItem(key:string, event:any):JSX.Element;
onResize(event:onResizeEvent):void;
width?: number,
height?: number,
}
export function findItemByKey(layout:LayoutItem, key:string):LayoutItem {
function recurseFind(item:LayoutItem):LayoutItem {
if (item.key === key) return item;
if (item.children) {
for (const child of item.children) {
const found = recurseFind(child);
if (found) return found;
}
}
return null;
}
const output = recurseFind(layout);
if (!output) throw new Error(`Invalid item key: ${key}`);
return output;
}
function updateLayoutItem(layout:LayoutItem, key:string, props:any) {
return produce(layout, (draftState:LayoutItem) => {
function recurseFind(item:LayoutItem) {
if (item.key === key) {
for (const n in props) {
(item as any)[n] = props[n];
}
} else {
if (item.children) {
for (const child of item.children) {
recurseFind(child);
}
}
}
}
recurseFind(draftState);
});
}
function renderContainer(item:LayoutItem, sizes:LayoutItemSizes, onResizeStart:Function, onResize:Function, onResizeStop:Function, children:JSX.Element[]):JSX.Element {
const style:any = {
display: item.visible !== false ? 'flex' : 'none',
flexDirection: item.direction,
};
const size:Size = itemSize(item, sizes);
const className = `resizableLayoutItem rli-${item.key}`;
if (item.resizable) {
const enable = { top: false, right: true, bottom: false, left: false, topRight: false, bottomRight: false, bottomLeft: false, topLeft: false };
return (
<Resizable
key={item.key}
className={className}
style={style}
size={size}
onResizeStart={onResizeStart}
onResize={onResize}
onResizeStop={onResizeStop}
enable={enable}
minWidth={item.minWidth}
minHeight={item.minHeight}
>
{children}
</Resizable>
);
} else {
return (
<div key={item.key} className={className} style={{ ...style, ...size }}>
{children}
</div>
);
}
}
function ResizableLayout(props:Props) {
const eventEmitter = useRef(new EventEmitter());
const [resizedItem, setResizedItem] = useState<any>(null);
function renderLayoutItem(item:LayoutItem, sizes:LayoutItemSizes, isVisible:boolean):JSX.Element {
function onResizeStart() {
setResizedItem({
key: item.key,
initialWidth: sizes[item.key].width,
initialHeight: sizes[item.key].height,
});
}
function onResize(_event:any, _direction:any, _refToElement: HTMLDivElement, delta:any) {
const newLayout = updateLayoutItem(props.layout, item.key, {
width: resizedItem.initialWidth + delta.width,
height: resizedItem.initialHeight + delta.height,
});
props.onResize({ layout: newLayout });
eventEmitter.current.emit('resize');
}
function onResizeStop(_event:any, _direction:any, _refToElement: HTMLDivElement, delta:any) {
onResize(_event, _direction, _refToElement, delta);
setResizedItem(null);
}
if (!item.children) {
const comp = props.renderItem(item.key, {
item: item,
eventEmitter: eventEmitter.current,
size: sizes[item.key],
visible: isVisible,
});
return renderContainer(item, sizes, onResizeStart, onResize, onResizeStop, [comp]);
} else {
const childrenComponents = [];
for (const child of item.children) {
childrenComponents.push(renderLayoutItem(child, sizes, isVisible && child.visible !== false));
}
return renderContainer(item, sizes, onResizeStart, onResize, onResizeStop, childrenComponents);
}
}
useWindowResizeEvent(eventEmitter);
const sizes = useLayoutItemSizes(props.layout);
return renderLayoutItem(props.layout, sizes, props.layout.visible !== false);
}
export default ResizableLayout;

View File

@@ -0,0 +1,79 @@
import { useMemo } from 'react';
import { LayoutItem, Size } from '../ResizableLayout';
export interface LayoutItemSizes {
[key:string]: Size,
}
export function itemSize(item:LayoutItem, sizes:LayoutItemSizes):Size {
return {
width: 'width' in item ? item.width : sizes[item.key].width,
height: 'height' in item ? item.height : sizes[item.key].height,
};
}
function calculateChildrenSizes(item:LayoutItem, sizes:LayoutItemSizes):LayoutItemSizes {
if (!item.children) return sizes;
const parentSize = itemSize(item, sizes);
const remainingSize:Size = {
width: parentSize.width,
height: parentSize.height,
};
const noWidthChildren:LayoutItem[] = [];
const noHeightChildren:LayoutItem[] = [];
for (const child of item.children) {
let w = 'width' in child ? child.width : null;
let h = 'height' in child ? child.height : null;
if (child.visible === false) {
w = 0;
h = 0;
}
sizes[child.key] = { width: w, height: h };
if (w !== null) remainingSize.width -= w;
if (h !== null) remainingSize.height -= h;
if (w === null) noWidthChildren.push(child);
if (h === null) noHeightChildren.push(child);
}
if (noWidthChildren.length) {
const w = item.direction === 'row' ? remainingSize.width / noWidthChildren.length : parentSize.width;
for (const child of noWidthChildren) {
sizes[child.key].width = w;
}
}
if (noHeightChildren.length) {
const h = item.direction === 'column' ? remainingSize.height / noHeightChildren.length : parentSize.height;
for (const child of noHeightChildren) {
sizes[child.key].height = h;
}
}
for (const child of item.children) {
const childrenSizes = calculateChildrenSizes(child, sizes);
sizes = { ...sizes, ...childrenSizes };
}
return sizes;
}
export default function useLayoutItemSizes(layout:LayoutItem) {
return useMemo(() => {
let sizes:LayoutItemSizes = {};
if (!('width' in layout) || !('height' in layout)) throw new Error('width and height are required on layout root');
sizes[layout.key] = {
width: layout.width,
height: layout.height,
};
sizes = calculateChildrenSizes(layout, sizes);
return sizes;
}, [layout]);
}

View File

@@ -0,0 +1,17 @@
import { useEffect } from 'react';
const debounce = require('debounce');
export default function useWindowResizeEvent(eventEmitter:any) {
useEffect(() => {
const window_resize = debounce(() => {
eventEmitter.current.emit('resize');
}, 500);
window.addEventListener('resize', window_resize);
return () => {
window_resize.clear();
window.removeEventListener('resize', window_resize);
};
}, []);
}

View File

@@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import ButtonBar from './ConfigScreen/ButtonBar';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
const { Header } = require('./Header/Header.min.js');
const prettyBytes = require('pretty-bytes'); const prettyBytes = require('pretty-bytes');
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
@@ -14,8 +14,9 @@ interface Style {
} }
interface Props { interface Props {
theme: any; themeId: number;
style: Style style: Style,
dispatch: Function,
} }
interface Resource { interface Resource {
@@ -37,7 +38,7 @@ interface ResourceTable {
onResourceClick: (resource: Resource) => any onResourceClick: (resource: Resource) => any
onResourceDelete: (resource: Resource) => any onResourceDelete: (resource: Resource) => any
onToggleSorting: (order: SortingOrder) => any onToggleSorting: (order: SortingOrder) => any
theme: any themeId: number
style: Style style: Style
} }
@@ -50,17 +51,19 @@ interface ActiveSorting {
} }
const ResourceTable: React.FC<ResourceTable> = (props: ResourceTable) => { const ResourceTable: React.FC<ResourceTable> = (props: ResourceTable) => {
const theme = themeStyle(props.themeId);
const sortOrderEngagedMarker = (s: SortingOrder) => { const sortOrderEngagedMarker = (s: SortingOrder) => {
return ( return (
<a href="#" <a href="#"
style={{ color: props.theme.urlColor }} style={{ color: theme.urlColor }}
onClick={() => props.onToggleSorting(s)}>{ onClick={() => props.onToggleSorting(s)}>{
(props.sorting.order === s && props.sorting.type === 'desc') ? '▾' : '▴'}</a> (props.sorting.order === s && props.sorting.type === 'desc') ? '▾' : '▴'}</a>
); );
}; };
const titleCellStyle = { const titleCellStyle = {
...props.theme.textStyle, ...theme.textStyle,
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
overflowX: 'hidden', overflowX: 'hidden',
maxWidth: 1, maxWidth: 1,
@@ -69,14 +72,14 @@ const ResourceTable: React.FC<ResourceTable> = (props: ResourceTable) => {
}; };
const cellStyle = { const cellStyle = {
...props.theme.textStyle, ...theme.textStyle,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
color: props.theme.colorFaded, color: theme.colorFaded,
width: 1, width: 1,
}; };
const headerStyle = { const headerStyle = {
...props.theme.textStyle, ...theme.textStyle,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
width: 1, width: 1,
fontWeight: 'bold', fontWeight: 'bold',
@@ -97,7 +100,7 @@ const ResourceTable: React.FC<ResourceTable> = (props: ResourceTable) => {
<tr key={index}> <tr key={index}>
<td style={titleCellStyle} className="titleCell"> <td style={titleCellStyle} className="titleCell">
<a <a
style={{ color: props.theme.urlColor }} style={{ color: theme.urlColor }}
href="#" href="#"
onClick={() => props.onResourceClick(resource)}>{resource.title || `(${_('Untitled')})`} onClick={() => props.onResourceClick(resource)}>{resource.title || `(${_('Untitled')})`}
</a> </a>
@@ -105,7 +108,7 @@ const ResourceTable: React.FC<ResourceTable> = (props: ResourceTable) => {
<td style={cellStyle} className="dataCell">{prettyBytes(resource.size)}</td> <td style={cellStyle} className="dataCell">{prettyBytes(resource.size)}</td>
<td style={cellStyle} className="dataCell">{resource.id}</td> <td style={cellStyle} className="dataCell">{resource.id}</td>
<td style={cellStyle} className="dataCell"> <td style={cellStyle} className="dataCell">
<button style={props.theme.buttonStyle} onClick={() => props.onResourceDelete(resource)}>{_('Delete')}</button> <button style={theme.buttonStyle} onClick={() => props.onResourceDelete(resource)}>{_('Delete')}</button>
</td> </td>
</tr> </tr>
)} )}
@@ -202,8 +205,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
render() { render() {
const style = this.props.style; const style = this.props.style;
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width });
const rootStyle:any = { const rootStyle:any = {
...style, ...style,
@@ -211,13 +213,16 @@ class ResourceScreenComponent extends React.Component<Props, State> {
color: theme.color, color: theme.color,
padding: 20, padding: 20,
boxSizing: 'border-box', boxSizing: 'border-box',
flex: 1,
}; };
rootStyle.height = style.height - 35; // Minus the header height // rootStyle.height = style.height - 35; // Minus the header height
delete rootStyle.height;
delete rootStyle.width; delete rootStyle.width;
const containerHeight = style.height;
return ( return (
<div style={{ ...theme.containerStyle, fontFamily: theme.fontFamily }}> <div style={{ ...theme.containerStyle, fontFamily: theme.fontFamily, height: containerHeight, display: 'flex', flexDirection: 'column' }}>
<Header style={headerStyle} />
<div style={rootStyle}> <div style={rootStyle}>
<div style={{ ...theme.notificationBox, marginBottom: 10 }}>{ <div style={{ ...theme.notificationBox, marginBottom: 10 }}>{
_('This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.') _('This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.')
@@ -232,7 +237,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
<div>{_('Warning: not all resources shown for performance reasons (limit: %s).', MAX_RESOURCES)}</div> <div>{_('Warning: not all resources shown for performance reasons (limit: %s).', MAX_RESOURCES)}</div>
} }
{this.state.resources && <ResourceTable {this.state.resources && <ResourceTable
theme={theme} themeId={this.props.themeId}
style={style} style={style}
resources={this.state.resources} resources={this.state.resources}
sorting={this.state.sorting} sorting={this.state.sorting}
@@ -243,13 +248,16 @@ class ResourceScreenComponent extends React.Component<Props, State> {
</div> </div>
} }
</div> </div>
<ButtonBar
onCancelClick={() => this.props.dispatch({ type: 'NAV_BACK' })}
/>
</div> </div>
); );
} }
} }
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: any) => ({
theme: state.settings.theme, themeId: state.settings.theme,
}); });
const ResourceScreen = connect(mapStateToProps)(ResourceScreenComponent); const ResourceScreen = connect(mapStateToProps)(ResourceScreenComponent);

View File

@@ -5,20 +5,30 @@ const { connect, Provider } = require('react-redux');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');
const { MainScreen } = require('.//MainScreen/MainScreen.min.js'); const MainScreen = require('./MainScreen/MainScreen').default;
const ConfigScreen = require('./ConfigScreen/ConfigScreen').default;
const StatusScreen = require('./StatusScreen/StatusScreen').default;
const OneDriveLoginScreen = require('./OneDriveLoginScreen').default;
const DropboxLoginScreen = require('./DropboxLoginScreen').default;
const ErrorBoundary = require('./ErrorBoundary').default; const ErrorBoundary = require('./ErrorBoundary').default;
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
const { DropboxLoginScreen } = require('./DropboxLoginScreen.min.js');
const { StatusScreen } = require('./StatusScreen.min.js');
const { ImportScreen } = require('./ImportScreen.min.js'); const { ImportScreen } = require('./ImportScreen.min.js');
const { ConfigScreen } = require('./ConfigScreen.min.js');
const { ResourceScreen } = require('./ResourceScreen.js'); const { ResourceScreen } = require('./ResourceScreen.js');
const { Navigator } = require('./Navigator.min.js'); const { Navigator } = require('./Navigator.min.js');
const WelcomeUtils = require('lib/WelcomeUtils'); const WelcomeUtils = require('lib/WelcomeUtils');
const { app } = require('../app'); const { app } = require('../app');
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
const { themeStyle } = require('lib/theme');
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
const GlobalStyle = createGlobalStyle`
div, span, a {
color: ${(props) => props.theme.color};
/*font-size: ${(props) => props.theme.fontSize}px;*/
font-family: ${(props) => props.theme.fontFamily};
}
`;
async function initialize() { async function initialize() {
this.wcsTimeoutId_ = null; this.wcsTimeoutId_ = null;
@@ -84,6 +94,8 @@ class RootComponent extends React.Component {
height: this.props.size.height / this.props.zoomFactor, height: this.props.size.height / this.props.zoomFactor,
}; };
const theme = themeStyle(this.props.themeId);
const screens = { const screens = {
Main: { screen: MainScreen }, Main: { screen: MainScreen },
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') }, OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
@@ -94,7 +106,14 @@ class RootComponent extends React.Component {
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') }, Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
}; };
return <Navigator style={navigatorStyle} screens={screens} />; return (
<StyleSheetManager disableVendorPrefixes>
<ThemeProvider theme={theme}>
<GlobalStyle/>
<Navigator style={navigatorStyle} screens={screens} />
</ThemeProvider>
</StyleSheetManager>
);
} }
} }
@@ -103,6 +122,7 @@ const mapStateToProps = state => {
size: state.windowContentSize, size: state.windowContentSize,
zoomFactor: state.settings.windowContentZoomFactor / 100, zoomFactor: state.settings.windowContentZoomFactor / 100,
appState: state.appState, appState: state.appState,
themeId: state.settings.theme,
}; };
}; };

View File

@@ -0,0 +1,87 @@
import * as React from 'react';
import { useState, useCallback, useEffect } from 'react';
import CommandService from 'lib/services/CommandService';
import useSearch from './hooks/useSearch';
import { Root, SearchInput, SearchButton, SearchButtonIcon } from './styles';
const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
interface Props {
inputRef?: any,
notesParentType: string,
dispatch?: Function,
}
function SearchBar(props:Props) {
const [query, setQuery] = useState('');
const iconName = !query ? CommandService.instance().iconName('search') : 'fa fa-times';
function onChange(event:any) {
setQuery(event.currentTarget.value);
}
function onFocus() {
props.dispatch({
type: 'FOCUS_SET',
field: 'globalSearch',
});
}
function onBlur() {
// Do it after a delay so that the "Clear" button
// can be clicked on (otherwise the field loses focus
// and is resized before the click event has been processed)
setTimeout(() => {
props.dispatch({
type: 'FOCUS_CLEAR',
field: 'globalSearch',
});
}, 300);
}
function onKeyDown(event:any) {
if (event.key === 'Escape') {
setQuery('');
if (document.activeElement) (document.activeElement as any).blur();
}
}
const onSearchButtonClick = useCallback(() => {
setQuery('');
}, []);
useSearch(query);
useEffect(() => {
if (props.notesParentType !== 'Search') {
setQuery('');
}
}, [props.notesParentType]);
return (
<Root>
<SearchInput
ref={props.inputRef}
value={query}
type="text"
placeholder={_('Search...')}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
/>
<SearchButton onClick={onSearchButtonClick}>
<SearchButtonIcon className={iconName}/>
</SearchButton>
</Root>
);
}
const mapStateToProps = (state:any) => {
return {
notesParentType: state.notesParentType,
};
};
export default connect(mapStateToProps)(SearchBar);

View File

@@ -0,0 +1,17 @@
import { useEffect } from 'react';
import CommandService from 'lib/services/CommandService';
const debounce = require('debounce');
export default function useSearch(query:string) {
useEffect(() => {
const search = debounce((query:string) => {
CommandService.instance().execute('search', { query });
}, 500);
search(query);
return () => {
search.clear();
};
}, [query]);
}

View File

@@ -0,0 +1,28 @@
import StyledInput from '../../style/StyledInput';
const styled = require('styled-components').default;
export const Root = styled.div`
position: relative;
display: flex;
width: 100%;
`;
export const SearchButton = styled.button`
position: absolute;
right: 0;
background: none;
border: none;
height: 100%;
opacity: ${(props:any) => props.disabled ? 0.5 : 1};
`;
export const SearchButtonIcon = styled.span`
font-size: ${(props:any) => props.theme.toolbarIconSize}px;
color: ${(props:any) => props.theme.color4};
`;
export const SearchInput = styled(StyledInput)`
padding-right: 20px;
flex: 1;
width: 10px;
`;

View File

@@ -12,7 +12,7 @@ const { reg } = require('lib/registry.js');
const { clipboard } = require('electron'); const { clipboard } = require('electron');
interface ShareNoteDialogProps { interface ShareNoteDialogProps {
theme: number, themeId: number,
noteIds: Array<string>, noteIds: Array<string>,
onClose: Function, onClose: Function,
} }
@@ -22,7 +22,7 @@ interface SharesMap {
} }
function styles_(props:ShareNoteDialogProps) { function styles_(props:ShareNoteDialogProps) {
return buildStyle('ShareNoteDialog', props.theme, (theme:any) => { return buildStyle('ShareNoteDialog', props.themeId, (theme:any) => {
return { return {
noteList: { noteList: {
marginBottom: 10, marginBottom: 10,
@@ -67,7 +67,7 @@ export default function ShareNoteDialog(props:ShareNoteDialogProps) {
const [shares, setShares] = useState<SharesMap>({}); const [shares, setShares] = useState<SharesMap>({});
const noteCount = notes.length; const noteCount = notes.length;
const theme = themeStyle(props.theme); const theme = themeStyle(props.themeId);
const styles = styles_(props); const styles = styles_(props);
useEffect(() => { useEffect(() => {
@@ -206,7 +206,7 @@ export default function ShareNoteDialog(props:ShareNoteDialogProps) {
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button> <button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage(sharesState)}</div> <div style={theme.textStyle}>{statusMessage(sharesState)}</div>
{encryptionWarningMessage} {encryptionWarningMessage}
<DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/> <DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</div> </div>
</div> </div>
); );

View File

@@ -1,766 +0,0 @@
const React = require('react');
const { connect } = require('react-redux');
const shared = require('lib/components/shared/side-menu-shared.js');
const { Synchronizer } = require('lib/synchronizer.js');
const CommandService = require('lib/services/CommandService.js').default;
const BaseModel = require('lib/BaseModel.js');
const Setting = require('lib/models/Setting.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/theme');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const InteropServiceHelper = require('../../InteropServiceHelper.js');
const { substrWithEllipsis } = require('lib/string-utils');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');
const commands = [
require('./commands/focusElementSideBar'),
];
class SideBarComponent extends React.Component {
constructor() {
super();
CommandService.instance().componentRegisterCommands(this, commands);
this.onFolderDragStart_ = event => {
const folderId = event.currentTarget.getAttribute('folderid');
if (!folderId) return;
event.dataTransfer.setDragImage(new Image(), 1, 1);
event.dataTransfer.clearData();
event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId]));
};
this.onFolderDragOver_ = event => {
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault();
if (event.dataTransfer.types.indexOf('text/x-jop-folder-ids') >= 0) event.preventDefault();
};
this.onFolderDrop_ = async event => {
const folderId = event.currentTarget.getAttribute('folderid');
const dt = event.dataTransfer;
if (!dt) return;
// folderId can be NULL when dropping on the sidebar Notebook header. In that case, it's used
// to put the dropped folder at the root. But for notes, folderId needs to always be defined
// since there's no such thing as a root note.
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
event.preventDefault();
if (!folderId) return;
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folderId);
}
} else if (dt.types.indexOf('text/x-jop-folder-ids') >= 0) {
event.preventDefault();
const folderIds = JSON.parse(dt.getData('text/x-jop-folder-ids'));
for (let i = 0; i < folderIds.length; i++) {
await Folder.moveToFolder(folderIds[i], folderId);
}
}
};
this.onTagDrop_ = async event => {
const tagId = event.currentTarget.getAttribute('tagid');
const dt = event.dataTransfer;
if (!dt) return;
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
event.preventDefault();
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
for (let i = 0; i < noteIds.length; i++) {
await Tag.addNote(tagId, noteIds[i]);
}
}
};
this.onFolderToggleClick_ = async event => {
const folderId = event.currentTarget.getAttribute('folderid');
this.props.dispatch({
type: 'FOLDER_TOGGLE',
id: folderId,
});
};
this.folderItemsOrder_ = [];
this.tagItemsOrder_ = [];
this.onKeyDown = this.onKeyDown.bind(this);
this.onAllNotesClick_ = this.onAllNotesClick_.bind(this);
this.rootRef = React.createRef();
this.anchorItemRefs = {};
this.state = {
tagHeaderIsExpanded: Setting.value('tagHeaderIsExpanded'),
folderHeaderIsExpanded: Setting.value('folderHeaderIsExpanded'),
};
}
style() {
const theme = themeStyle(this.props.theme);
const itemHeight = 25;
const style = {
root: {
backgroundColor: theme.backgroundColor2,
},
listItemContainer: {
boxSizing: 'border-box',
height: itemHeight,
display: 'flex',
flexDirection: 'row',
},
listItem: {
fontFamily: theme.fontFamily,
fontSize: theme.fontSize,
textDecoration: 'none',
color: theme.color2,
cursor: 'default',
opacity: 0.8,
whiteSpace: 'nowrap',
display: 'flex',
flex: 1,
alignItems: 'center',
userSelect: 'none',
},
listItemSelected: {
backgroundColor: theme.selectedColor2,
},
listItemExpandIcon: {
color: theme.color2,
cursor: 'default',
opacity: 0.8,
fontSize: theme.fontSize,
textDecoration: 'none',
paddingRight: 5,
display: 'flex',
alignItems: 'center',
width: 12,
},
conflictFolder: {
color: theme.colorError2,
fontWeight: 'bold',
},
header: {
height: itemHeight * 1.8,
fontFamily: theme.fontFamily,
fontSize: theme.fontSize * 1.16,
textDecoration: 'none',
boxSizing: 'border-box',
color: theme.color2,
paddingLeft: 8,
display: 'flex',
alignItems: 'center',
userSelect: 'none',
},
button: {
padding: 6,
fontFamily: theme.fontFamily,
fontSize: theme.fontSize,
textDecoration: 'none',
boxSizing: 'border-box',
color: theme.color2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid rgba(255,255,255,0.2)',
marginTop: 10,
marginLeft: 5,
marginRight: 5,
cursor: 'default',
userSelect: 'none',
},
syncReport: {
fontFamily: theme.fontFamily,
fontSize: Math.round(theme.fontSize * 0.9),
color: theme.color2,
opacity: 0.5,
display: 'flex',
alignItems: 'left',
justifyContent: 'top',
flexDirection: 'column',
marginTop: 10,
marginLeft: 5,
marginRight: 5,
marginBottom: 10,
wordWrap: 'break-word',
},
noteCount: {
paddingLeft: 5,
opacity: 0.5,
userSelect: 'none',
},
};
style.tagItem = Object.assign({}, style.listItem);
style.tagItem.paddingLeft = 23;
style.tagItem.height = itemHeight;
return style;
}
clearForceUpdateDuringSync() {
if (this.forceUpdateDuringSyncIID_) {
clearInterval(this.forceUpdateDuringSyncIID_);
this.forceUpdateDuringSyncIID_ = null;
}
}
componentWillUnmount() {
this.clearForceUpdateDuringSync();
CommandService.instance().componentUnregisterCommands(commands);
}
async itemContextMenu(event) {
const itemId = event.currentTarget.getAttribute('data-id');
if (itemId === Folder.conflictFolderId()) return;
const itemType = Number(event.currentTarget.getAttribute('data-type'));
if (!itemId || !itemType) throw new Error('No data on element');
let deleteMessage = '';
let buttonLabel = _('Remove');
if (itemType === BaseModel.TYPE_FOLDER) {
const folder = await Folder.load(itemId);
deleteMessage = _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32));
buttonLabel = _('Delete');
} else if (itemType === BaseModel.TYPE_TAG) {
const tag = await Tag.load(itemId);
deleteMessage = _('Remove tag "%s" from all notes?', substrWithEllipsis(tag.title, 0, 32));
} else if (itemType === BaseModel.TYPE_SEARCH) {
deleteMessage = _('Remove this search from the sidebar?');
}
const menu = new Menu();
let item = null;
if (itemType === BaseModel.TYPE_FOLDER) {
item = BaseModel.byId(this.props.folders, itemId);
}
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append(
new MenuItem(CommandService.instance().commandToMenuItem('newNotebook', { parentId: itemId }))
);
}
menu.append(
new MenuItem({
label: buttonLabel,
click: async () => {
const ok = bridge().showConfirmMessageBox(deleteMessage, {
buttons: [buttonLabel, _('Cancel')],
defaultId: 1,
});
if (!ok) return;
if (itemType === BaseModel.TYPE_FOLDER) {
await Folder.delete(itemId);
} else if (itemType === BaseModel.TYPE_TAG) {
await Tag.untagAll(itemId);
} else if (itemType === BaseModel.TYPE_SEARCH) {
this.props.dispatch({
type: 'SEARCH_DELETE',
id: itemId,
});
}
},
})
);
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append(new MenuItem(CommandService.instance().commandToMenuItem('renameFolder', { folderId: itemId })));
menu.append(new MenuItem({ type: 'separator' }));
const InteropService = require('lib/services/InteropService.js');
const exportMenu = new Menu();
const ioService = new InteropService();
const ioModules = ioService.modules();
for (let i = 0; i < ioModules.length; i++) {
const module = ioModules[i];
if (module.type !== 'exporter') continue;
exportMenu.append(
new MenuItem({
label: module.fullLabel(),
click: async () => {
await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId] });
},
})
);
}
menu.append(
new MenuItem({
label: _('Export'),
submenu: exportMenu,
})
);
}
if (itemType === BaseModel.TYPE_TAG) {
menu.append(new MenuItem(
CommandService.instance().commandToMenuItem('renameTag', { tagId: itemId })
));
}
menu.popup(bridge().window());
}
folderItem_click(folder) {
this.props.dispatch({
type: 'FOLDER_SELECT',
id: folder ? folder.id : null,
});
}
tagItem_click(tag) {
this.props.dispatch({
type: 'TAG_SELECT',
id: tag ? tag.id : null,
});
}
// async sync_click() {
// await shared.synchronize_press(this);
// }
anchorItemRef(type, id) {
if (!this.anchorItemRefs[type]) this.anchorItemRefs[type] = {};
if (this.anchorItemRefs[type][id]) return this.anchorItemRefs[type][id];
this.anchorItemRefs[type][id] = React.createRef();
return this.anchorItemRefs[type][id];
}
firstAnchorItemRef(type) {
const refs = this.anchorItemRefs[type];
if (!refs) return null;
const n = `${type}s`;
const item = this.props[n] && this.props[n].length ? this.props[n][0] : null;
console.info('props', this.props[n], item);
if (!item) return null;
return refs[item.id];
}
noteCountElement(count) {
return <div style={this.style().noteCount}>({count})</div>;
}
folderItem(folder, selected, hasChildren, depth) {
let style = Object.assign({}, this.style().listItem);
if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder);
const itemTitle = Folder.displayTitle(folder);
let containerStyle = Object.assign({}, this.style().listItemContainer);
if (selected) containerStyle = Object.assign(containerStyle, this.style().listItemSelected);
containerStyle.paddingLeft = 8 + depth * 15;
const expandLinkStyle = Object.assign({}, this.style().listItemExpandIcon);
const expandIconStyle = {
visibility: hasChildren ? 'visible' : 'hidden',
};
const iconName = this.props.collapsedFolderIds.indexOf(folder.id) >= 0 ? 'fa-chevron-right' : 'fa-chevron-down';
const expandIcon = <i style={expandIconStyle} className={`fas ${iconName}`}></i>;
const expandLink = hasChildren ? (
<a style={expandLinkStyle} href="#" folderid={folder.id} onClick={this.onFolderToggleClick_}>
{expandIcon}
</a>
) : (
<span style={expandLinkStyle}>{expandIcon}</span>
);
const anchorRef = this.anchorItemRef('folder', folder.id);
const noteCount = folder.note_count ? this.noteCountElement(folder.note_count) : '';
return (
<div className={`list-item-container list-item-depth-${depth}`} style={containerStyle} key={folder.id} onDragStart={this.onFolderDragStart_} onDragOver={this.onFolderDragOver_} onDrop={this.onFolderDrop_} draggable={true} folderid={folder.id}>
{expandLink}
<a
ref={anchorRef}
className="list-item"
href="#"
data-id={folder.id}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={event => this.itemContextMenu(event)}
style={style}
folderid={folder.id}
onClick={() => {
this.folderItem_click(folder);
}}
onDoubleClick={this.onFolderToggleClick_}
>
{itemTitle} {noteCount}
</a>
</div>
);
}
tagItem(tag, selected) {
let style = Object.assign({}, this.style().tagItem);
if (selected) style = Object.assign(style, this.style().listItemSelected);
const anchorRef = this.anchorItemRef('tag', tag.id);
const noteCount = Setting.value('showNoteCounts') ? this.noteCountElement(tag.note_count) : '';
return (
<a
className="list-item"
href="#"
ref={anchorRef}
data-id={tag.id}
data-type={BaseModel.TYPE_TAG}
onContextMenu={event => this.itemContextMenu(event)}
tagid={tag.id}
key={tag.id}
style={style}
onDrop={this.onTagDrop_}
onClick={() => {
this.tagItem_click(tag);
}}
>
{Tag.displayTitle(tag)} {noteCount}
</a>
);
}
// searchItem(search, selected) {
// let style = Object.assign({}, this.style().listItem);
// if (selected) style = Object.assign(style, this.style().listItemSelected);
// return (
// <a
// className="list-item"
// href="#"
// data-id={search.id}
// data-type={BaseModel.TYPE_SEARCH}
// onContextMenu={event => this.itemContextMenu(event)}
// key={search.id}
// style={style}
// onClick={() => {
// this.searchItem_click(search);
// }}
// >
// {search.title}
// </a>
// );
// }
makeDivider(key) {
return <div style={{ height: 2, backgroundColor: 'blue' }} key={key} />;
}
makeHeader(key, label, iconName, extraProps = {}) {
const style = this.style().header;
const icon = <i style={{ fontSize: style.fontSize, marginRight: 5 }} className={`fas ${iconName}`} />;
if (extraProps.toggleblock || extraProps.onClick) {
style.cursor = 'pointer';
}
const headerClick = extraProps.onClick || null;
delete extraProps.onClick;
// check if toggling option is set.
let toggleIcon = null;
const toggleKey = `${key}IsExpanded`;
if (extraProps.toggleblock) {
const isExpanded = this.state[toggleKey];
toggleIcon = <i className={`fas ${isExpanded ? 'fa-chevron-down' : 'fa-chevron-right'}`} style={{ fontSize: style.fontSize * 0.75, marginRight: 12, marginLeft: 5, marginTop: style.fontSize * 0.125 }}></i>;
}
if (extraProps.selected) {
style.backgroundColor = this.style().listItemSelected.backgroundColor;
}
const ref = this.anchorItemRef('headers', key);
return (
<div
ref={ref}
style={style}
key={key}
{...extraProps}
onClick={event => {
// if a custom click event is attached, trigger that.
if (headerClick) {
headerClick(key, event);
}
this.onHeaderClick_(key, event);
}}
>
{icon}
<span style={{ flex: 1 }}>{label}</span>
{toggleIcon}
</div>
);
}
selectedItem() {
if (this.props.notesParentType === 'Folder' && this.props.selectedFolderId) {
return { type: 'folder', id: this.props.selectedFolderId };
} else if (this.props.notesParentType === 'Tag' && this.props.selectedTagId) {
return { type: 'tag', id: this.props.selectedTagId };
}
return null;
}
onKeyDown(event) {
const keyCode = event.keyCode;
const selectedItem = this.selectedItem();
if (keyCode === 40 || keyCode === 38) {
// DOWN / UP
event.preventDefault();
const focusItems = [];
for (let i = 0; i < this.folderItemsOrder_.length; i++) {
const id = this.folderItemsOrder_[i];
focusItems.push({ id: id, ref: this.anchorItemRefs['folder'][id], type: 'folder' });
}
for (let i = 0; i < this.tagItemsOrder_.length; i++) {
const id = this.tagItemsOrder_[i];
focusItems.push({ id: id, ref: this.anchorItemRefs['tag'][id], type: 'tag' });
}
let currentIndex = 0;
for (let i = 0; i < focusItems.length; i++) {
if (!selectedItem || focusItems[i].id === selectedItem.id) {
currentIndex = i;
break;
}
}
const inc = keyCode === 38 ? -1 : +1;
let newIndex = currentIndex + inc;
if (newIndex < 0) newIndex = 0;
if (newIndex > focusItems.length - 1) newIndex = focusItems.length - 1;
const focusItem = focusItems[newIndex];
const actionName = `${focusItem.type.toUpperCase()}_SELECT`;
this.props.dispatch({
type: actionName,
id: focusItem.id,
});
focusItem.ref.current.focus();
}
if (keyCode === 9) {
// TAB
event.preventDefault();
if (event.shiftKey) {
CommandService.instance().execute('focusElement', { target: 'noteBody' });
} else {
CommandService.instance().execute('focusElement', { target: 'noteList' });
}
}
if (selectedItem && selectedItem.type === 'folder' && keyCode === 32) {
// SPACE
event.preventDefault();
this.props.dispatch({
type: 'FOLDER_TOGGLE',
id: selectedItem.id,
});
}
if (keyCode === 65 && (event.ctrlKey || event.metaKey)) {
// Ctrl+A key
event.preventDefault();
}
}
onHeaderClick_(key, event) {
const currentHeader = event.currentTarget;
const toggleBlock = +currentHeader.getAttribute('toggleblock');
if (toggleBlock) {
const toggleKey = `${key}IsExpanded`;
const isExpanded = this.state[toggleKey];
this.setState({ [toggleKey]: !isExpanded });
Setting.setValue(toggleKey, !isExpanded);
}
}
onAllNotesClick_() {
this.props.dispatch({
type: 'SMART_FILTER_SELECT',
id: ALL_NOTES_FILTER_ID,
});
}
synchronizeButton(type) {
const style = Object.assign({}, this.style().button, { marginBottom: 5 });
const iconName = 'fa-sync-alt';
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
const iconStyle = { fontSize: style.fontSize, marginRight: 5 };
if (type !== 'sync') {
iconStyle.animation = 'icon-infinite-rotation 1s linear infinite';
}
const icon = <i style={iconStyle} className={`fas ${iconName}`} />;
return (
<a
className="synchronize-button"
style={style}
href="#"
key="sync_button"
onClick={() => {
CommandService.instance().execute('synchronize');
// this.sync_click();
}}
>
{icon}
{label}
</a>
);
}
render() {
const style = Object.assign({}, this.style().root, this.props.style, {
overflowX: 'hidden',
overflowY: 'hidden',
display: 'inline-flex',
flexDirection: 'column',
});
const items = [];
items.push(
this.makeHeader('allNotesHeader', _('All notes'), 'fa-clone', {
onClick: this.onAllNotesClick_,
selected: this.props.notesParentType === 'SmartFilter' && this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID,
})
);
items.push(
this.makeHeader('folderHeader', _('Notebooks'), 'fa-book', {
onDrop: this.onFolderDrop_,
folderid: '',
toggleblock: 1,
})
);
if (this.props.folders.length) {
const result = shared.renderFolders(this.props, this.folderItem.bind(this));
const folderItems = result.items;
this.folderItemsOrder_ = result.order;
items.push(
<div className="folders" key="folder_items" style={{ display: this.state.folderHeaderIsExpanded ? 'block' : 'none' }}>
{folderItems}
</div>
);
}
items.push(
this.makeHeader('tagHeader', _('Tags'), 'fa-tags', {
toggleblock: 1,
})
);
if (this.props.tags.length) {
const result = shared.renderTags(this.props, this.tagItem.bind(this));
const tagItems = result.items;
this.tagItemsOrder_ = result.order;
items.push(
<div className="tags" key="tag_items" style={{ display: this.state.tagHeaderIsExpanded ? 'block' : 'none' }}>
{tagItems}
</div>
);
}
let decryptionReportText = '';
if (this.props.decryptionWorker && this.props.decryptionWorker.state !== 'idle' && this.props.decryptionWorker.itemCount) {
decryptionReportText = _('Decrypting items: %d/%d', this.props.decryptionWorker.itemIndex + 1, this.props.decryptionWorker.itemCount);
}
let resourceFetcherText = '';
if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) {
resourceFetcherText = _('Fetching resources: %d/%d', this.props.resourceFetcher.fetchingCount, this.props.resourceFetcher.toFetchCount);
}
const lines = Synchronizer.reportToLines(this.props.syncReport);
if (resourceFetcherText) lines.push(resourceFetcherText);
if (decryptionReportText) lines.push(decryptionReportText);
const syncReportText = [];
for (let i = 0; i < lines.length; i++) {
syncReportText.push(
<div key={i} style={{ wordWrap: 'break-word', width: '100%' }}>
{lines[i]}
</div>
);
}
const syncButton = this.synchronizeButton(this.props.syncStarted ? 'cancel' : 'sync');
const syncReportComp = !syncReportText.length ? null : (
<div style={this.style().syncReport} key="sync_report">
{syncReportText}
</div>
);
return (
<div ref={this.rootRef} onKeyDown={this.onKeyDown} className="side-bar" style={style}>
<div style={{ flex: 1, overflowX: 'hidden', overflowY: 'auto' }}>{items}</div>
<div style={{ flex: 0 }}>
{syncReportComp}
{syncButton}
</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
folders: state.folders,
tags: state.tags,
searches: state.searches,
syncStarted: state.syncStarted,
syncReport: state.syncReport,
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
selectedSmartFilterId: state.selectedSmartFilterId,
notesParentType: state.notesParentType,
locale: state.settings.locale,
theme: state.settings.theme,
collapsedFolderIds: state.collapsedFolderIds,
decryptionWorker: state.decryptionWorker,
resourceFetcher: state.resourceFetcher,
sidebarVisibility: state.sidebarVisibility,
noteListVisibility: state.noteListVisibility,
};
};
const SideBar = connect(mapStateToProps)(SideBarComponent);
module.exports = { SideBar };

View File

@@ -0,0 +1,678 @@
import * as React from 'react';
import { StyledRoot, StyledAddButton, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
import { ButtonLevel } from '../Button/Button';
import CommandService from 'lib/services/CommandService';
const { connect } = require('react-redux');
const shared = require('lib/components/shared/side-menu-shared.js');
const { Synchronizer } = require('lib/synchronizer.js');
const BaseModel = require('lib/BaseModel.js');
const Setting = require('lib/models/Setting.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/theme');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const InteropServiceHelper = require('../../InteropServiceHelper.js');
const { substrWithEllipsis } = require('lib/string-utils');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');
interface Props {
themeId: number,
dispatch: Function,
folders: any[],
collapsedFolderIds: string[],
notesParentType: string,
selectedFolderId: string,
selectedTagId: string,
selectedSmartFilterId:string,
decryptionWorker: any,
resourceFetcher: any,
syncReport: any,
tags: any[],
syncStarted: boolean,
}
interface State {
tagHeaderIsExpanded: boolean,
folderHeaderIsExpanded: boolean,
}
const commands = [
require('./commands/focusElementSideBar'),
];
function ExpandIcon(props:any) {
const theme = themeStyle(props.themeId);
const style:any = { width: 16, maxWidth: 16, opacity: 0.5, fontSize: Math.round(theme.toolbarIconSize * 0.8), display: 'flex', justifyContent: 'center' };
if (!props.isVisible) style.visibility = 'hidden';
return <i className={props.isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right'} style={style}></i>;
}
function ExpandLink(props:any) {
return props.hasChildren ? (
<StyledExpandLink href="#" data-folder-id={props.folderId} onClick={props.onClick}>
<ExpandIcon themeId={props.themeId} isVisible={true} isExpanded={props.isExpanded}/>
</StyledExpandLink>
) : (
<StyledExpandLink><ExpandIcon themeId={props.themeId} isVisible={false} isExpanded={false}/></StyledExpandLink>
);
}
function FolderItem(props:any) {
const { hasChildren, isExpanded, depth, selected, folderId, folderTitle, anchorRef, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_ } = props;
const noteCountComp = noteCount ? <StyledNoteCount>{noteCount}</StyledNoteCount> : null;
return (
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth}`} onDragStart={onFolderDragStart_} onDragOver={onFolderDragOver_} onDrop={onFolderDrop_} draggable={true} data-folder-id={folderId}>
<ExpandLink themeId={props.themeId} hasChildren={hasChildren} folderId={folderId} onClick={onFolderToggleClick_} isExpanded={isExpanded}/>
<StyledListItemAnchor
ref={anchorRef}
className="list-item"
isConflictFolder={folderId === Folder.conflictFolderId()}
href="#"
selected={selected}
data-id={folderId}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={itemContextMenu}
data-folder-id={folderId}
onClick={() => {
folderItem_click(folderId);
}}
onDoubleClick={onFolderToggleClick_}
>
{folderTitle} {noteCountComp}
</StyledListItemAnchor>
</StyledListItem>
);
}
class SideBarComponent extends React.Component<Props, State> {
private folderItemsOrder_:any[] = [];
private tagItemsOrder_:any[] = [];
private rootRef:any = null;
private anchorItemRefs:any = {};
private forceUpdateDuringSyncIID_:any = null;
constructor(props:any) {
super(props);
CommandService.instance().componentRegisterCommands(this, commands);
this.state = {
tagHeaderIsExpanded: Setting.value('tagHeaderIsExpanded'),
folderHeaderIsExpanded: Setting.value('folderHeaderIsExpanded'),
};
this.onFolderToggleClick_ = this.onFolderToggleClick_.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onAllNotesClick_ = this.onAllNotesClick_.bind(this);
this.header_contextMenu = this.header_contextMenu.bind(this);
this.onAddFolderButtonClick = this.onAddFolderButtonClick.bind(this);
this.folderItem_click = this.folderItem_click.bind(this);
this.itemContextMenu = this.itemContextMenu.bind(this);
}
onFolderDragStart_(event:any) {
const folderId = event.currentTarget.getAttribute('data-folder-id');
if (!folderId) return;
event.dataTransfer.setDragImage(new Image(), 1, 1);
event.dataTransfer.clearData();
event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId]));
}
onFolderDragOver_(event:any) {
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault();
if (event.dataTransfer.types.indexOf('text/x-jop-folder-ids') >= 0) event.preventDefault();
}
async onFolderDrop_(event:any) {
const folderId = event.currentTarget.getAttribute('data-folder-id');
const dt = event.dataTransfer;
if (!dt) return;
// folderId can be NULL when dropping on the sidebar Notebook header. In that case, it's used
// to put the dropped folder at the root. But for notes, folderId needs to always be defined
// since there's no such thing as a root note.
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
event.preventDefault();
if (!folderId) return;
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folderId);
}
} else if (dt.types.indexOf('text/x-jop-folder-ids') >= 0) {
event.preventDefault();
const folderIds = JSON.parse(dt.getData('text/x-jop-folder-ids'));
for (let i = 0; i < folderIds.length; i++) {
await Folder.moveToFolder(folderIds[i], folderId);
}
}
}
async onTagDrop_(event:any) {
const tagId = event.currentTarget.getAttribute('data-tag-id');
const dt = event.dataTransfer;
if (!dt) return;
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
event.preventDefault();
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
for (let i = 0; i < noteIds.length; i++) {
await Tag.addNote(tagId, noteIds[i]);
}
}
}
async onFolderToggleClick_(event:any) {
const folderId = event.currentTarget.getAttribute('data-folder-id');
this.props.dispatch({
type: 'FOLDER_TOGGLE',
id: folderId,
});
}
clearForceUpdateDuringSync() {
if (this.forceUpdateDuringSyncIID_) {
clearInterval(this.forceUpdateDuringSyncIID_);
this.forceUpdateDuringSyncIID_ = null;
}
}
componentWillUnmount() {
this.clearForceUpdateDuringSync();
CommandService.instance().componentUnregisterCommands(commands);
}
async header_contextMenu() {
const menu = new Menu();
menu.append(
new MenuItem(CommandService.instance().commandToMenuItem('newFolder'))
);
menu.popup(bridge().window());
}
async itemContextMenu(event:any) {
const itemId = event.currentTarget.getAttribute('data-id');
if (itemId === Folder.conflictFolderId()) return;
const itemType = Number(event.currentTarget.getAttribute('data-type'));
if (!itemId || !itemType) throw new Error('No data on element');
let deleteMessage = '';
let buttonLabel = _('Remove');
if (itemType === BaseModel.TYPE_FOLDER) {
const folder = await Folder.load(itemId);
deleteMessage = _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32));
buttonLabel = _('Delete');
} else if (itemType === BaseModel.TYPE_TAG) {
const tag = await Tag.load(itemId);
deleteMessage = _('Remove tag "%s" from all notes?', substrWithEllipsis(tag.title, 0, 32));
} else if (itemType === BaseModel.TYPE_SEARCH) {
deleteMessage = _('Remove this search from the sidebar?');
}
const menu = new Menu();
let item = null;
if (itemType === BaseModel.TYPE_FOLDER) {
item = BaseModel.byId(this.props.folders, itemId);
}
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append(
new MenuItem(CommandService.instance().commandToMenuItem('newFolder', { parentId: itemId }))
);
}
menu.append(
new MenuItem({
label: buttonLabel,
click: async () => {
const ok = bridge().showConfirmMessageBox(deleteMessage, {
buttons: [buttonLabel, _('Cancel')],
defaultId: 1,
});
if (!ok) return;
if (itemType === BaseModel.TYPE_FOLDER) {
await Folder.delete(itemId);
} else if (itemType === BaseModel.TYPE_TAG) {
await Tag.untagAll(itemId);
} else if (itemType === BaseModel.TYPE_SEARCH) {
this.props.dispatch({
type: 'SEARCH_DELETE',
id: itemId,
});
}
},
})
);
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append(new MenuItem(CommandService.instance().commandToMenuItem('renameFolder', { folderId: itemId })));
menu.append(new MenuItem({ type: 'separator' }));
const InteropService = require('lib/services/InteropService.js');
const exportMenu = new Menu();
const ioService = new InteropService();
const ioModules = ioService.modules();
for (let i = 0; i < ioModules.length; i++) {
const module = ioModules[i];
if (module.type !== 'exporter') continue;
exportMenu.append(
new MenuItem({
label: module.fullLabel(),
click: async () => {
await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId] });
},
})
);
}
menu.append(
new MenuItem({
label: _('Export'),
submenu: exportMenu,
})
);
}
if (itemType === BaseModel.TYPE_TAG) {
menu.append(new MenuItem(
CommandService.instance().commandToMenuItem('renameTag', { tagId: itemId })
));
}
menu.popup(bridge().window());
}
folderItem_click(folderId:string) {
this.props.dispatch({
type: 'FOLDER_SELECT',
id: folderId ? folderId : null,
});
}
tagItem_click(tag:any) {
this.props.dispatch({
type: 'TAG_SELECT',
id: tag ? tag.id : null,
});
}
anchorItemRef(type:string, id:string) {
if (!this.anchorItemRefs[type]) this.anchorItemRefs[type] = {};
if (this.anchorItemRefs[type][id]) return this.anchorItemRefs[type][id];
this.anchorItemRefs[type][id] = React.createRef();
return this.anchorItemRefs[type][id];
}
firstAnchorItemRef(type:string) {
const refs = this.anchorItemRefs[type];
if (!refs) return null;
const n = `${type}s`;
const p = this.props as any;
const item = p[n] && p[n].length ? p[n][0] : null;
if (!item) return null;
return refs[item.id];
}
renderNoteCount(count:number) {
return count ? <StyledNoteCount>{count}</StyledNoteCount> : null;
}
renderExpandIcon(isExpanded:boolean, isVisible:boolean = true) {
const theme = themeStyle(this.props.themeId);
const style:any = { width: 16, maxWidth: 16, opacity: 0.5, fontSize: Math.round(theme.toolbarIconSize * 0.8), display: 'flex', justifyContent: 'center' };
if (!isVisible) style.visibility = 'hidden';
return <i className={isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right'} style={style}></i>;
}
renderAllNotesItem(selected:boolean) {
return (
<StyledListItem key="allNotesHeader" selected={selected} className={'list-item-container list-item-depth-0'} isSpecialItem={true}>
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink>
<StyledAllNotesIcon className="icon-notes"/>
<StyledListItemAnchor
className="list-item"
isSpecialItem={true}
href="#"
selected={selected}
onClick={this.onAllNotesClick_}
>
{_('All notes')}
</StyledListItemAnchor>
</StyledListItem>
);
}
renderFolderItem(folder:any, selected:boolean, hasChildren:boolean, depth:number) {
const anchorRef = this.anchorItemRef('folder', folder.id);
return <FolderItem
key={folder.id}
folderId={folder.id}
folderTitle={Folder.displayTitle(folder)}
themeId={this.props.themeId}
depth={depth}
selected={selected}
isExpanded={this.props.collapsedFolderIds.indexOf(folder.id) < 0}
hasChildren={hasChildren}
anchorRef={anchorRef}
noteCount={folder.note_count}
onFolderDragStart_={this.onFolderDragStart_}
onFolderDragOver_={this.onFolderDragOver_}
onFolderDrop_={this.onFolderDrop_}
itemContextMenu={this.itemContextMenu}
folderItem_click={this.folderItem_click}
onFolderToggleClick_={this.onFolderToggleClick_}
/>;
}
renderTag(tag:any, selected:boolean) {
const anchorRef = this.anchorItemRef('tag', tag.id);
const noteCount = Setting.value('showNoteCounts') ? this.renderNoteCount(tag.note_count) : '';
return (
<StyledListItem selected={selected} className={'list-item-container'} key={tag.id} onDrop={this.onTagDrop_} data-tag-id={tag.id}>
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink>
<StyledListItemAnchor
ref={anchorRef}
className="list-item"
href="#"
selected={selected}
data-id={tag.id}
data-type={BaseModel.TYPE_TAG}
onContextMenu={this.itemContextMenu}
onClick={() => {
this.tagItem_click(tag);
}}
>
{Tag.displayTitle(tag)} {noteCount}
</StyledListItemAnchor>
</StyledListItem>
);
}
makeDivider(key:string) {
return <div style={{ height: 2, backgroundColor: 'blue' }} key={key} />;
}
renderHeader(key:string, label:string, iconName:string, contextMenuHandler:Function = null, onPlusButtonClick:Function = null, extraProps:any = {}) {
const headerClick = extraProps.onClick || null;
delete extraProps.onClick;
const ref = this.anchorItemRef('headers', key);
return (
<div key={key} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<StyledHeader
ref={ref}
{...extraProps}
onContextMenu={contextMenuHandler}
onClick={(event:any) => {
// if a custom click event is attached, trigger that.
if (headerClick) {
headerClick(key, event);
}
this.onHeaderClick_(key);
}}
>
<StyledHeaderIcon className={iconName}/>
<StyledHeaderLabel>{label}</StyledHeaderLabel>
</StyledHeader>
{ onPlusButtonClick && <StyledAddButton onClick={onPlusButtonClick} iconName="fas fa-plus" level={ButtonLevel.SideBarSecondary}/> }
</div>
);
}
selectedItem() {
if (this.props.notesParentType === 'Folder' && this.props.selectedFolderId) {
return { type: 'folder', id: this.props.selectedFolderId };
} else if (this.props.notesParentType === 'Tag' && this.props.selectedTagId) {
return { type: 'tag', id: this.props.selectedTagId };
}
return null;
}
onKeyDown(event:any) {
const keyCode = event.keyCode;
const selectedItem = this.selectedItem();
if (keyCode === 40 || keyCode === 38) {
// DOWN / UP
event.preventDefault();
const focusItems = [];
for (let i = 0; i < this.folderItemsOrder_.length; i++) {
const id = this.folderItemsOrder_[i];
focusItems.push({ id: id, ref: this.anchorItemRefs['folder'][id], type: 'folder' });
}
for (let i = 0; i < this.tagItemsOrder_.length; i++) {
const id = this.tagItemsOrder_[i];
focusItems.push({ id: id, ref: this.anchorItemRefs['tag'][id], type: 'tag' });
}
let currentIndex = 0;
for (let i = 0; i < focusItems.length; i++) {
if (!selectedItem || focusItems[i].id === selectedItem.id) {
currentIndex = i;
break;
}
}
const inc = keyCode === 38 ? -1 : +1;
let newIndex = currentIndex + inc;
if (newIndex < 0) newIndex = 0;
if (newIndex > focusItems.length - 1) newIndex = focusItems.length - 1;
const focusItem = focusItems[newIndex];
const actionName = `${focusItem.type.toUpperCase()}_SELECT`;
this.props.dispatch({
type: actionName,
id: focusItem.id,
});
focusItem.ref.current.focus();
}
if (keyCode === 9) {
// TAB
event.preventDefault();
if (event.shiftKey) {
CommandService.instance().execute('focusElement', { target: 'noteBody' });
} else {
CommandService.instance().execute('focusElement', { target: 'noteList' });
}
}
if (selectedItem && selectedItem.type === 'folder' && keyCode === 32) {
// SPACE
event.preventDefault();
this.props.dispatch({
type: 'FOLDER_TOGGLE',
id: selectedItem.id,
});
}
if (keyCode === 65 && (event.ctrlKey || event.metaKey)) {
// Ctrl+A key
event.preventDefault();
}
}
onHeaderClick_(key:string) {
const toggleKey = `${key}IsExpanded`;
const isExpanded = (this.state as any)[toggleKey];
const newState:any = { [toggleKey]: !isExpanded };
this.setState(newState);
Setting.setValue(toggleKey, !isExpanded);
}
onAllNotesClick_() {
this.props.dispatch({
type: 'SMART_FILTER_SELECT',
id: ALL_NOTES_FILTER_ID,
});
}
renderSynchronizeButton(type:string) {
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : '';
return (
<StyledSynchronizeButton
level={ButtonLevel.SideBarSecondary}
iconName="icon-sync"
key="sync_button"
iconAnimation={iconAnimation}
title={label}
onClick={() => {
CommandService.instance().execute('synchronize', { syncStarted: type !== 'sync' });
}}
/>
);
}
onAddFolderButtonClick() {
CommandService.instance().execute('newFolder');
}
render() {
const theme = themeStyle(this.props.themeId);
const items = [];
items.push(
this.renderHeader('folderHeader', _('Notebooks'), 'icon-notebooks', this.header_contextMenu, this.onAddFolderButtonClick, {
onDrop: this.onFolderDrop_,
['data-folder-id']: '',
toggleblock: 1,
})
);
if (this.props.folders.length) {
const allNotesSelected = this.props.notesParentType === 'SmartFilter' && this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID;
const result = shared.renderFolders(this.props, this.renderFolderItem.bind(this));
const folderItems = [this.renderAllNotesItem(allNotesSelected)].concat(result.items);
this.folderItemsOrder_ = result.order;
items.push(
<div className="folders" key="folder_items" style={{ display: this.state.folderHeaderIsExpanded ? 'block' : 'none', paddingBottom: 10 }}>
{folderItems}
</div>
);
}
items.push(
this.renderHeader('tagHeader', _('Tags'), 'icon-tags', null, null, {
toggleblock: 1,
})
);
if (this.props.tags.length) {
const result = shared.renderTags(this.props, this.renderTag.bind(this));
const tagItems = result.items;
this.tagItemsOrder_ = result.order;
items.push(
<div className="tags" key="tag_items" style={{ display: this.state.tagHeaderIsExpanded ? 'block' : 'none' }}>
{tagItems}
</div>
);
}
let decryptionReportText = '';
if (this.props.decryptionWorker && this.props.decryptionWorker.state !== 'idle' && this.props.decryptionWorker.itemCount) {
decryptionReportText = _('Decrypting items: %d/%d', this.props.decryptionWorker.itemIndex + 1, this.props.decryptionWorker.itemCount);
}
let resourceFetcherText = '';
if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) {
resourceFetcherText = _('Fetching resources: %d/%d', this.props.resourceFetcher.fetchingCount, this.props.resourceFetcher.toFetchCount);
}
const lines = Synchronizer.reportToLines(this.props.syncReport);
if (resourceFetcherText) lines.push(resourceFetcherText);
if (decryptionReportText) lines.push(decryptionReportText);
const syncReportText = [];
for (let i = 0; i < lines.length; i++) {
syncReportText.push(
<StyledSyncReportText key={i}>
{lines[i]}
</StyledSyncReportText>
);
}
const syncButton = this.renderSynchronizeButton(this.props.syncStarted ? 'cancel' : 'sync');
const syncReportComp = !syncReportText.length ? null : (
<StyledSyncReport key="sync_report">
{syncReportText}
</StyledSyncReport>
);
return (
<StyledRoot ref={this.rootRef} onKeyDown={this.onKeyDown} className="side-bar">
<div style={{ flex: 1, overflowX: 'hidden', overflowY: 'auto' }}>{items}</div>
<div style={{ flex: 0, padding: theme.mainPadding }}>
{syncReportComp}
{syncButton}
</div>
</StyledRoot>
);
}
}
const mapStateToProps = (state:any) => {
return {
folders: state.folders,
tags: state.tags,
searches: state.searches,
syncStarted: state.syncStarted,
syncReport: state.syncReport,
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
selectedSmartFilterId: state.selectedSmartFilterId,
notesParentType: state.notesParentType,
locale: state.settings.locale,
themeId: state.settings.theme,
collapsedFolderIds: state.collapsedFolderIds,
decryptionWorker: state.decryptionWorker,
resourceFetcher: state.resourceFetcher,
sidebarVisibility: state.sidebarVisibility,
noteListVisibility: state.noteListVisibility,
};
};
const SideBar = connect(mapStateToProps)(SideBarComponent);
module.exports = { SideBar };

View File

@@ -0,0 +1,131 @@
import Button from '../../Button/Button';
const styled = require('styled-components').default;
export const StyledRoot = styled.div`
background-color: ${(props:any) => props.theme.backgroundColor2};
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: hidden;
display: inline-flex;
flex-direction: column;
`;
export const StyledHeader = styled.div`
//height: ${(props:any) => props.theme.topRowHeight}px;
//text-decoration: none;
flex: 1;
box-sizing: border-box;
padding: ${(props:any) => props.theme.mainPadding}px;
padding-bottom: ${(props:any) => props.theme.mainPadding / 2}px;
display: flex;
align-items: center;
user-select: none;
text-transform: uppercase;
//cursor: pointer;
`;
export const StyledHeaderIcon = styled.i`
font-size: ${(props:any) => props.theme.toolbarIconSize}px;
color: ${(props:any) => props.theme.color2};
margin-right: 8px;
`;
export const StyledAllNotesIcon = styled(StyledHeaderIcon)`
font-size: ${(props:any) => props.theme.toolbarIconSize * 0.8}px;
color: ${(props:any) => props.theme.colorFaded2};
margin-right: 8px;
`;
export const StyledHeaderLabel = styled.span`
flex: 1;
color: ${(props:any) => props.theme.color2};
font-size: ${(props:any) => Math.round(props.theme.fontSize * 1.1)}px;
font-weight: bold;
`;
export const StyledListItem = styled.div`
box-sizing: border-box;
height: 25px;
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${(props:any) => props.theme.mainPadding + ('depth' in props ? props.depth : 0) * 16}px;
background: ${(props:any) => props.selected ? props.theme.selectedColor2 : 'none'};
/*text-transform: ${(props:any) => props.isSpecialItem ? 'uppercase' : 'none'};*/
transition: 0.1s;
&:hover {
background-color: ${(props:any) => props.theme.backgroundColorHover2};
}
`;
function listItemTextColor(props:any) {
if (props.isConflictFolder) return props.theme.colorError2;
if (props.isSpecialItem) return props.theme.colorFaded2;
return props.theme.color2;
}
export const StyledListItemAnchor = styled.a`
font-size: ${(props:any) => Math.round(props.theme.fontSize * 1.0833333)}px;
font-weight: 500;
text-decoration: none;
color: ${(props:any) => listItemTextColor(props)};
cursor: default;
opacity: ${(props:any) => props.selected ? 1 : 0.8};
white-space: nowrap;
display: flex;
flex: 1;
align-items: center;
user-select: none;
height: 100%;
`;
export const StyledExpandLink = styled.a`
color: ${(props:any) => props.theme.color2};
cursor: default;
opacity: 0.8;
text-decoration: none;
padding-right: 8px;
display: flex;
align-items: center;
width: 16px;
max-width: 16px;
min-width: 16px;
height: 100%;
`;
export const StyledNoteCount = styled.div`
color: ${(props:any) => props.theme.color2};
padding-left: 8px;
opacity: 0.5;
user-select: none;
`;
export const StyledSynchronizeButton = styled(Button)`
width: 100%;
`;
export const StyledAddButton = styled(Button)`
border: none;
padding-right: 15px;
padding-top: 4px;
`;
export const StyledSyncReport = styled.div`
font-size: ${(props:any) => Math.round(props.theme.fontSize * 0.9)}px;
color: ${(props:any) => props.theme.color2};
opacity: 0.5;
display: flex;
flex-direction: column;
margin-left: 5px;
margin-right: 5px;
margin-bottom: 10px;
word-wrap: break-word;
`;
export const StyledSyncReportText = styled.div`
color: ${(props:any) => props.theme.color2};
word-wrap: break-word;
width: 100%;
`;

View File

@@ -1,159 +0,0 @@
const React = require('react');
const { connect } = require('react-redux');
const Setting = require('lib/models/Setting.js');
const { bridge } = require('electron').remote.require('./bridge');
const { Header } = require('./Header/Header.min.js');
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');
const fs = require('fs-extra');
class StatusScreenComponent extends React.Component {
constructor() {
super();
this.state = {
report: [],
};
}
UNSAFE_componentWillMount() {
this.resfreshScreen();
}
async resfreshScreen() {
const service = new ReportService();
const report = await service.status(Setting.value('sync.target'));
this.setState({ report: report });
}
async exportDebugReportClick() {
const filename = `syncReport-${new Date().getTime()}.csv`;
const filePath = bridge().showSaveDialog({
title: _('Please select where the sync status should be exported to'),
defaultPath: filename,
});
if (!filePath) return;
const service = new ReportService();
const csv = await service.basicItemList({ format: 'csv' });
await fs.writeFileSync(filePath, csv);
}
render() {
const theme = themeStyle(this.props.theme);
const style = this.props.style;
const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width });
const retryStyle = Object.assign({}, theme.urlStyle, { marginLeft: 5 });
const retryAllStyle = Object.assign({}, theme.urlStyle, { marginTop: 5, display: 'inline-block' });
const containerPadding = 10;
const containerStyle = Object.assign({}, theme.containerStyle, {
padding: containerPadding,
height: style.height - theme.headerHeight - containerPadding * 2,
});
function renderSectionTitleHtml(key, title) {
return (
<h2 key={`section_${key}`} style={theme.h2Style}>
{title}
</h2>
);
}
function renderSectionRetryAllHtml(key, retryAllHandler) {
return (
<a key={`retry_all_${key}`} href="#" onClick={retryAllHandler} style={retryAllStyle}>
{_('Retry All')}
</a>
);
}
const renderSectionHtml = (key, section) => {
const itemsHtml = [];
itemsHtml.push(renderSectionTitleHtml(section.title, section.title));
for (const n in section.body) {
if (!section.body.hasOwnProperty(n)) continue;
const item = section.body[n];
let text = '';
let retryLink = null;
if (typeof item === 'object') {
if (item.canRetry) {
const onClick = async () => {
await item.retryHandler();
this.resfreshScreen();
};
retryLink = (
<a href="#" onClick={onClick} style={retryStyle}>
{_('Retry')}
</a>
);
}
text = item.text;
} else {
text = item;
}
if (!text) text = '\xa0';
itemsHtml.push(
<div style={theme.textStyle} key={`item_${n}`}>
<span>{text}</span>
{retryLink}
</div>
);
}
if (section.canRetryAll) {
itemsHtml.push(renderSectionRetryAllHtml(section.title, section.retryAllHandler));
}
return <div key={key}>{itemsHtml}</div>;
};
function renderBodyHtml(report) {
const sectionsHtml = [];
for (let i = 0; i < report.length; i++) {
const section = report[i];
if (!section.body.length) continue;
sectionsHtml.push(renderSectionHtml(i, section));
}
return <div>{sectionsHtml}</div>;
}
const body = renderBodyHtml(this.state.report);
return (
<div style={style}>
<Header style={headerStyle} />
<div style={containerStyle}>
<a style={theme.textStyle} onClick={() => this.exportDebugReportClick()} href="#">
Export debug report
</a>
{body}
</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
theme: state.settings.theme,
settings: state.settings,
locale: state.settings.locale,
};
};
const StatusScreen = connect(mapStateToProps)(StatusScreenComponent);
module.exports = { StatusScreen };

View File

@@ -0,0 +1,163 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import ButtonBar from '../ConfigScreen/ButtonBar';
const { connect } = require('react-redux');
const Setting = require('lib/models/Setting.js');
const { bridge } = require('electron').remote.require('./bridge');
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');
const fs = require('fs-extra');
interface Props {
themeId: string,
style: any,
dispatch: Function,
}
async function exportDebugReportClick() {
const filename = `syncReport-${new Date().getTime()}.csv`;
const filePath = bridge().showSaveDialog({
title: _('Please select where the sync status should be exported to'),
defaultPath: filename,
});
if (!filePath) return;
const service = new ReportService();
const csv = await service.basicItemList({ format: 'csv' });
await fs.writeFileSync(filePath, csv);
}
function StatusScreen(props:Props) {
const [report, setReport] = useState<any[]>([]);
async function resfreshScreen() {
const service = new ReportService();
const r = await service.status(Setting.value('sync.target'));
setReport(r);
}
useEffect(() => {
resfreshScreen();
}, []);
const theme = themeStyle(props.themeId);
const style = { ...props.style,
display: 'flex',
flexDirection: 'column',
};
const retryStyle = Object.assign({}, theme.urlStyle, { marginLeft: 5 });
const retryAllStyle = Object.assign({}, theme.urlStyle, { marginTop: 5, display: 'inline-block' });
const containerPadding = theme.configScreenPadding;
const containerStyle = Object.assign({}, theme.containerStyle, {
padding: containerPadding,
flex: 1,
});
function renderSectionTitleHtml(key:string, title:string) {
return (
<h2 key={`section_${key}`} style={theme.h2Style}>
{title}
</h2>
);
}
function renderSectionRetryAllHtml(key:string, retryAllHandler:any) {
return (
<a key={`retry_all_${key}`} href="#" onClick={retryAllHandler} style={retryAllStyle}>
{_('Retry All')}
</a>
);
}
const renderSectionHtml = (key:string, section:any) => {
const itemsHtml = [];
itemsHtml.push(renderSectionTitleHtml(section.title, section.title));
for (const n in section.body) {
if (!section.body.hasOwnProperty(n)) continue;
const item = section.body[n];
let text = '';
let retryLink = null;
if (typeof item === 'object') {
if (item.canRetry) {
const onClick = async () => {
await item.retryHandler();
resfreshScreen();
};
retryLink = (
<a href="#" onClick={onClick} style={retryStyle}>
{_('Retry')}
</a>
);
}
text = item.text;
} else {
text = item;
}
if (!text) text = '\xa0';
itemsHtml.push(
<div style={theme.textStyle} key={`item_${n}`}>
<span>{text}</span>
{retryLink}
</div>
);
}
if (section.canRetryAll) {
itemsHtml.push(renderSectionRetryAllHtml(section.title, section.retryAllHandler));
}
return <div key={key}>{itemsHtml}</div>;
};
function renderBodyHtml(report:any) {
const sectionsHtml = [];
for (let i = 0; i < report.length; i++) {
const section = report[i];
if (!section.body.length) continue;
sectionsHtml.push(renderSectionHtml(`${i}`, section));
}
return <div>{sectionsHtml}</div>;
}
const body = renderBodyHtml(report);
return (
<div style={style}>
<div style={containerStyle}>
<a style={theme.textStyle} onClick={() => exportDebugReportClick()} href="#">
Export debug report
</a>
{body}
</div>
<ButtonBar
onCancelClick={() => props.dispatch({ type: 'NAV_BACK' })}
/>
</div>
);
}
const mapStateToProps = (state:any) => {
return {
themeId: state.settings.theme,
settings: state.settings,
locale: state.settings.locale,
};
};
export default connect(mapStateToProps)(StatusScreen);

View File

@@ -4,7 +4,7 @@ const { themeStyle } = require('lib/theme');
class TagItemComponent extends React.Component { class TagItemComponent extends React.Component {
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = Object.assign({}, theme.tagStyle); const style = Object.assign({}, theme.tagStyle);
const title = this.props.title; const title = this.props.title;
@@ -13,7 +13,7 @@ class TagItemComponent extends React.Component {
} }
const mapStateToProps = state => { const mapStateToProps = state => {
return { theme: state.settings.theme }; return { themeId: state.settings.theme };
}; };
const TagItem = connect(mapStateToProps)(TagItemComponent); const TagItem = connect(mapStateToProps)(TagItemComponent);

View File

@@ -6,7 +6,7 @@ const TagItem = require('./TagItem.min.js');
class TagListComponent extends React.Component { class TagListComponent extends React.Component {
render() { render() {
const style = Object.assign({}, this.props.style); const style = Object.assign({}, this.props.style);
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const tags = this.props.items; const tags = this.props.items;
style.display = 'flex'; style.display = 'flex';
@@ -15,11 +15,13 @@ class TagListComponent extends React.Component {
style.boxSizing = 'border-box'; style.boxSizing = 'border-box';
style.fontSize = theme.fontSize; style.fontSize = theme.fontSize;
style.whiteSpace = 'nowrap'; style.whiteSpace = 'nowrap';
style.height = 25; // style.height = 40;
style.paddingTop = 8;
style.paddingBottom = 8;
const tagItems = []; const tagItems = [];
if (tags && tags.length > 0) { if (tags && tags.length > 0) {
// Sort by id for now, but probably needs to be changed in the future.
tags.sort((a, b) => { tags.sort((a, b) => {
return a.title < b.title ? -1 : +1; return a.title < b.title ? -1 : +1;
}); });
@@ -42,7 +44,7 @@ class TagListComponent extends React.Component {
} }
const mapStateToProps = state => { const mapStateToProps = state => {
return { theme: state.settings.theme }; return { themeId: state.settings.theme };
}; };
const TagList = connect(mapStateToProps)(TagListComponent); const TagList = connect(mapStateToProps)(TagListComponent);

View File

@@ -0,0 +1,29 @@
import * as React from 'react';
import styles_ from './styles';
import { ToolbarButtonInfo } from 'lib/services/CommandService';
export enum Value {
Markdown = 'markdown',
RichText = 'richText',
}
export interface Props {
themeId: number,
value: Value,
toolbarButtonInfo: ToolbarButtonInfo,
}
export default function ToggleEditorsButton(props:Props) {
const style = styles_(props);
return (
<button style={style.button} disabled={!props.toolbarButtonInfo.enabled} aria-label={props.toolbarButtonInfo.title} title={props.toolbarButtonInfo.title} type="button" className="tox-tbtn" aria-pressed="false" onClick={props.toolbarButtonInfo.onClick}>
<div style={style.leftInnerButton}>
<i style={style.leftIcon} className="fab fa-markdown"></i>
</div>
<div style={style.rightInnerButton}>
<i style={style.rightIcon} className="fas fa-edit"></i>
</div>
</button>
);
}

View File

@@ -0,0 +1,68 @@
import { Props, Value } from '../ToggleEditorsButton';
const { buildStyle } = require('lib/theme');
export default function styles(props:Props) {
return buildStyle(['ToggleEditorsButton', props.value], props.themeId, (theme: any) => {
const iconSize = 15;
const mdIconWidth = iconSize * 1.25;
const buttonHeight = theme.toolbarHeight - 8;
const mdIconPadding = Math.round((buttonHeight - iconSize) / 2) + 3;
const innerButton:any = {
borderStyle: 'solid',
borderColor: theme.color3,
borderWidth: 1,
borderRadius: 0,
width: mdIconWidth + mdIconPadding * 2,
height: buttonHeight,
display: 'flex',
justifyContent: 'center',
};
const output:any = {
button: {
border: 'none',
padding: 0,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
background: 'none',
},
leftInnerButton: {
...innerButton,
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
rightInnerButton: {
...innerButton,
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
leftIcon: {
fontSize: iconSize,
position: 'relative',
top: 1,
color: theme.color3,
},
rightIcon: {
fontSize: iconSize - 1,
borderLeft: 'none',
position: 'relative',
top: 1,
color: theme.color3,
},
};
if (props.value === Value.Markdown) {
output.leftInnerButton.backgroundColor = theme.color3;
output.leftIcon.color = theme.backgroundColor3;
output.rightInnerButton.opacity = 0.5;
} else if (props.value === Value.RichText) {
output.rightInnerButton.backgroundColor = theme.color3;
output.rightIcon.color = theme.backgroundColor3;
output.leftInnerButton.opacity = 0.5;
}
return output;
});
}

View File

@@ -1,62 +0,0 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('lib/theme');
const ToolbarButton = require('./ToolbarButton.min.js');
const ToolbarSpace = require('./ToolbarSpace.min.js');
class ToolbarComponent extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
const style = Object.assign({
// height: theme.toolbarHeight,
display: 'flex',
flexDirection: 'row',
borderBottom: `1px solid ${theme.dividerColor}`,
boxSizing: 'border-box',
}, this.props.style);
const itemComps = [];
if (this.props.items) {
for (let i = 0; i < this.props.items.length; i++) {
const o = this.props.items[i];
let key = o.iconName ? o.iconName : '';
key += o.title ? o.title : '';
const itemType = !('type' in o) ? 'button' : o.type;
if (!key) key = `${o.type}_${i}`;
const props = Object.assign(
{
key: key,
theme: this.props.theme,
},
o
);
if (this.props.disabled) props.disabled = true;
if (itemType === 'button') {
itemComps.push(<ToolbarButton {...props} />);
} else if (itemType === 'separator') {
itemComps.push(<ToolbarSpace {...props} />);
}
}
}
return (
<div className="editor-toolbar" style={style}>
{itemComps}
</div>
);
}
}
const mapStateToProps = state => {
return { theme: state.settings.theme };
};
const Toolbar = connect(mapStateToProps)(ToolbarComponent);
module.exports = Toolbar;

View File

@@ -0,0 +1,91 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('lib/theme');
const ToolbarButton = require('./ToolbarButton/ToolbarButton.js').default;
const ToolbarSpace = require('./ToolbarSpace.min.js');
const ToggleEditorsButton = require('./ToggleEditorsButton/ToggleEditorsButton.js').default;
interface Props {
themeId: number,
style: any,
items: any[],
}
class ToolbarBaseComponent extends React.Component<Props, any> {
render() {
const theme = themeStyle(this.props.themeId);
const style:any = Object.assign({
display: 'flex',
flexDirection: 'row',
boxSizing: 'border-box',
backgroundColor: theme.backgroundColor3,
padding: theme.toolbarPadding,
paddingRight: theme.mainPadding,
}, this.props.style);
const groupStyle:any = {
display: 'flex',
flexDirection: 'row',
boxSizing: 'border-box',
};
const leftItemComps:any[] = [];
const centerItemComps:any[] = [];
const rightItemComps:any[] = [];
if (this.props.items) {
for (let i = 0; i < this.props.items.length; i++) {
const o = this.props.items[i];
let key = o.iconName ? o.iconName : '';
key += o.title ? o.title : '';
const itemType = !('type' in o) ? 'button' : o.type;
if (!key) key = `${o.type}_${i}`;
const props = Object.assign(
{
key: key,
themeId: this.props.themeId,
},
o
);
if (o.name === 'toggleEditors') {
rightItemComps.push(<ToggleEditorsButton
key={o.name}
value={'markdown'}
themeId={this.props.themeId}
toolbarButtonInfo={o}
/>);
} else if (itemType === 'button') {
const target = ['historyForward', 'historyBackward', 'startExternalEditing'].includes(o.name) ? leftItemComps : centerItemComps;
target.push(<ToolbarButton {...props} />);
} else if (itemType === 'separator') {
centerItemComps.push(<ToolbarSpace {...props} />);
}
}
}
return (
<div className="editor-toolbar" style={style}>
<div style={groupStyle}>
{leftItemComps}
</div>
<div style={groupStyle}>
{centerItemComps}
</div>
<div style={Object.assign({}, groupStyle, { flex: 1, justifyContent: 'flex-end' })}>
{rightItemComps}
</div>
</div>
);
}
}
const mapStateToProps = (state:any) => {
return { themeId: state.settings.theme };
};
export default connect(mapStateToProps)(ToolbarBaseComponent);

View File

@@ -1,51 +0,0 @@
const React = require('react');
const { themeStyle } = require('lib/theme');
class ToolbarButton extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
const style = Object.assign({}, theme.toolbarStyle);
const title = this.props.title ? this.props.title : '';
const tooltip = this.props.tooltip ? this.props.tooltip : title;
let icon = null;
if (this.props.iconName) {
const iconStyle = {
fontSize: Math.round(theme.fontSize * 1.5),
color: theme.iconColor,
};
if (title) iconStyle.marginRight = 5;
icon = <i style={iconStyle} className={`fas ${this.props.iconName}`}></i>;
}
// Keep this for legacy compatibility but for consistency we should use "disabled" prop
let isEnabled = !('enabled' in this.props) || this.props.enabled === true;
if (this.props.disabled) isEnabled = false;
const classes = ['button'];
if (!isEnabled) classes.push('disabled');
const finalStyle = Object.assign({}, style, {
opacity: isEnabled ? 1 : 0.4,
});
return (
<a
className={classes.join(' ')}
style={finalStyle}
title={tooltip}
href="#"
onClick={() => {
if (isEnabled && this.props.onClick) this.props.onClick();
}}
>
{icon}
{title}
</a>
);
}
}
module.exports = ToolbarButton;

View File

@@ -0,0 +1,63 @@
import * as React from 'react';
import { ToolbarButtonInfo } from 'lib/services/CommandService';
import { StyledRoot, StyledIconSpan, StyledIconI } from './styles';
interface Props {
readonly themeId: number,
readonly toolbarButtonInfo?: ToolbarButtonInfo,
readonly title?: string,
readonly tooltip?: string,
readonly iconName?: string,
readonly disabled?: boolean,
readonly backgroundHover?: boolean,
}
function isFontAwesomeIcon(iconName:string) {
const s = iconName.split(' ');
return s.length === 2 && ['fa', 'fas'].includes(s[0]);
}
function getProp(props:Props, name:string, defaultValue:any = null) {
if (props.toolbarButtonInfo && (name in props.toolbarButtonInfo)) return (props.toolbarButtonInfo as any)[name];
if (!(name in props)) return defaultValue;
return (props as any)[name];
}
export default function ToolbarButton(props:Props) {
const title = getProp(props, 'title', '');
const tooltip = getProp(props, 'tooltip', title);
let icon = null;
const iconName = getProp(props, 'iconName');
if (iconName) {
const IconClass = isFontAwesomeIcon(iconName) ? StyledIconI : StyledIconSpan;
icon = <IconClass className={iconName} title={title}/>;
}
// Keep this for legacy compatibility but for consistency we should use "disabled" prop
let isEnabled = getProp(props, 'enabled', null);
if (isEnabled === null) isEnabled = true;
if (props.disabled) isEnabled = false;
const classes = ['button'];
if (!isEnabled) classes.push('disabled');
const onClick = getProp(props, 'onClick');
return (
<StyledRoot
className={classes.join(' ')}
disabled={!isEnabled}
title={tooltip}
href="#"
hasTitle={!!title}
onClick={() => {
if (isEnabled && onClick) onClick();
}}
>
{icon}
{title}
</StyledRoot>
);
}

View File

@@ -0,0 +1,40 @@
const styled = require('styled-components').default;
const { css } = require('styled-components');
interface RootProps {
readonly theme: any;
readonly disabled: boolean;
readonly hasTitle: boolean;
}
export const StyledRoot = styled.a<RootProps>`
opacity: ${(props:RootProps) => props.disabled ? 0.3 : 1};
height: ${(props:RootProps) => props.theme.toolbarHeight}px;
min-height: ${(props:RootProps) => props.theme.toolbarHeight}px;
width: ${(props:RootProps) => props.hasTitle ? 'auto' : `${props.theme.toolbarHeight}px`};
max-width: ${(props:RootProps) => props.hasTitle ? 'auto' : `${props.theme.toolbarHeight}px`};
display: flex;
align-items: center;
justify-content: center;
cursor: default;
border-radius: 3px;
box-sizing: border-box;
&:hover {
background-color: ${(props:RootProps) => props.disabled ? 'none' : props.theme.backgroundColorHover3};
}
`;
interface IconProps {
readonly theme: any;
readonly title: string;
}
const iconStyle = css<IconProps>`
font-size: ${(props:IconProps) => props.theme.toolbarIconSize}px;
color: ${(props:IconProps) => props.theme.color3};
margin-right: ${(props:IconProps) => props.title ? 5 : 0}px;
`;
export const StyledIconI = styled.i`${iconStyle}`;
export const StyledIconSpan = styled.span`${iconStyle}`;

View File

@@ -3,7 +3,7 @@ const { themeStyle } = require('lib/theme');
class ToolbarSpace extends React.Component { class ToolbarSpace extends React.Component {
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.themeId);
const style = Object.assign({}, theme.toolbarStyle); const style = Object.assign({}, theme.toolbarStyle);
style.minWidth = style.height / 2; style.minWidth = style.height / 2;

View File

@@ -1,7 +1,7 @@
const { createSelector } = require('reselect'); const { createSelector } = require('reselect');
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const themeSelector = (state, props) => themeStyle(props.theme); const themeSelector = (state, props) => themeStyle(props.themeId);
const style = createSelector( const style = createSelector(
themeSelector, themeSelector,

View File

@@ -1,7 +1,7 @@
const { createSelector } = require('reselect'); const { createSelector } = require('reselect');
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const themeSelector = (state, props) => themeStyle(props.theme); const themeSelector = (state, props) => themeStyle(props.themeId);
const style = createSelector( const style = createSelector(
themeSelector, themeSelector,

View File

@@ -0,0 +1,25 @@
const styled = require('styled-components').default;
const Color = require('color');
const StyledInput = styled.input`
border: 1px solid ${(props:any) => Color(props.theme.color3).alpha(0.6)};
border-radius: 3px;
font-size: ${(props:any) => props.theme.fontSize}px;
color: ${(props:any) => props.theme.color};
padding: 0 8px;
height: ${(props:any) => `${props.theme.toolbarHeight}px`};
max-height: ${(props:any) => `${props.theme.toolbarHeight}px`};
box-sizing: border-box;
background-color: ${(props:any) => Color(props.theme.backgroundColor4).alpha(0.5)};
&::placeholder {
color: ${(props:any) => props.theme.colorFaded};
}
&:focus {
background-color: ${(props:any) => props.theme.backgroundColor4};
border: 1px solid ${(props:any) => props.theme.color3};
}
`;
export default StyledInput;

View File

@@ -0,0 +1,7 @@
const styled = require('styled-components').default;
const StyledInput = styled.input`
`;
export default StyledInput;

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