You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-27 20:29:45 +02:00
Compare commits
69 Commits
ios-v10.1.
...
android-v1
Author | SHA1 | Date | |
---|---|---|---|
|
559655bf33 | ||
|
0eab23fbcf | ||
|
f334f4f487 | ||
|
00057da17d | ||
|
0a05464013 | ||
|
9ebb574059 | ||
|
d29c3c2466 | ||
|
a71f1c19ec | ||
|
485921d879 | ||
|
15de7572c0 | ||
|
09f41dd50e | ||
|
7b8ee467a0 | ||
|
99a496d684 | ||
|
f43ee123d8 | ||
|
f42fb1b871 | ||
|
cf2442c5b2 | ||
|
e0e4735b03 | ||
|
8bd58c9608 | ||
|
215a725ded | ||
|
12c0a05af0 | ||
|
a7fa119041 | ||
|
7fb52b8b0e | ||
|
3e86ae4a82 | ||
|
947d81d96d | ||
|
6ca640d2ed | ||
|
6aca233b21 | ||
|
2200be697e | ||
|
25ab3c323b | ||
|
5bf30a9586 | ||
|
b6779a8074 | ||
|
59599d318c | ||
|
538600fd6c | ||
|
63264ba471 | ||
|
95e7f3df7d | ||
|
366fd2a333 | ||
|
5be99a4a16 | ||
|
d86f6a1fbd | ||
|
7d68208cb4 | ||
|
e9de9d9128 | ||
|
1af16d9f0b | ||
|
8e11eababa | ||
|
4ec9faadd5 | ||
|
5cf462c885 | ||
|
f7ef0a2b1e | ||
|
870f55a6c5 | ||
|
7f7e38b434 | ||
|
460a07b1a3 | ||
|
48c9b86d2b | ||
|
7202066c1f | ||
|
5226f0019b | ||
|
26ac745419 | ||
|
b3f2bbee5b | ||
|
56c6cfc785 | ||
|
1db4932573 | ||
|
a2873ebbc5 | ||
|
f652011d59 | ||
|
27c572b2f5 | ||
|
7a4c97618d | ||
|
3ac4fbeee5 | ||
|
9e05fa553c | ||
|
a9b26246e6 | ||
|
cc1e941dd9 | ||
|
ad85a12535 | ||
|
b825346829 | ||
|
bd4cbaf93d | ||
|
9af2a19bdf | ||
|
d3fa906a9a | ||
|
22679641ee | ||
|
056285deda |
@@ -69,8 +69,12 @@ ElectronClient/commands/focusElement.js
|
||||
ElectronClient/commands/startExternalEditing.js
|
||||
ElectronClient/commands/stopExternalEditing.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/Header/commands/focusSearch.js
|
||||
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
ElectronClient/gui/KeymapConfig/ShortcutRecorder.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/hideModalMessage.js
|
||||
ElectronClient/gui/MainScreen/commands/moveToFolder.js
|
||||
ElectronClient/gui/MainScreen/commands/newFolder.js
|
||||
ElectronClient/gui/MainScreen/commands/newNote.js
|
||||
ElectronClient/gui/MainScreen/commands/newNotebook.js
|
||||
ElectronClient/gui/MainScreen/commands/newTodo.js
|
||||
ElectronClient/gui/MainScreen/commands/print.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/showNoteProperties.js
|
||||
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleEditors.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleNoteList.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleSidebar.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js
|
||||
ElectronClient/gui/MainScreen/MainScreen.js
|
||||
ElectronClient/gui/MultiNoteActions.js
|
||||
ElectronClient/gui/NoteContentPropertiesDialog.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/useEditorSearch.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/useListIdent.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/utils/useScroll.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/types.js
|
||||
ElectronClient/gui/NoteEditor/utils/useDropHandler.js
|
||||
ElectronClient/gui/NoteEditor/utils/useFolder.js
|
||||
ElectronClient/gui/NoteEditor/utils/useFormNote.js
|
||||
ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||
ElectronClient/gui/NoteEditor/utils/useMessageHandler.js
|
||||
ElectronClient/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||
ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.js
|
||||
ElectronClient/gui/NoteEditor/utils/useSearchMarkers.js
|
||||
ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.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/NoteTextViewer.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/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/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/checkPermissions.js
|
||||
ReactNativeClient/lib/commands/historyBackward.js
|
||||
@@ -147,6 +178,7 @@ ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js
|
||||
ReactNativeClient/lib/hooks/useEffectDebugger.js
|
||||
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.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/fence.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/ntpDate.js
|
||||
ReactNativeClient/lib/services/CommandService.js
|
||||
ReactNativeClient/lib/services/debug/populateDatabase.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainService.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.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/ShareExtension.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/PluginAssetsLoader.js
|
||||
ReactNativeClient/setUpQuickActions.js
|
||||
|
47
.gitignore
vendored
47
.gitignore
vendored
@@ -62,8 +62,12 @@ ElectronClient/commands/focusElement.js
|
||||
ElectronClient/commands/startExternalEditing.js
|
||||
ElectronClient/commands/stopExternalEditing.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/Header/commands/focusSearch.js
|
||||
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
ElectronClient/gui/KeymapConfig/ShortcutRecorder.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/hideModalMessage.js
|
||||
ElectronClient/gui/MainScreen/commands/moveToFolder.js
|
||||
ElectronClient/gui/MainScreen/commands/newFolder.js
|
||||
ElectronClient/gui/MainScreen/commands/newNote.js
|
||||
ElectronClient/gui/MainScreen/commands/newNotebook.js
|
||||
ElectronClient/gui/MainScreen/commands/newTodo.js
|
||||
ElectronClient/gui/MainScreen/commands/print.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/showNoteProperties.js
|
||||
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleEditors.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleNoteList.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleSidebar.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js
|
||||
ElectronClient/gui/MainScreen/MainScreen.js
|
||||
ElectronClient/gui/MultiNoteActions.js
|
||||
ElectronClient/gui/NoteContentPropertiesDialog.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/useEditorSearch.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/useListIdent.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/utils/useScroll.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/types.js
|
||||
ElectronClient/gui/NoteEditor/utils/useDropHandler.js
|
||||
ElectronClient/gui/NoteEditor/utils/useFolder.js
|
||||
ElectronClient/gui/NoteEditor/utils/useFormNote.js
|
||||
ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||
ElectronClient/gui/NoteEditor/utils/useMessageHandler.js
|
||||
ElectronClient/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||
ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.js
|
||||
ElectronClient/gui/NoteEditor/utils/useSearchMarkers.js
|
||||
ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.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/NoteTextViewer.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/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/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/checkPermissions.js
|
||||
ReactNativeClient/lib/commands/historyBackward.js
|
||||
@@ -140,6 +171,7 @@ ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js
|
||||
ReactNativeClient/lib/hooks/useEffectDebugger.js
|
||||
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.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/fence.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/ntpDate.js
|
||||
ReactNativeClient/lib/services/CommandService.js
|
||||
ReactNativeClient/lib/services/debug/populateDatabase.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainService.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.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/ShareExtension.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/PluginAssetsLoader.js
|
||||
ReactNativeClient/setUpQuickActions.js
|
||||
|
3
BUILD.md
3
BUILD.md
@@ -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: Install Cocoapods - `brew install cocoapods`
|
||||
- Windows: Install Windows Build Tools - `npm install -g windows-build-tools`
|
||||
- Linux: Install dependencies - `sudo apt install libnss3 libsecret-1-dev`
|
||||
|
||||
## Building
|
||||
|
||||
@@ -25,6 +26,8 @@ Then you can test the various applications:
|
||||
cd ElectronClient
|
||||
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
|
||||
|
||||
cd CliClient
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ msgstr ""
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.0.6\n"
|
||||
"X-Generator: Poedit 2.4.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
@@ -822,8 +822,8 @@ msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"Entrez le titre d’une note, ou entrez # suivit du nom d’une étiquette, ou @ "
|
||||
"suivit du nom d’un carnet."
|
||||
"Entrez le titre d’une note, ou entrez # suivi du nom d’une étiquette, ou @ "
|
||||
"suivi du nom d’un carnet."
|
||||
|
||||
#: ElectronClient/plugins/GotoAnything.min.js:486
|
||||
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:20
|
||||
|
@@ -15,6 +15,8 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 1.8.4\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
|
||||
#: CliClient/app/command-cp.js:13
|
||||
msgid ""
|
||||
@@ -312,7 +314,7 @@ msgstr "指定のターゲットと同期します。(標準: sync.targetの
|
||||
|
||||
#: CliClient/app/command-sync.js:35
|
||||
msgid "Upgrade the sync target to the latest version."
|
||||
msgstr ""
|
||||
msgstr "同期先を最新バージョンにアップグレード。"
|
||||
|
||||
#: CliClient/app/command-sync.js:81 CliClient/app/command-sync.js:95
|
||||
#: ElectronClient/gui/OneDriveLoginScreen.min.js:40
|
||||
@@ -796,6 +798,8 @@ msgstr "キャンセル"
|
||||
msgid ""
|
||||
"The app is now going to close. Please relaunch it to complete the process."
|
||||
msgstr ""
|
||||
"まもなくアプリケーションは終了します。もう一度起動して処理を完了させてくださ"
|
||||
"い。"
|
||||
|
||||
#: ElectronClient/plugins/GotoAnything.min.js:446
|
||||
msgid ""
|
||||
@@ -1157,13 +1161,13 @@ msgstr "新しい %s を作成中..."
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteEditor.js:344
|
||||
msgid "The following attachments are being watched for changes:"
|
||||
msgstr ""
|
||||
msgstr "下記の添付ファイルが変更されたかどうかを監視中です。"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteEditor.js:347
|
||||
msgid ""
|
||||
"The attachments will no longer be watched when you switch to a different "
|
||||
"note."
|
||||
msgstr ""
|
||||
msgstr "添付ファイルの監視は他のノートに移動すると終了します。"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:25
|
||||
msgid "Select all"
|
||||
@@ -1274,7 +1278,7 @@ msgstr "ノートのプロパティ"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:62
|
||||
msgid "An unexpected error occured while importing the keymap!"
|
||||
msgstr ""
|
||||
msgstr "キーマップのインポート中に予期しないエラーが発生しました!"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:119
|
||||
#: ElectronClient/gui/MainScreen/MainScreen.min.js:437
|
||||
@@ -1289,12 +1293,11 @@ msgstr "インポート"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:125
|
||||
msgid "Command"
|
||||
msgstr ""
|
||||
msgstr "コマンド"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:126
|
||||
#, fuzzy
|
||||
msgid "Keyboard Shortcut"
|
||||
msgstr "キーバインド"
|
||||
msgstr "ショートカットキー"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:14
|
||||
#: ElectronClient/app.js:690
|
||||
@@ -1317,9 +1320,8 @@ msgid "Website and documentation"
|
||||
msgstr "Webサイトとドキュメント"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:24
|
||||
#, fuzzy
|
||||
msgid "Hide Joplin"
|
||||
msgstr "Joplinについて"
|
||||
msgstr "Joplinを隠す"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:26
|
||||
#: ElectronClient/app.js:703
|
||||
@@ -1327,7 +1329,6 @@ msgid "Close Window"
|
||||
msgstr "ウィンドウを閉じる"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28
|
||||
#, fuzzy
|
||||
msgid "Preferences"
|
||||
msgstr "環境設定"
|
||||
|
||||
@@ -1338,13 +1339,15 @@ msgstr "オプション"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48
|
||||
msgid "Press the shortcut"
|
||||
msgstr ""
|
||||
msgstr "ショートカットキーを押してください"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48
|
||||
msgid ""
|
||||
"Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the "
|
||||
"shortcut."
|
||||
msgstr ""
|
||||
"ショートカットキーに続けてENTERを押すことで設定します。ショートカットを削除す"
|
||||
"るにはBACKSPACEを押してください。"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:49
|
||||
#: 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 "
|
||||
"proceed please click on the link."
|
||||
msgstr ""
|
||||
"同期するには同期先をアップグレードする必要があります。アップグレードには数分"
|
||||
"かかるかもしれません。またアプリケーションの再起動が必要です。アップグレード"
|
||||
"するにはリンクをクリックしてください。"
|
||||
|
||||
#: ElectronClient/gui/MainScreen/MainScreen.min.js:306
|
||||
#, fuzzy
|
||||
msgid "Restart and upgrade"
|
||||
msgstr "アップグレードが必要なマスターキー"
|
||||
msgstr "再起動してアップグレード"
|
||||
|
||||
#: ElectronClient/gui/MainScreen/MainScreen.min.js:313
|
||||
msgid "Some items cannot be synchronised."
|
||||
@@ -2114,9 +2119,8 @@ msgid "Templates"
|
||||
msgstr "テンプレート"
|
||||
|
||||
#: ElectronClient/app.js:668
|
||||
#, fuzzy
|
||||
msgid "Export all"
|
||||
msgstr "エクスポート"
|
||||
msgstr "すべてをエクスポート"
|
||||
|
||||
#: ElectronClient/app.js:683
|
||||
#, javascript-format
|
||||
@@ -2215,7 +2219,7 @@ msgstr "不明なレベルID: %s"
|
||||
|
||||
#: ReactNativeClient/lib/SyncTargetAmazonS3.js:28
|
||||
msgid "AWS S3"
|
||||
msgstr ""
|
||||
msgstr "AWS S3"
|
||||
|
||||
#: ReactNativeClient/lib/SyncTargetDropbox.js:25
|
||||
msgid "Dropbox"
|
||||
@@ -2360,15 +2364,15 @@ msgstr "WevDAV パスワード"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:195
|
||||
msgid "AWS S3 bucket"
|
||||
msgstr ""
|
||||
msgstr "AWS S3 バケット"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:206
|
||||
msgid "AWS key"
|
||||
msgstr ""
|
||||
msgstr "AWS アクセスキーID"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:216
|
||||
msgid "AWS secret"
|
||||
msgstr ""
|
||||
msgstr "AWS シークレットアクセスキー"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:230
|
||||
msgid "Attachment download behaviour"
|
||||
@@ -2823,9 +2827,8 @@ msgid "Web Clipper"
|
||||
msgstr "Webクリッパー"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:1259
|
||||
#, fuzzy
|
||||
msgid "Keyboard Shortcuts"
|
||||
msgstr "キーバインド"
|
||||
msgstr "キーボードショートカット"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:1264
|
||||
msgid ""
|
||||
@@ -2980,6 +2983,8 @@ msgstr "いくつかの項目は同期されませんでした。詳細はクリ
|
||||
#: ReactNativeClient/lib/components/screen-header.js:453
|
||||
msgid "The sync target needs to be upgraded. Press this banner to proceed."
|
||||
msgstr ""
|
||||
"同期先をアップグレードする必要があります。アップグレードするにはバナーをク"
|
||||
"リックしてください。"
|
||||
|
||||
#: ReactNativeClient/lib/components/side-menu-content.js:126
|
||||
#, javascript-format
|
||||
@@ -3024,8 +3029,8 @@ msgid ""
|
||||
"Error. Please check that URL, username, password, etc. are correct and that "
|
||||
"the sync target is accessible. The reported error was:"
|
||||
msgstr ""
|
||||
"エラーです。URL、ユーザー名、パスワードなどを修正し、同期するターゲットにアク"
|
||||
"セスできるかを確認してください。次が報告されたエラーです:"
|
||||
"エラーです。URL、ユーザー名、パスワードなどを修正し、同期先にアクセスできるか"
|
||||
"を確認してください。次が報告されたエラーです:"
|
||||
|
||||
#: ReactNativeClient/lib/components/shared/dropbox-login-shared.js:39
|
||||
msgid "The application has been authorised!"
|
||||
@@ -3102,7 +3107,7 @@ msgstr "更新"
|
||||
|
||||
#: ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js:42
|
||||
msgid "Sync Target Upgrade"
|
||||
msgstr ""
|
||||
msgstr "同期先のアップグレード"
|
||||
|
||||
#: ReactNativeClient/lib/components/screens/NoteTagsDialog.js:163
|
||||
msgid "New tags:"
|
||||
@@ -3151,10 +3156,10 @@ msgid ""
|
||||
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||
"the data! To enable encryption, please enter your password below."
|
||||
msgstr ""
|
||||
"暗号化を有効にするとは、*すべて*のノートや添付ファイルを再同期し、同期ター"
|
||||
"ゲットに暗号化した状態で送ることを意味します。パスワードはなくさないようにし"
|
||||
"てください。セキュリティ上、このパスワードがデータを復号する*唯一*の方法にな"
|
||||
"るためです! 暗号化を有効にするには、下にパスワードを入力してください。"
|
||||
"暗号化を有効にするとは、*すべて*のノートや添付ファイルを再同期し、同期先に暗"
|
||||
"号化した状態で送ることを意味します。パスワードはなくさないようにしてくださ"
|
||||
"い。セキュリティ上、このパスワードがデータを復号する*唯一*の方法になるためで"
|
||||
"す! 暗号化を有効にするには、下にパスワードを入力してください。"
|
||||
|
||||
#: ReactNativeClient/lib/components/screens/encryption-config.js:177
|
||||
msgid "Enable"
|
||||
@@ -3546,32 +3551,35 @@ msgstr "ノートをどのノートブックにインポートするのか指定
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:124
|
||||
#, javascript-format
|
||||
msgid "Error loading the keymap from file: %s"
|
||||
msgstr ""
|
||||
msgstr "キーマップ読み込みエラー(ファイル: %s)"
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:141
|
||||
#, javascript-format
|
||||
msgid "Error saving the keymap to file: %s"
|
||||
msgstr ""
|
||||
msgstr "キーマップ書き出しエラー(ファイル: %s)"
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:204
|
||||
#, javascript-format
|
||||
msgid "Keymap item %s is missing the required \"command\" property."
|
||||
msgstr ""
|
||||
"キーマップアイテム %s は必須の \"command\" プロパティを持っていません。"
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:207
|
||||
#, javascript-format
|
||||
msgid "Keymap item %s is invalid because %s is not a valid command."
|
||||
msgstr ""
|
||||
msgstr "キーマップアイテム %s は %s が有効なコマンドでないため無効です。"
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:210
|
||||
#, javascript-format
|
||||
msgid "Keymap item %s is missing the required \"accelerator\" property."
|
||||
msgstr ""
|
||||
"キーマップアイテム %s は必須の \"accelerator\" プロパティを持っていません。"
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:217
|
||||
#, javascript-format
|
||||
msgid "Keymap item %s is invalid because %s is not a valid accelerator."
|
||||
msgstr ""
|
||||
"キーマップアイテム %s は %s が有効なショートカットキーでないため無効です。"
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:235
|
||||
#, javascript-format
|
||||
@@ -3579,11 +3587,13 @@ msgid ""
|
||||
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to "
|
||||
"unexpected behaviour."
|
||||
msgstr ""
|
||||
"ショートカットキー \"%s\" は \"%s\" コマンドと \"%s\" コマンドで使われていま"
|
||||
"す。これにより予想外の動作が起こる可能性があります。"
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:260
|
||||
#, javascript-format
|
||||
msgid "Accelerator \"%s\" is not valid."
|
||||
msgstr ""
|
||||
msgstr "ショートカットキー \"%s\" は無効です。"
|
||||
|
||||
#: ReactNativeClient/lib/services/report.js:121
|
||||
msgid "Items that cannot be synchronised"
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,9 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\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
|
||||
msgid ""
|
||||
@@ -319,7 +321,7 @@ msgstr ""
|
||||
|
||||
#: CliClient/app/command-sync.js:35
|
||||
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
|
||||
#: ElectronClient/gui/OneDriveLoginScreen.min.js:40
|
||||
@@ -813,6 +815,8 @@ msgstr "İptal et"
|
||||
msgid ""
|
||||
"The app is now going to close. Please relaunch it to complete the process."
|
||||
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
|
||||
msgid ""
|
||||
@@ -1173,13 +1177,13 @@ msgstr "Yeni %s oluşturuluyor..."
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteEditor.js:344
|
||||
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
|
||||
msgid ""
|
||||
"The attachments will no longer be watched when you switch to a different "
|
||||
"note."
|
||||
msgstr ""
|
||||
msgstr "Eğer başka bir not'a geçerseniz ek dosyalar artık izlenmeyecek."
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:25
|
||||
msgid "Select all"
|
||||
@@ -1290,7 +1294,7 @@ msgstr "Not özellikleri"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:62
|
||||
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/MainScreen/MainScreen.min.js:437
|
||||
@@ -1305,12 +1309,11 @@ msgstr "İçe aktar"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:125
|
||||
msgid "Command"
|
||||
msgstr ""
|
||||
msgstr "Komut"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js:126
|
||||
#, fuzzy
|
||||
msgid "Keyboard Shortcut"
|
||||
msgstr "Klavye modu"
|
||||
msgstr "Klavye Kısayolu"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:14
|
||||
#: ElectronClient/app.js:690
|
||||
@@ -1333,9 +1336,8 @@ msgid "Website and documentation"
|
||||
msgstr "Web sitesi ve dökümanlar"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:24
|
||||
#, fuzzy
|
||||
msgid "Hide Joplin"
|
||||
msgstr "Joplin hakkında"
|
||||
msgstr "Joplin'i Gizle"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:26
|
||||
#: ElectronClient/app.js:703
|
||||
@@ -1343,9 +1345,8 @@ msgid "Close Window"
|
||||
msgstr "Pencereyi Kapat"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28
|
||||
#, fuzzy
|
||||
msgid "Preferences"
|
||||
msgstr "Tercihler..."
|
||||
msgstr "Tercihler"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/utils/getLabel.js:28
|
||||
#: ElectronClient/gui/Root.min.js:92 ElectronClient/app.js:572
|
||||
@@ -1354,13 +1355,15 @@ msgstr "Seçenekler"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48
|
||||
msgid "Press the shortcut"
|
||||
msgstr ""
|
||||
msgstr "Kısayolu girin"
|
||||
|
||||
#: ElectronClient/gui/KeymapConfig/ShortcutRecorder.js:48
|
||||
msgid ""
|
||||
"Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the "
|
||||
"shortcut."
|
||||
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/EncryptionConfigScreen.min.js:95
|
||||
@@ -1374,11 +1377,14 @@ msgid ""
|
||||
"may take a few minutes to complete and the app needs to be restarted. To "
|
||||
"proceed please click on the link."
|
||||
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
|
||||
#, fuzzy
|
||||
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
|
||||
msgid "Some items cannot be synchronised."
|
||||
@@ -2138,9 +2144,8 @@ msgid "Templates"
|
||||
msgstr "Şablonlar"
|
||||
|
||||
#: ElectronClient/app.js:668
|
||||
#, fuzzy
|
||||
msgid "Export all"
|
||||
msgstr "Dışa aktar"
|
||||
msgstr "Tümünü dışa aktar"
|
||||
|
||||
#: ElectronClient/app.js:683
|
||||
#, javascript-format
|
||||
@@ -2241,7 +2246,7 @@ msgstr "Bilinmeyen ID seviyesi: %s"
|
||||
|
||||
#: ReactNativeClient/lib/SyncTargetAmazonS3.js:28
|
||||
msgid "AWS S3"
|
||||
msgstr ""
|
||||
msgstr "AWS S3"
|
||||
|
||||
#: ReactNativeClient/lib/SyncTargetDropbox.js:25
|
||||
msgid "Dropbox"
|
||||
@@ -2384,15 +2389,15 @@ msgstr "WebDAV şifresi"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:195
|
||||
msgid "AWS S3 bucket"
|
||||
msgstr ""
|
||||
msgstr "AWS S3 deposu"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:206
|
||||
msgid "AWS key"
|
||||
msgstr ""
|
||||
msgstr "AWS anahtarı"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:216
|
||||
msgid "AWS secret"
|
||||
msgstr ""
|
||||
msgstr "AWS gizli anahtarı"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:230
|
||||
msgid "Attachment download behaviour"
|
||||
@@ -2850,9 +2855,8 @@ msgid "Web Clipper"
|
||||
msgstr "Web Alıntılayıcısı"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:1259
|
||||
#, fuzzy
|
||||
msgid "Keyboard Shortcuts"
|
||||
msgstr "Klavye modu"
|
||||
msgstr "Klavye Kısayolları"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:1264
|
||||
msgid ""
|
||||
@@ -3008,6 +3012,8 @@ msgstr "Bazı öğeler senkronize edilemiyor. Detayları için tıklayın."
|
||||
#: ReactNativeClient/lib/components/screen-header.js:453
|
||||
msgid "The sync target needs to be upgraded. Press this banner to proceed."
|
||||
msgstr ""
|
||||
"Senkronizasyon hedefinin güncellenmesi gerekmekte. Bu banner'a tıklayarak "
|
||||
"devam edebilirsiniz."
|
||||
|
||||
#: ReactNativeClient/lib/components/side-menu-content.js:126
|
||||
#, javascript-format
|
||||
@@ -3129,7 +3135,7 @@ msgstr "Yenile"
|
||||
|
||||
#: ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js:42
|
||||
msgid "Sync Target Upgrade"
|
||||
msgstr ""
|
||||
msgstr "Senkronizasyon Hedefi Güncellemesi"
|
||||
|
||||
#: ReactNativeClient/lib/components/screens/NoteTagsDialog.js:163
|
||||
msgid "New tags:"
|
||||
@@ -3578,32 +3584,37 @@ msgstr "Lütfen notların alınacağı not defterini belirtin."
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:124
|
||||
#, javascript-format
|
||||
msgid "Error loading the keymap from file: %s"
|
||||
msgstr ""
|
||||
msgstr "%s dosyasından tuş haritası yüklenemedi"
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:141
|
||||
#, javascript-format
|
||||
msgid "Error saving the keymap to file: %s"
|
||||
msgstr ""
|
||||
msgstr "%s dosyasına tuş haritası kaydedilemedi"
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:204
|
||||
#, javascript-format
|
||||
msgid "Keymap item %s is missing the required \"command\" property."
|
||||
msgstr ""
|
||||
"%s tuş haritası, işlemler için gerekli olan \"command\" özelliğini "
|
||||
"barındırmıyor."
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:207
|
||||
#, javascript-format
|
||||
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
|
||||
#, javascript-format
|
||||
msgid "Keymap item %s is missing the required \"accelerator\" property."
|
||||
msgstr ""
|
||||
"%s tuş haritası, işlemler için gerekli olan \"accelerator\" özelliğini "
|
||||
"barındırmıyor."
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:217
|
||||
#, javascript-format
|
||||
msgid "Keymap item %s is invalid because %s is not a valid accelerator."
|
||||
msgstr ""
|
||||
"%s tuş haritası geçersizdir, çünkü %s geçerli bir accelerator değildir."
|
||||
|
||||
#: ReactNativeClient/lib/services/KeymapService.js:235
|
||||
#, javascript-format
|
||||
@@ -3611,11 +3622,13 @@ msgid ""
|
||||
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to "
|
||||
"unexpected behaviour."
|
||||
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
|
||||
#, javascript-format
|
||||
msgid "Accelerator \"%s\" is not valid."
|
||||
msgstr ""
|
||||
msgstr "Kısayol \"%s\" geçersiz."
|
||||
|
||||
#: ReactNativeClient/lib/services/report.js:121
|
||||
msgid "Items that cannot be synchronised"
|
||||
@@ -3949,7 +3962,7 @@ msgstr ""
|
||||
#~ "Şu anda not defteriniz yok. (+) butonuna tıklayarak bir tane oluşturun."
|
||||
|
||||
#~ msgid "Welcome"
|
||||
#~ msgstr "Hoşgeldiniz"
|
||||
#~ msgstr "Hoş Geldiniz"
|
||||
|
||||
#~ msgid "Separate each tag by a comma."
|
||||
#~ msgstr "Her etiketi virgülle ayırın."
|
||||
|
20
CliClient/package-lock.json
generated
20
CliClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "1.1.8",
|
||||
"version": "1.2.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -5880,6 +5880,11 @@
|
||||
"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": {
|
||||
"version": "0.8.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.0.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@@ -28,7 +28,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "1.1.8",
|
||||
"version": "1.2.2",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -109,7 +109,7 @@
|
||||
"terminal-kit": "^1.30.0",
|
||||
"tkwidgets": "^0.5.26",
|
||||
"url-parse": "^1.4.7",
|
||||
"uslug": "^1.0.4",
|
||||
"slug": "^3.3.4",
|
||||
"uuid": "^3.0.1",
|
||||
"valid-url": "^1.0.9",
|
||||
"word-wrap": "^1.2.3",
|
||||
|
@@ -1,163 +1,163 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint prefer-const: 0*/
|
||||
|
||||
require('app-module-path').addPath(__dirname);
|
||||
// require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-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 Note = require('lib/models/Note');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Tag = require('lib/models/Tag');
|
||||
const ItemChange = require('lib/models/ItemChange');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { shim } = require('lib/shim');
|
||||
const ResourceService = require('lib/services/ResourceService.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 SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
// const Note = require('lib/models/Note');
|
||||
// const Folder = require('lib/models/Folder');
|
||||
// const Tag = require('lib/models/Tag');
|
||||
// const ItemChange = require('lib/models/ItemChange');
|
||||
// const Setting = require('lib/models/Setting');
|
||||
// const Resource = require('lib/models/Resource.js');
|
||||
// const { shim } = require('lib/shim');
|
||||
// const ResourceService = require('lib/services/ResourceService.js');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
// process.on('unhandledRejection', (reason, p) => {
|
||||
// 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() {
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
// describe('services_SearchFuzzy', function() {
|
||||
// beforeEach(async (done) => {
|
||||
// await setupDatabaseAndSynchronizer(1);
|
||||
// await switchClient(1);
|
||||
|
||||
engine = new SearchEngine();
|
||||
engine.setDb(db());
|
||||
// engine = new SearchEngine();
|
||||
// engine.setDb(db());
|
||||
|
||||
Setting.setValue('db.fuzzySearchEnabled', 1);
|
||||
done();
|
||||
});
|
||||
// Setting.setValue('db.fuzzySearchEnabled', 1);
|
||||
// done();
|
||||
// });
|
||||
|
||||
|
||||
it('should return note almost matching title', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'If It Ain\'t Baroque, Don\'t Fix It' });
|
||||
const n2 = await Note.save({ title: 'Important note' });
|
||||
// it('should return note almost matching title', asyncTest(async () => {
|
||||
// let rows;
|
||||
// const n1 = await Note.save({ title: 'If It Ain\'t Baroque, Don\'t Fix It' });
|
||||
// const n2 = await Note.save({ title: 'Important note' });
|
||||
|
||||
await engine.syncTables();
|
||||
rows = await engine.search('Broke', { fuzzy: false });
|
||||
expect(rows.length).toBe(0);
|
||||
// await engine.syncTables();
|
||||
// rows = await engine.search('Broke', { fuzzy: false });
|
||||
// expect(rows.length).toBe(0);
|
||||
|
||||
rows = await engine.search('Broke', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
// rows = await engine.search('Broke', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
|
||||
rows = await engine.search('title:Broke', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
// rows = await engine.search('title:Broke', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
rows = await engine.search('title:"Broke"', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
// rows = await engine.search('title:"Broke"', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
rows = await engine.search('Imprtant', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n2.id);
|
||||
}));
|
||||
// rows = await engine.search('Imprtant', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows[0].id).toBe(n2.id);
|
||||
// }));
|
||||
|
||||
|
||||
it('should order results by min fuzziness', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'I demand you take me to him' });
|
||||
const n2 = await Note.save({ title: 'He demanded an answer' });
|
||||
const n3 = await Note.save({ title: 'Don\'t you make demands of me' });
|
||||
const n4 = await Note.save({ title: 'No drama for me' });
|
||||
const n5 = await Note.save({ title: 'Just minding my own business' });
|
||||
// it('should order results by min fuzziness', asyncTest(async () => {
|
||||
// let rows;
|
||||
// const n1 = await Note.save({ title: 'I demand you take me to him' });
|
||||
// const n2 = await Note.save({ title: 'He demanded an answer' });
|
||||
// const n3 = await Note.save({ title: 'Don\'t you make demands of me' });
|
||||
// const n4 = await Note.save({ title: 'No drama for me' });
|
||||
// const n5 = await Note.save({ title: 'Just minding my own business' });
|
||||
|
||||
await engine.syncTables();
|
||||
rows = await engine.search('demand', { fuzzy: false });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
// await engine.syncTables();
|
||||
// rows = await engine.search('demand', { fuzzy: false });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
|
||||
rows = await engine.search('demand', { fuzzy: true });
|
||||
expect(rows.length).toBe(3);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
expect(rows[1].id).toBe(n3.id);
|
||||
expect(rows[2].id).toBe(n2.id);
|
||||
}));
|
||||
// rows = await engine.search('demand', { fuzzy: true });
|
||||
// expect(rows.length).toBe(3);
|
||||
// expect(rows[0].id).toBe(n1.id);
|
||||
// expect(rows[1].id).toBe(n3.id);
|
||||
// expect(rows[2].id).toBe(n2.id);
|
||||
// }));
|
||||
|
||||
it('should consider any:1', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'cat' });
|
||||
const n2 = await Note.save({ title: 'cats' });
|
||||
const n3 = await Note.save({ title: 'cot' });
|
||||
// it('should consider any:1', asyncTest(async () => {
|
||||
// let rows;
|
||||
// const n1 = await Note.save({ title: 'cat' });
|
||||
// const n2 = await Note.save({ title: 'cats' });
|
||||
// const n3 = await Note.save({ title: 'cot' });
|
||||
|
||||
const n4 = await Note.save({ title: 'defenestrate' });
|
||||
const n5 = await Note.save({ title: 'defenstrate' });
|
||||
const n6 = await Note.save({ title: 'defenestrated' });
|
||||
// const n4 = await Note.save({ title: 'defenestrate' });
|
||||
// const n5 = await Note.save({ title: 'defenstrate' });
|
||||
// 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 });
|
||||
expect(rows.length).toBe(1);
|
||||
// rows = await engine.search('defenestrated cat', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
|
||||
rows = await engine.search('any:1 defenestrated cat', { fuzzy: true });
|
||||
expect(rows.length).toBe(7);
|
||||
}));
|
||||
// rows = await engine.search('any:1 defenestrated cat', { fuzzy: true });
|
||||
// expect(rows.length).toBe(7);
|
||||
// }));
|
||||
|
||||
it('should leave phrase searches alone', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'abc def' });
|
||||
const n2 = await Note.save({ title: 'def ghi' });
|
||||
const n3 = await Note.save({ title: 'ghi jkl' });
|
||||
const n4 = await Note.save({ title: 'def abc' });
|
||||
const n5 = await Note.save({ title: 'mno pqr ghi jkl' });
|
||||
// it('should leave phrase searches alone', asyncTest(async () => {
|
||||
// let rows;
|
||||
// const n1 = await Note.save({ title: 'abc def' });
|
||||
// const n2 = await Note.save({ title: 'def ghi' });
|
||||
// const n3 = await Note.save({ title: 'ghi jkl' });
|
||||
// const n4 = await Note.save({ title: 'def abc' });
|
||||
// const n5 = await Note.save({ title: 'mno pqr ghi jkl' });
|
||||
|
||||
await engine.syncTables();
|
||||
// await engine.syncTables();
|
||||
|
||||
rows = await engine.search('abc def', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
// rows = await engine.search('abc def', { fuzzy: true });
|
||||
// expect(rows.length).toBe(2);
|
||||
// expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
// expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
|
||||
rows = await engine.search('"abc def"', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
// rows = await engine.search('"abc def"', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
|
||||
rows = await engine.search('"ghi jkl"', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
// rows = await engine.search('"ghi jkl"', { fuzzy: true });
|
||||
// expect(rows.length).toBe(2);
|
||||
// expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
// expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
|
||||
rows = await engine.search('"ghi jkl" mno', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
// rows = await engine.search('"ghi jkl" mno', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
|
||||
rows = await engine.search('any:1 "ghi jkl" mno', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
}));
|
||||
// rows = await engine.search('any:1 "ghi jkl" mno', { fuzzy: true });
|
||||
// expect(rows.length).toBe(2);
|
||||
// expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
// expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
// }));
|
||||
|
||||
it('should leave wild card searches alone', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'abc def' });
|
||||
const n2 = await Note.save({ title: 'abcc ghi' });
|
||||
const n3 = await Note.save({ title: 'abccc ghi' });
|
||||
const n4 = await Note.save({ title: 'abcccc ghi' });
|
||||
const n5 = await Note.save({ title: 'wxy zzz' });
|
||||
// it('should leave wild card searches alone', asyncTest(async () => {
|
||||
// let rows;
|
||||
// const n1 = await Note.save({ title: 'abc def' });
|
||||
// const n2 = await Note.save({ title: 'abcc ghi' });
|
||||
// const n3 = await Note.save({ title: 'abccc ghi' });
|
||||
// const n4 = await Note.save({ title: 'abcccc ghi' });
|
||||
// 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.map(r=>r.id)).toContain(n1.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(n4.id);
|
||||
}));
|
||||
// expect(rows.length).toBe(4);
|
||||
// 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(n3.id);
|
||||
// expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
// }));
|
||||
|
||||
});
|
||||
// });
|
||||
|
@@ -26,10 +26,11 @@ describe('timeUtils', function() {
|
||||
startDate = new Date('3 Aug 2020 07:30:20');
|
||||
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');
|
||||
endDate = new Date('9 Aug 2020'); // week start;
|
||||
expect(time.goBackInTime(startDate, 0, 'week')).toBe(endDate.getTime().toString());
|
||||
// startDate = new Date('11 Aug 2020');
|
||||
// endDate = new Date('9 Aug 2020'); // week start;
|
||||
// expect(time.goBackInTime(startDate, 0, 'week')).toBe(endDate.getTime().toString());
|
||||
|
||||
startDate = new Date('02 Feb 2020');
|
||||
endDate = new Date('01 Jan 2020');
|
||||
@@ -50,9 +51,9 @@ describe('timeUtils', function() {
|
||||
expect(time.goForwardInTime(startDate, 1, 'day')).toBe(endDate.getTime().toString());
|
||||
|
||||
|
||||
startDate = new Date('9 Aug 2020');
|
||||
endDate = new Date('9 Aug 2020'); // week start;
|
||||
expect(time.goForwardInTime(startDate, 0, 'week')).toBe(endDate.getTime().toString());
|
||||
// startDate = new Date('9 Aug 2020');
|
||||
// endDate = new Date('9 Aug 2020'); // week start;
|
||||
// expect(time.goForwardInTime(startDate, 0, 'week')).toBe(endDate.getTime().toString());
|
||||
|
||||
startDate = new Date('02 Jan 2020');
|
||||
endDate = new Date('01 Feb 2020');
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"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.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
@@ -34,16 +34,17 @@ const KeymapService = require('lib/services/KeymapService').default;
|
||||
const TemplateUtils = require('lib/TemplateUtils');
|
||||
const CssUtils = require('lib/CssUtils');
|
||||
const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/reducer').default;
|
||||
// const populateDatabase = require('lib/services/debug/populateDatabase').default;
|
||||
const versionInfo = require('lib/versionInfo').default;
|
||||
|
||||
const commands = [
|
||||
require('./gui/Header/commands/focusSearch'),
|
||||
require('./gui/NoteListControls/commands/focusSearch'),
|
||||
require('./gui/MainScreen/commands/editAlarm'),
|
||||
require('./gui/MainScreen/commands/exportPdf'),
|
||||
require('./gui/MainScreen/commands/hideModalMessage'),
|
||||
require('./gui/MainScreen/commands/moveToFolder'),
|
||||
require('./gui/MainScreen/commands/newNote'),
|
||||
require('./gui/MainScreen/commands/newNotebook'),
|
||||
require('./gui/MainScreen/commands/newFolder'),
|
||||
require('./gui/MainScreen/commands/newTodo'),
|
||||
require('./gui/MainScreen/commands/print'),
|
||||
require('./gui/MainScreen/commands/renameFolder'),
|
||||
@@ -58,6 +59,7 @@ const commands = [
|
||||
require('./gui/MainScreen/commands/toggleNoteList'),
|
||||
require('./gui/MainScreen/commands/toggleSidebar'),
|
||||
require('./gui/MainScreen/commands/toggleVisiblePanes'),
|
||||
require('./gui/MainScreen/commands/toggleEditors'),
|
||||
require('./gui/NoteEditor/commands/focusElementNoteBody'),
|
||||
require('./gui/NoteEditor/commands/focusElementNoteTitle'),
|
||||
require('./gui/NoteEditor/commands/showLocalSearch'),
|
||||
@@ -98,6 +100,8 @@ const appDefaultState = Object.assign({}, defaultState, {
|
||||
watchedNoteFiles: [],
|
||||
lastEditorScrollPercents: {},
|
||||
devToolsVisible: false,
|
||||
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: null,
|
||||
});
|
||||
|
||||
class Application extends BaseApplication {
|
||||
@@ -279,6 +283,31 @@ class Application extends BaseApplication {
|
||||
newState.devToolsVisible = action.value;
|
||||
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) {
|
||||
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||
@@ -286,10 +315,11 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
newState = resourceEditWatcherReducer(newState, action);
|
||||
newState = super.reducer(newState, action);
|
||||
|
||||
CommandService.instance().scheduleMapStateToProps(newState);
|
||||
|
||||
return super.reducer(newState, action);
|
||||
return newState;
|
||||
}
|
||||
|
||||
toggleDevTools(visible) {
|
||||
@@ -375,7 +405,7 @@ class Application extends BaseApplication {
|
||||
await this.updateMenu(screen);
|
||||
}
|
||||
|
||||
async updateMenu(screen) {
|
||||
async updateMenu(screen, updateStates = true) {
|
||||
if (this.lastMenuScreen_ === screen) return;
|
||||
|
||||
const cmdService = CommandService.instance();
|
||||
@@ -519,7 +549,7 @@ class Application extends BaseApplication {
|
||||
|
||||
const newNoteItem = cmdService.commandToMenuItem('newNote');
|
||||
const newTodoItem = cmdService.commandToMenuItem('newTodo');
|
||||
const newNotebookItem = cmdService.commandToMenuItem('newNotebook');
|
||||
const newFolderItem = cmdService.commandToMenuItem('newFolder');
|
||||
const printItem = cmdService.commandToMenuItem('print');
|
||||
|
||||
toolsItemsFirst.push(syncStatusItem, {
|
||||
@@ -650,7 +680,7 @@ class Application extends BaseApplication {
|
||||
},
|
||||
shim.isMac() ? noItem : newNoteItem,
|
||||
shim.isMac() ? noItem : newTodoItem,
|
||||
shim.isMac() ? noItem : newNotebookItem, {
|
||||
shim.isMac() ? noItem : newFolderItem, {
|
||||
type: 'separator',
|
||||
visible: shim.isMac() ? false : true,
|
||||
}, {
|
||||
@@ -699,7 +729,7 @@ class Application extends BaseApplication {
|
||||
submenu: [
|
||||
newNoteItem,
|
||||
newTodoItem,
|
||||
newNotebookItem, {
|
||||
newFolderItem, {
|
||||
label: _('Close Window'),
|
||||
platforms: ['darwin'],
|
||||
accelerator: shim.isMac() && keymapService.getAccelerator('closeWindow'),
|
||||
@@ -738,7 +768,6 @@ class Application extends BaseApplication {
|
||||
const separator = () => {
|
||||
return {
|
||||
type: 'separator',
|
||||
screens: ['Main'],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -986,6 +1015,8 @@ class Application extends BaseApplication {
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
this.lastMenuScreen_ = screen;
|
||||
|
||||
if (updateStates) await this.updateMenuItemStates();
|
||||
}
|
||||
|
||||
async updateMenuItemStates(state = null) {
|
||||
@@ -1095,7 +1126,7 @@ class Application extends BaseApplication {
|
||||
try {
|
||||
await keymapService.loadCustomKeymap(`${dir}/keymap-desktop.json`);
|
||||
} catch (err) {
|
||||
bridge().showErrorMessageBox(err.message);
|
||||
reg.logger().error(err.message);
|
||||
}
|
||||
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
@@ -1128,7 +1159,7 @@ class Application extends BaseApplication {
|
||||
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
|
||||
// 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);
|
||||
|
||||
// await populateDatabase(reg.db());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ interface Props {
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'startExternalEditing',
|
||||
label: () => _('Edit in external editor'),
|
||||
iconName: 'fa-share-square',
|
||||
iconName: 'icon-share',
|
||||
};
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
@@ -27,10 +27,14 @@ export const runtime = ():CommandRuntime => {
|
||||
// await comp.saveNoteAndWait(comp.formNote);
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
if (props.routeName !== 'Main') return false;
|
||||
return !!props.noteId;
|
||||
},
|
||||
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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@@ -18,10 +18,11 @@ export const runtime = ():CommandRuntime => {
|
||||
ExternalEditWatcher.instance().stopWatching(props.noteId);
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
if (props.routeName !== 'Main') return false;
|
||||
return !!props.noteId;
|
||||
},
|
||||
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 };
|
||||
},
|
||||
};
|
||||
};
|
||||
|
195
ElectronClient/gui/Button/Button.tsx
Normal file
195
ElectronClient/gui/Button/Button.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -40,10 +40,12 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const containerStyle = Object.assign({}, theme.containerStyle, {
|
||||
overflowY: 'scroll',
|
||||
padding: theme.configScreenPadding,
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
});
|
||||
|
||||
const buttonStyle = Object.assign({}, theme.buttonStyle, { marginRight: 10 });
|
||||
@@ -106,8 +108,8 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<div style={containerStyle}>
|
||||
<div style={{ padding: theme.margin }}>
|
||||
<p style={theme.textStyle}>{_('Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.')}</p>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<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.textStyle}>{_('Download and install the relevant extension for your browser:')}</p>
|
||||
<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 style={{ marginLeft: 10 }} theme={this.props.theme} type="chrome" url="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek"/>
|
||||
<ExtensionBadge themeId={this.props.themeId} type="firefox" url="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/"/>
|
||||
<ExtensionBadge style={{ marginLeft: 10 }} themeId={this.props.themeId} type="chrome" url="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -145,7 +147,7 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
clipperServer: state.clipperServer,
|
||||
clipperServerAutoStart: state.settings['clipperServer.autoStart'],
|
||||
apiToken: state.settings['api.token'],
|
||||
|
@@ -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;
|
52
ElectronClient/gui/ConfigScreen/ButtonBar.tsx
Normal file
52
ElectronClient/gui/ConfigScreen/ButtonBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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 Setting = require('lib/models/Setting.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
const pathUtils = require('lib/path-utils.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const shared = require('lib/components/shared/config-shared.js');
|
||||
const ConfigMenuBar = require('./ConfigMenuBar.min.js');
|
||||
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min');
|
||||
const { ClipperConfigScreen } = require('./ClipperConfigScreen.min');
|
||||
const { KeymapConfigScreen } = require('./KeymapConfig/KeymapConfigScreen');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min');
|
||||
const { ClipperConfigScreen } = require('../ClipperConfigScreen.min');
|
||||
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
|
||||
|
||||
class ConfigScreenComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
rowStyle_:any = null;
|
||||
|
||||
constructor(props:any) {
|
||||
super(props);
|
||||
|
||||
shared.init(this);
|
||||
|
||||
this.state.selectedSectionName = 'general';
|
||||
this.state.screenName = '';
|
||||
|
||||
this.checkSyncConfig_ = async () => {
|
||||
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.state = {
|
||||
selectedSectionName: 'general',
|
||||
screenName: '',
|
||||
changedSettingKeys: [],
|
||||
};
|
||||
|
||||
this.rowStyle_ = {
|
||||
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() {
|
||||
@@ -57,7 +72,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
sectionByName(name) {
|
||||
sectionByName(name:string) {
|
||||
const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings });
|
||||
for (const section of sections) {
|
||||
if (section.name === name) return section;
|
||||
@@ -66,15 +81,15 @@ class ConfigScreenComponent extends React.Component {
|
||||
throw new Error(`Invalid section name: ${name}`);
|
||||
}
|
||||
|
||||
screenFromName(screenName) {
|
||||
if (screenName === 'encryption') return <EncryptionConfigScreen theme={this.props.theme}/>;
|
||||
if (screenName === 'server') return <ClipperConfigScreen theme={this.props.theme}/>;
|
||||
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.theme}/>;
|
||||
screenFromName(screenName:string) {
|
||||
if (screenName === 'encryption') return <EncryptionConfigScreen themeId={this.props.themeId}/>;
|
||||
if (screenName === 'server') return <ClipperConfigScreen themeId={this.props.themeId}/>;
|
||||
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.themeId}/>;
|
||||
|
||||
throw new Error(`Invalid screen name: ${screenName}`);
|
||||
}
|
||||
|
||||
switchSection(name) {
|
||||
switchSection(name:string) {
|
||||
const section = this.sectionByName(name);
|
||||
let screenName = '';
|
||||
if (section.isScreen) {
|
||||
@@ -89,11 +104,11 @@ class ConfigScreenComponent extends React.Component {
|
||||
this.setState({ selectedSectionName: section.name, screenName: screenName });
|
||||
}
|
||||
|
||||
configMenuBar_selectionChange(event) {
|
||||
sideBar_selectionChange(event:any) {
|
||||
this.switchSection(event.section.name);
|
||||
}
|
||||
|
||||
keyValueToArray(kv) {
|
||||
keyValueToArray(kv:any) {
|
||||
const output = [];
|
||||
for (const k in kv) {
|
||||
if (!kv.hasOwnProperty(k)) continue;
|
||||
@@ -106,11 +121,11 @@ class ConfigScreenComponent extends React.Component {
|
||||
return output;
|
||||
}
|
||||
|
||||
renderSectionDescription(section) {
|
||||
renderSectionDescription(section:any) {
|
||||
const description = Setting.sectionDescription(section.name);
|
||||
if (!description) return null;
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
return (
|
||||
<div style={Object.assign({}, theme.textStyle, { marginBottom: 15 })}>
|
||||
{description}
|
||||
@@ -118,10 +133,10 @@ class ConfigScreenComponent extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
sectionToComponent(key, section, settings, selected) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
sectionToComponent(key:string, section:any, settings:any, selected:boolean) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const createSettingComponents = (advanced) => {
|
||||
const createSettingComponents = (advanced:boolean) => {
|
||||
const output = [];
|
||||
for (let i = 0; i < section.metadatas.length; i++) {
|
||||
const md = section.metadatas[i];
|
||||
@@ -135,9 +150,10 @@ class ConfigScreenComponent extends React.Component {
|
||||
const settingComps = createSettingComponents(false);
|
||||
const advancedSettingComps = createSettingComponents(true);
|
||||
|
||||
const sectionStyle = {
|
||||
const sectionStyle:any = {
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
maxWidth: 640,
|
||||
};
|
||||
|
||||
if (!selected) sectionStyle.display = 'none';
|
||||
@@ -161,9 +177,12 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
settingComps.push(
|
||||
<div key="check_sync_config_button" style={this.rowStyle_}>
|
||||
<button disabled={this.state.checkSyncConfigResult === 'checking'} style={theme.buttonStyle} onClick={this.checkSyncConfig_}>
|
||||
{_('Check synchronisation configuration')}
|
||||
</button>
|
||||
<Button
|
||||
title={_('Check synchronisation configuration')}
|
||||
level={ButtonLevel.Secondary}
|
||||
disabled={this.state.checkSyncConfigResult === 'checking'}
|
||||
onClick={this.checkSyncConfig_}
|
||||
/>
|
||||
{statusComp}
|
||||
</div>
|
||||
);
|
||||
@@ -204,9 +223,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
{showLogButton}
|
||||
|
||||
<button disabled={this.state.checkNextcloudAppResult === 'checking'} style={theme.buttonStyle} onClick={this.checkNextcloudAppButton_click}>
|
||||
{_('Check Status')}
|
||||
</button>
|
||||
<Button level={ButtonLevel.Secondary} style={{ display: 'inline-block' }} title={_('Check Status')} disabled={this.state.checkNextcloudAppResult === 'checking'} onClick={this.checkNextcloudAppButton_click}/>
|
||||
|
||||
<a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a>
|
||||
{statusComp}
|
||||
@@ -220,8 +237,17 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
if (advancedSettingComps.length) {
|
||||
const iconName = this.state.showAdvancedSettings ? 'fa fa-angle-down' : 'fa fa-angle-right';
|
||||
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>;
|
||||
// const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 });
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -235,35 +261,39 @@ class ConfigScreenComponent extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
settingToComponent(key, value) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
settingToComponent(key:string, value:any) {
|
||||
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, {
|
||||
display: 'inline-block',
|
||||
marginRight: 10,
|
||||
display: 'block',
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize * 1.083333,
|
||||
fontWeight: 500,
|
||||
marginBottom: theme.mainPadding / 4,
|
||||
});
|
||||
|
||||
const subLabel = Object.assign({}, labelStyle, {
|
||||
display: 'block',
|
||||
opacity: 0.7,
|
||||
marginBottom: Math.round(rowStyle.marginBottom * 0.7),
|
||||
});
|
||||
|
||||
const invisibleLabel = Object.assign({}, labelStyle, {
|
||||
opacity: 0,
|
||||
marginBottom: labelStyle.marginBottom,
|
||||
});
|
||||
|
||||
const checkboxLabelStyle = Object.assign({}, labelStyle, {
|
||||
marginLeft: 8,
|
||||
display: 'inline',
|
||||
backgroundColor: 'transparent',
|
||||
});
|
||||
|
||||
const controlStyle = {
|
||||
display: 'inline-block',
|
||||
color: theme.color,
|
||||
fontFamily: theme.fontFamily,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
|
||||
@@ -275,13 +305,19 @@ class ConfigScreenComponent extends React.Component {
|
||||
});
|
||||
|
||||
const textInputBaseStyle = Object.assign({}, controlStyle, {
|
||||
fontFamily: theme.fontFamily,
|
||||
border: '1px solid',
|
||||
padding: '4px 6px',
|
||||
borderColor: theme.dividerColor,
|
||||
borderRadius: 4,
|
||||
boxSizing: 'border-box',
|
||||
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);
|
||||
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 (
|
||||
<div key={key} style={rowStyle}>
|
||||
@@ -316,7 +359,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
<select
|
||||
value={value}
|
||||
style={selectStyle}
|
||||
onChange={event => {
|
||||
onChange={(event:any) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
}}
|
||||
>
|
||||
@@ -330,35 +373,38 @@ class ConfigScreenComponent extends React.Component {
|
||||
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.
|
||||
// There's probably a better way to do this but can't figure it out.
|
||||
|
||||
return (
|
||||
<div key={key + value.toString()} style={rowStyle}>
|
||||
<div style={controlStyle}>
|
||||
<div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
|
||||
<input
|
||||
id={`setting_checkbox_${key}`}
|
||||
type="checkbox"
|
||||
checked={!!value}
|
||||
onChange={event => {
|
||||
onCheckboxClick(event);
|
||||
onChange={() => {
|
||||
onCheckboxClick();
|
||||
}}
|
||||
style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }}
|
||||
/>
|
||||
<label
|
||||
onClick={event => {
|
||||
onCheckboxClick(event);
|
||||
onClick={() => {
|
||||
onCheckboxClick();
|
||||
}}
|
||||
style={checkboxLabelStyle}
|
||||
style={{ ...checkboxLabelStyle, marginLeft: 5, marginBottom: 0 }}
|
||||
htmlFor={`setting_checkbox_${key}`}
|
||||
>
|
||||
{md.label()}
|
||||
</label>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_STRING) {
|
||||
const inputStyle = Object.assign({}, textInputBaseStyle, {
|
||||
const inputStyle:any = Object.assign({}, textInputBaseStyle, {
|
||||
width: '50%',
|
||||
minWidth: '20em',
|
||||
});
|
||||
@@ -367,13 +413,13 @@ class ConfigScreenComponent extends React.Component {
|
||||
if (md.subType === 'file_path_and_args') {
|
||||
inputStyle.marginBottom = subLabel.marginBottom;
|
||||
|
||||
const splitCmd = cmdString => {
|
||||
const splitCmd = (cmdString:string) => {
|
||||
const path = pathUtils.extractExecutablePath(cmdString);
|
||||
const args = cmdString.substr(path.length + 1);
|
||||
return [pathUtils.unquotePath(path), args];
|
||||
};
|
||||
|
||||
const joinCmd = cmdArray => {
|
||||
const joinCmd = (cmdArray:string[]) => {
|
||||
if (!cmdArray[0] && !cmdArray[1]) return '';
|
||||
let cmdString = pathUtils.quotePath(cmdArray[0]);
|
||||
if (!cmdString) cmdString = '""';
|
||||
@@ -381,13 +427,13 @@ class ConfigScreenComponent extends React.Component {
|
||||
return cmdString;
|
||||
};
|
||||
|
||||
const onPathChange = event => {
|
||||
const onPathChange = (event:any) => {
|
||||
const cmd = splitCmd(this.state.settings[key]);
|
||||
cmd[0] = event.target.value;
|
||||
updateSettingValue(key, joinCmd(cmd));
|
||||
};
|
||||
|
||||
const onArgsChange = event => {
|
||||
const onArgsChange = (event:any) => {
|
||||
const cmd = splitCmd(this.state.settings[key]);
|
||||
cmd[1] = event.target.value;
|
||||
updateSettingValue(key, joinCmd(cmd));
|
||||
@@ -405,53 +451,51 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}>
|
||||
<label>{md.label()}</label>
|
||||
</div>
|
||||
<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={{ 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
|
||||
type={inputType}
|
||||
style={Object.assign({}, inputStyle, { marginBottom: 0 })}
|
||||
onChange={event => {
|
||||
onPathChange(event);
|
||||
style={inputStyle}
|
||||
onChange={(event:any) => {
|
||||
onArgsChange(event);
|
||||
}}
|
||||
value={cmd[0]}
|
||||
value={cmd[1]}
|
||||
/>
|
||||
<button onClick={browseButtonClick} style={Object.assign({}, theme.buttonStyle, { marginLeft: 5 })}>
|
||||
{_('Browse...')}
|
||||
</button>
|
||||
<div style={{ width: inputStyle.width }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type={inputType}
|
||||
style={inputStyle}
|
||||
onChange={event => {
|
||||
onArgsChange(event);
|
||||
}}
|
||||
value={cmd[1]}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
} else {
|
||||
const onTextChange = event => {
|
||||
const onTextChange = (event:any) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
};
|
||||
|
||||
@@ -464,23 +508,25 @@ class ConfigScreenComponent extends React.Component {
|
||||
type={inputType}
|
||||
style={inputStyle}
|
||||
value={this.state.settings[key]}
|
||||
onChange={event => {
|
||||
onChange={(event:any) => {
|
||||
onTextChange(event);
|
||||
}}
|
||||
/>
|
||||
{descriptionComp}
|
||||
<div style={{ width: inputStyle.width }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (md.type === Setting.TYPE_INT) {
|
||||
const onNumChange = event => {
|
||||
const onNumChange = (event:any) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
};
|
||||
|
||||
const label = [md.label()];
|
||||
if (md.unitLabel) label.push(`(${md.unitLabel()})`);
|
||||
|
||||
const inputStyle = Object.assign({}, textInputBaseStyle);
|
||||
const inputStyle:any = Object.assign({}, textInputBaseStyle);
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
@@ -491,7 +537,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
type="number"
|
||||
style={inputStyle}
|
||||
value={this.state.settings[key]}
|
||||
onChange={event => {
|
||||
onChange={(event:any) => {
|
||||
onNumChange(event);
|
||||
}}
|
||||
min={md.minimum}
|
||||
@@ -502,20 +548,12 @@ class ConfigScreenComponent extends React.Component {
|
||||
</div>
|
||||
);
|
||||
} 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 (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}>
|
||||
<label>{md.label()}</label>
|
||||
</div>
|
||||
<button style={buttonStyle} onClick={md.onClick}>
|
||||
{_('Edit')}
|
||||
</button>
|
||||
<Button level={ButtonLevel.Secondary} title={_('Edit')} onClick={md.onClick}/>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
@@ -544,46 +582,35 @@ class ConfigScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const style = Object.assign(
|
||||
{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
const style = Object.assign({},
|
||||
this.props.style,
|
||||
{
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
}
|
||||
);
|
||||
|
||||
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 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 buttonBarStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
borderTopWidth: 1,
|
||||
borderTopStyle: 'solid',
|
||||
borderTopColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
// screenComp is a custom config screen, such as the encryption config screen or keymap config screen.
|
||||
// These screens handle their own loading/saving of settings and have bespoke rendering.
|
||||
// When screenComp is null, it means we are viewing the regular settings.
|
||||
const screenComp = this.state.screenName ? <div style={{ overflow: 'scroll', flex: 1 }}>{this.screenFromName(this.state.screenName)}</div> : null;
|
||||
|
||||
if (screenComp) containerStyle.display = 'none';
|
||||
@@ -591,45 +618,35 @@ class ConfigScreenComponent extends React.Component {
|
||||
const sections = shared.settingsSections({ device: 'desktop', settings });
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<ConfigMenuBar
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<SideBar
|
||||
selection={this.state.selectedSectionName}
|
||||
onSelectionChange={this.configMenuBar_selectionChange}
|
||||
onSelectionChange={this.sideBar_selectionChange}
|
||||
sections={sections}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
{screenComp}
|
||||
<div style={containerStyle}>{settingComps}</div>
|
||||
<div style={buttonBarStyle}>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.onCancelClick();
|
||||
}}
|
||||
style={buttonStyle}
|
||||
>
|
||||
<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 style={style}>
|
||||
{screenComp}
|
||||
<div style={containerStyle}>{settingComps}</div>
|
||||
<ButtonBar
|
||||
hasChanges={hasChanges}
|
||||
backButtonTitle={hasChanges && !screenComp ? _('Cancel') : _('Back')}
|
||||
onCancelClick={this.onCancelClick}
|
||||
onSaveClick={screenComp ? null : this.onSaveClick}
|
||||
onApplyClick={screenComp ? null : this.onApplyClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state:any) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
settings: state.settings,
|
||||
locale: state.settings.locale,
|
||||
};
|
||||
};
|
||||
|
||||
const ConfigScreen = connect(mapStateToProps)(ConfigScreenComponent);
|
||||
export default connect(mapStateToProps)(ConfigScreenComponent);
|
||||
|
||||
module.exports = { ConfigScreen };
|
74
ElectronClient/gui/ConfigScreen/SideBar.tsx
Normal file
74
ElectronClient/gui/ConfigScreen/SideBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -3,7 +3,7 @@ const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
|
||||
function DialogButtonRow(props) {
|
||||
const theme = themeStyle(props.theme);
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const okButton_click = () => {
|
||||
if (props.onClick) props.onClick({ buttonName: 'ok' });
|
||||
|
@@ -1,16 +1,24 @@
|
||||
const React = require('react');
|
||||
import * as React from 'react';
|
||||
import ButtonBar from './ConfigScreen/ButtonBar';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
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 Shared = require('lib/components/shared/dropbox-login-shared');
|
||||
|
||||
class DropboxLoginScreenComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
interface Props {
|
||||
themeId: string,
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -19,19 +27,18 @@ class DropboxLoginScreenComponent extends React.Component {
|
||||
|
||||
render() {
|
||||
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, {
|
||||
padding: theme.margin,
|
||||
height: style.height - theme.headerHeight - theme.margin * 2,
|
||||
padding: theme.configScreenPadding,
|
||||
height: style.height - theme.margin * 2,
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
const inputStyle = Object.assign({}, theme.inputStyle, { width: 500 });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header style={headerStyle} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={containerStyle}>
|
||||
<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>
|
||||
@@ -46,17 +53,18 @@ class DropboxLoginScreenComponent extends React.Component {
|
||||
{_('Submit')}
|
||||
</button>
|
||||
</div>
|
||||
<ButtonBar
|
||||
onCancelClick={() => this.props.dispatch({ type: 'NAV_BACK' })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state:any) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
const DropboxLoginScreen = connect(mapStateToProps)(DropboxLoginScreenComponent);
|
||||
|
||||
module.exports = { DropboxLoginScreen };
|
||||
export default connect(mapStateToProps)(DropboxLoginScreenComponent);
|
@@ -35,7 +35,7 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
renderMasterKey(mk) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const passwordStyle = {
|
||||
color: theme.color,
|
||||
@@ -80,7 +80,7 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(this.props.masterKeys);
|
||||
if (!needUpgradeMasterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const rows = [];
|
||||
const comp = this;
|
||||
@@ -114,7 +114,7 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
renderReencryptData() {
|
||||
if (!shim.isElectron()) return null;
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
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.');
|
||||
@@ -139,13 +139,13 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const masterKeys = this.props.masterKeys;
|
||||
const containerPadding = 10;
|
||||
|
||||
const containerStyle = Object.assign({}, theme.containerStyle, {
|
||||
padding: containerPadding,
|
||||
padding: theme.configScreenPadding,
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
});
|
||||
|
||||
const mkComps = [];
|
||||
@@ -289,7 +289,7 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
masterKeys: state.masterKeys,
|
||||
passwords: state.settings['encryption.passwordCache'],
|
||||
encryptionEnabled: state.settings['encryption.enabled'],
|
||||
|
@@ -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 };
|
@@ -14,7 +14,7 @@ class HelpButtonComponent extends React.Component {
|
||||
}
|
||||
|
||||
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 helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 };
|
||||
const extraProps = {};
|
||||
@@ -29,7 +29,7 @@ class HelpButtonComponent extends React.Component {
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -4,7 +4,7 @@ const { themeStyle } = require('lib/theme');
|
||||
class IconButton extends React.Component {
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const iconStyle = {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize * 1.4,
|
||||
|
@@ -1,7 +1,6 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const { Header } = require('./Header/Header.min.js');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { filename, basename } = require('lib/path-utils.js');
|
||||
@@ -94,8 +93,7 @@ class ImportScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const messages = this.uniqueMessages();
|
||||
|
||||
const messagesStyle = {
|
||||
@@ -105,10 +103,6 @@ class ImportScreenComponent extends React.Component {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width,
|
||||
};
|
||||
|
||||
const messageComps = [];
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
messageComps.push(<div key={messages[i].key}>{messages[i].text}</div>);
|
||||
@@ -116,7 +110,6 @@ class ImportScreenComponent extends React.Component {
|
||||
|
||||
return (
|
||||
<div style={{}}>
|
||||
<Header style={headerStyle} />
|
||||
<div style={messagesStyle}>{messageComps}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -125,7 +118,7 @@ class ImportScreenComponent extends React.Component {
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -36,6 +36,10 @@ class ItemList extends React.Component {
|
||||
return this.listRef.current ? this.listRef.current.offsetTop : 0;
|
||||
}
|
||||
|
||||
offsetScroll() {
|
||||
return this.scrollTop_;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
@@ -5,7 +5,8 @@ export default function styles(themeId: number) {
|
||||
return {
|
||||
container: {
|
||||
...theme.containerStyle,
|
||||
padding: 16,
|
||||
padding: theme.configScreenPadding,
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
},
|
||||
actionsContainer: {
|
||||
display: 'flex',
|
||||
|
@@ -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 { Header } = require('../Header/Header.min.js');
|
||||
const { SideBar } = require('../SideBar/SideBar.min.js');
|
||||
const { NoteList } = require('../NoteList/NoteList.min.js');
|
||||
const NoteEditor = require('../NoteEditor/NoteEditor.js').default;
|
||||
const { SideBar } = require('../SideBar/SideBar.js');
|
||||
const { stateUtils } = require('lib/reducer.js');
|
||||
const { PromptDialog } = require('../PromptDialog.min.js');
|
||||
const NoteContentPropertiesDialog = require('../NoteContentPropertiesDialog.js').default;
|
||||
const NotePropertiesDialog = require('../NotePropertiesDialog.min.js');
|
||||
const ShareNoteDialog = require('../ShareNoteDialog.js').default;
|
||||
const InteropServiceHelper = require('../../InteropServiceHelper.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { shim } = require('lib/shim');
|
||||
const { themeStyle } = require('lib/theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const VerticalResizer = require('../VerticalResizer.min');
|
||||
const PluginManager = require('lib/services/PluginManager');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const CommandService = require('lib/services/CommandService').default;
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
@@ -28,7 +30,7 @@ const commands = [
|
||||
require('./commands/hideModalMessage'),
|
||||
require('./commands/moveToFolder'),
|
||||
require('./commands/newNote'),
|
||||
require('./commands/newNotebook'),
|
||||
require('./commands/newFolder'),
|
||||
require('./commands/newTodo'),
|
||||
require('./commands/print'),
|
||||
require('./commands/renameFolder'),
|
||||
@@ -40,14 +42,76 @@ const commands = [
|
||||
require('./commands/showNoteContentProperties'),
|
||||
require('./commands/showNoteProperties'),
|
||||
require('./commands/showShareNoteDialog'),
|
||||
require('./commands/toggleEditors'),
|
||||
require('./commands/toggleNoteList'),
|
||||
require('./commands/toggleSidebar'),
|
||||
require('./commands/toggleVisiblePanes'),
|
||||
];
|
||||
|
||||
class MainScreenComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
class MainScreenComponent extends React.Component<any, any> {
|
||||
|
||||
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 = {
|
||||
promptOptions: null,
|
||||
@@ -58,6 +122,7 @@ class MainScreenComponent extends React.Component {
|
||||
notePropertiesDialogOptions: {},
|
||||
noteContentPropertiesDialogOptions: {},
|
||||
shareNoteDialogOptions: {},
|
||||
layout: layout,
|
||||
};
|
||||
|
||||
this.registerCommands();
|
||||
@@ -70,6 +135,16 @@ class MainScreenComponent extends React.Component {
|
||||
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
|
||||
this.sidebar_onDrag = this.sidebar_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() {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
noteList_onDrag(event) {
|
||||
noteList_onDrag(event:any) {
|
||||
Setting.setValue('style.noteList.width', Setting.value('style.noteList.width') + event.deltaX);
|
||||
}
|
||||
|
||||
@@ -123,13 +198,13 @@ class MainScreenComponent extends React.Component {
|
||||
this.setState({ shareNoteDialogOptions: {} });
|
||||
}
|
||||
|
||||
commandService_commandsEnabledStateChange(event) {
|
||||
commandService_commandsEnabledStateChange(event:any) {
|
||||
const buttonCommandNames = [
|
||||
'toggleSidebar',
|
||||
'toggleNoteList',
|
||||
'newNote',
|
||||
'newTodo',
|
||||
'newNotebook',
|
||||
'newFolder',
|
||||
'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() {
|
||||
CommandService.instance().on('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange);
|
||||
this.updateRootLayoutSize();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
CommandService.instance().off('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange);
|
||||
this.unregisterCommands();
|
||||
|
||||
window.removeEventListener('resize', this.window_resize);
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
@@ -162,14 +285,14 @@ class MainScreenComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
async waitForNoteToSaved(noteId) {
|
||||
async waitForNoteToSaved(noteId:string) {
|
||||
while (noteId && this.props.editorNoteStatuses[noteId] === 'saving') {
|
||||
console.info('Waiting for note to be saved...', this.props.editorNoteStatuses);
|
||||
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
|
||||
if (this.isPrinting_) {
|
||||
console.info(`Printing ${options.path} to ${target} disallowed, already printing.`);
|
||||
@@ -208,7 +331,23 @@ class MainScreenComponent extends React.Component {
|
||||
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('_');
|
||||
if (styleKey === this.styleKey_) return this.styles_;
|
||||
|
||||
@@ -224,14 +363,16 @@ class MainScreenComponent extends React.Component {
|
||||
|
||||
this.styles_.messageBox = {
|
||||
width: width,
|
||||
height: 50,
|
||||
height: this.messageBoxHeight(),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 10,
|
||||
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 = {
|
||||
width: 5,
|
||||
@@ -241,6 +382,10 @@ class MainScreenComponent extends React.Component {
|
||||
display: 'inline-block',
|
||||
};
|
||||
|
||||
this.styles_.resizableLayout = {
|
||||
height: rowHeight,
|
||||
};
|
||||
|
||||
this.styles_.verticalResizerNotelist = Object.assign({}, this.styles_.verticalResizerSidebar);
|
||||
|
||||
this.styles_.sideBar = {
|
||||
@@ -295,7 +440,7 @@ class MainScreenComponent extends React.Component {
|
||||
return this.styles_;
|
||||
}
|
||||
|
||||
renderNotification(theme, styles) {
|
||||
renderNotification(theme:any, styles:any) {
|
||||
if (!this.messageBoxVisible()) return null;
|
||||
|
||||
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() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = Object.assign(
|
||||
{
|
||||
color: theme.color,
|
||||
@@ -411,48 +582,12 @@ class MainScreenComponent extends React.Component {
|
||||
this.props.style
|
||||
);
|
||||
const promptOptions = this.state.promptOptions;
|
||||
const notes = this.props.notes;
|
||||
const sidebarVisibility = this.props.sidebarVisibility;
|
||||
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 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',
|
||||
});
|
||||
const styles = this.styles(this.props.themeId, style.width, style.height, this.messageBoxVisible(), sidebarVisibility, noteListVisibility, this.props.sidebarWidth, this.props.noteListWidth);
|
||||
|
||||
if (!this.promptOnClose_) {
|
||||
this.promptOnClose_ = (answer, buttonType) => {
|
||||
this.promptOnClose_ = (answer:any, buttonType:any) => {
|
||||
return this.state.promptOptions.onClose(answer, buttonType);
|
||||
};
|
||||
}
|
||||
@@ -468,34 +603,33 @@ class MainScreenComponent extends React.Component {
|
||||
const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions;
|
||||
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
|
||||
|
||||
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<div style={modalLayerStyle}>{this.state.modalLayer.message}</div>
|
||||
|
||||
{noteContentPropertiesDialogOptions.visible && <NoteContentPropertiesDialog theme={this.props.theme} onClose={this.noteContentPropertiesDialog_close} text={noteContentPropertiesDialogOptions.text}/>}
|
||||
{notePropertiesDialogOptions.visible && <NotePropertiesDialog theme={this.props.theme} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
|
||||
{shareNoteDialogOptions.visible && <ShareNoteDialog theme={this.props.theme} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />}
|
||||
{noteContentPropertiesDialogOptions.visible && <NoteContentPropertiesDialog markupLanguage={noteContentPropertiesDialogOptions.markupLanguage} themeId={this.props.themeId} onClose={this.noteContentPropertiesDialog_close} text={noteContentPropertiesDialogOptions.text}/>}
|
||||
{notePropertiesDialogOptions.visible && <NotePropertiesDialog themeId={this.props.themeId} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
|
||||
{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}
|
||||
<SideBar style={styles.sideBar} />
|
||||
<VerticalResizer style={styles.verticalResizerSidebar} onDrag={this.sidebar_onDrag} />
|
||||
<NoteList style={styles.noteList} />
|
||||
<VerticalResizer style={styles.verticalResizerNotelist} onDrag={this.noteList_onDrag} />
|
||||
<NoteEditor bodyEditor={bodyEditor} style={styles.noteText} />
|
||||
<ResizableLayout
|
||||
width={this.state.width}
|
||||
height={styles.rowHeight}
|
||||
layout={this.state.layout}
|
||||
onResize={this.resizableLayout_resize}
|
||||
renderItem={this.resizableLayout_renderItem}
|
||||
/>
|
||||
{pluginDialog}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state:any) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
settingEditorCodeView: state.settings['editor.codeView'],
|
||||
sidebarVisibility: state.sidebarVisibility,
|
||||
noteListVisibility: state.noteListVisibility,
|
||||
@@ -516,9 +650,8 @@ const mapStateToProps = state => {
|
||||
customCss: state.customCss,
|
||||
editorNoteStatuses: state.editorNoteStatuses,
|
||||
hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state),
|
||||
focusedField: state.focusedField,
|
||||
};
|
||||
};
|
||||
|
||||
const MainScreen = connect(mapStateToProps)(MainScreenComponent);
|
||||
|
||||
module.exports = { MainScreen };
|
||||
export default connect(mapStateToProps)(MainScreenComponent);
|
@@ -8,7 +8,7 @@ const { time } = require('lib/time-utils');
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'editAlarm',
|
||||
label: () => _('Set alarm'),
|
||||
iconName: 'fa-clock',
|
||||
iconName: 'icon-alarm',
|
||||
};
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
|
@@ -4,7 +4,7 @@ const Folder = require('lib/models/Folder');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'newNotebook',
|
||||
name: 'newFolder',
|
||||
label: () => _('New notebook'),
|
||||
iconName: 'fa-book',
|
||||
};
|
@@ -1,18 +1,15 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
const Note = require('lib/models/Note');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
// const { _ } = require('lib/locale');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'search',
|
||||
iconName: 'icon-search',
|
||||
};
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ query, fuzzy }:any) => {
|
||||
console.info('RUNTIME', query);
|
||||
|
||||
execute: async ({ query }:any) => {
|
||||
if (!comp.searchId_) comp.searchId_ = uuid.create();
|
||||
|
||||
comp.props.dispatch({
|
||||
@@ -23,7 +20,6 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
query_pattern: query,
|
||||
query_folder_id: null,
|
||||
type_: BaseModel.TYPE_SEARCH,
|
||||
fuzzy: fuzzy,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,14 +29,18 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
id: comp.searchId_,
|
||||
});
|
||||
} else {
|
||||
const note = await Note.load(comp.props.selectedNoteId);
|
||||
if (note) {
|
||||
comp.props.dispatch({
|
||||
type: 'FOLDER_AND_NOTE_SELECT',
|
||||
folderId: note.parent_id,
|
||||
noteId: note.id,
|
||||
});
|
||||
}
|
||||
// Note: Normally there's no need to do anything when the search query
|
||||
// is cleared as the reducer should handle all state changes.
|
||||
// https://github.com/laurent22/joplin/issues/3748
|
||||
|
||||
// const note = await Note.load(comp.props.selectedNoteId);
|
||||
// if (note) {
|
||||
// comp.props.dispatch({
|
||||
// type: 'FOLDER_AND_NOTE_SELECT',
|
||||
// folderId: note.parent_id,
|
||||
// noteId: note.id,
|
||||
// });
|
||||
// }
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -5,7 +5,7 @@ const { _ } = require('lib/locale');
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'setTags',
|
||||
label: () => _('Tags'),
|
||||
iconName: 'fa-tags',
|
||||
iconName: 'icon-tags',
|
||||
};
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
|
@@ -4,7 +4,7 @@ const { _ } = require('lib/locale');
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'showNoteProperties',
|
||||
label: () => _('Note properties'),
|
||||
iconName: 'fa-info-circle',
|
||||
iconName: 'icon-info',
|
||||
};
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
|
32
ElectronClient/gui/MainScreen/commands/toggleEditors.ts
Normal file
32
ElectronClient/gui/MainScreen/commands/toggleEditors.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
@@ -4,7 +4,7 @@ const { _ } = require('lib/locale');
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'toggleVisiblePanes',
|
||||
label: () => _('Toggle editor layout'),
|
||||
iconName: 'fa-columns',
|
||||
iconName: 'icon-layout ',
|
||||
};
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
|
@@ -5,22 +5,21 @@ const { bridge } = require('electron').remote.require('./bridge');
|
||||
const NoteListUtils = require('./utils/NoteListUtils');
|
||||
|
||||
interface MultiNoteActionsProps {
|
||||
theme: number,
|
||||
themeId: number,
|
||||
selectedNoteIds: string[],
|
||||
notes: any[],
|
||||
dispatch: Function,
|
||||
watchedNoteFiles: string[],
|
||||
style: any,
|
||||
}
|
||||
|
||||
function styles_(props:MultiNoteActionsProps) {
|
||||
return buildStyle('MultiNoteActions', props.theme, (theme:any) => {
|
||||
return buildStyle('MultiNoteActions', props.themeId, (theme:any) => {
|
||||
return {
|
||||
root: {
|
||||
...props.style,
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'center',
|
||||
paddingTop: theme.marginTop,
|
||||
width: '100%',
|
||||
},
|
||||
itemList: {
|
||||
display: 'flex',
|
||||
|
@@ -7,7 +7,7 @@ const Countable = require('countable');
|
||||
const markupLanguageUtils = require('lib/markupLanguageUtils');
|
||||
|
||||
interface NoteContentPropertiesDialogProps {
|
||||
theme: number,
|
||||
themeId: number,
|
||||
text: string,
|
||||
markupLanguage: number,
|
||||
onClose: Function,
|
||||
@@ -46,7 +46,9 @@ function formatReadTime(readTimeMinutes: number) {
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
// For the source Markdown
|
||||
const [lines, setLines] = useState<number>(0);
|
||||
@@ -150,6 +152,8 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
const readTimeLabel = _('Read time: %s min', formatReadTime(strippedReadTime));
|
||||
|
||||
return (
|
||||
<div style={theme.dialogModalLayer}>
|
||||
<div style={theme.dialogBox}>
|
||||
@@ -162,10 +166,10 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
|
||||
{tableBodyComps}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={labelCompStyle}>
|
||||
{_('Read time: %s min', formatReadTime(strippedReadTime))}
|
||||
<div style={{ ...labelCompStyle, marginTop: 10 }}>
|
||||
{readTimeLabel}
|
||||
</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>
|
||||
);
|
||||
|
@@ -9,6 +9,7 @@ import { useScrollHandler, usePrevious, cursorPositionToTextOffset, useRootSize
|
||||
import Toolbar from './Toolbar';
|
||||
import styles_ from './styles';
|
||||
import { RenderedBody, defaultRenderedBody } from './utils/types';
|
||||
import NoteTextViewer from '../../../NoteTextViewer';
|
||||
import Editor from './Editor';
|
||||
|
||||
// @ts-ignore
|
||||
@@ -17,7 +18,6 @@ const { bridge } = require('electron').remote.require('./bridge');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { clipboard } = require('electron');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const NoteTextViewer = require('../../../NoteTextViewer.min');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
@@ -37,6 +37,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
|
||||
const [webviewReady, setWebviewReady] = useState(false);
|
||||
|
||||
const previousContent = usePrevious(props.content);
|
||||
const previousRenderedBody = usePrevious(renderedBody);
|
||||
const previousSearchMarkers = usePrevious(props.searchMarkers);
|
||||
const previousContentKey = usePrevious(props.contentKey);
|
||||
@@ -48,7 +49,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
props_onChangeRef.current = props.onChange;
|
||||
const contentKeyHasChangedRef = useRef(false);
|
||||
contentKeyHasChangedRef.current = previousContentKey !== props.contentKey;
|
||||
const theme = themeStyle(props.theme);
|
||||
|
||||
const rootSize = useRootSize({ rootRef });
|
||||
|
||||
@@ -351,6 +351,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
}, [styles.editor.codeMirrorTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const element = document.createElement('style');
|
||||
element.setAttribute('id', 'codemirrorStyle');
|
||||
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*/
|
||||
.CodeMirror {
|
||||
font-family: monospace;
|
||||
font-size: ${theme.editorFontSize}px;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
color: inherit !important;
|
||||
@@ -371,37 +374,37 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
/* be applied to the viewer. */
|
||||
padding-bottom: 400px !important;
|
||||
}
|
||||
|
||||
|
||||
.cm-header-1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
|
||||
.cm-header-2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
|
||||
.cm-header-3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
|
||||
.cm-header-4, .cm-header-5, .cm-header-6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
|
||||
.cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4, .cm-header-5, .cm-header-6 {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
|
||||
.cm-search-marker {
|
||||
background: ${theme.searchMarkerBackgroundColor};
|
||||
color: ${theme.searchMarkerColor} !important;
|
||||
}
|
||||
|
||||
|
||||
.cm-search-marker-selected {
|
||||
background: ${theme.selectedColor2};
|
||||
color: ${theme.color2} !important;
|
||||
}
|
||||
|
||||
|
||||
.cm-search-marker-scrollbar {
|
||||
background: ${theme.searchMarkerBackgroundColor};
|
||||
-moz-box-sizing: border-box;
|
||||
@@ -415,12 +418,33 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
background-color: inherit !important;
|
||||
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 () => {
|
||||
document.head.removeChild(element);
|
||||
};
|
||||
}, [props.theme]);
|
||||
}, [props.themeId]);
|
||||
|
||||
const webview_domReady = useCallback(() => {
|
||||
setWebviewReady(true);
|
||||
@@ -478,7 +502,18 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
}, [renderedBody, webviewReady]);
|
||||
|
||||
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);
|
||||
|
||||
if (editorRef.current) {
|
||||
@@ -487,7 +522,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
props.setLocalSearchResultCount(matches);
|
||||
}
|
||||
}
|
||||
}, [props.searchMarkers, props.setLocalSearchResultCount, renderedBody]);
|
||||
}, [props.searchMarkers, previousSearchMarkers, props.setLocalSearchResultCount, props.content, previousContent, renderedBody, previousRenderedBody, renderedBody]);
|
||||
|
||||
const cellEditorStyle = useMemo(() => {
|
||||
const output = { ...styles.cellEditor };
|
||||
@@ -539,17 +574,16 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
editorRef.current.refresh();
|
||||
}, [rootSize, styles.editor, props.visiblePanes]);
|
||||
|
||||
const editorReadOnly = props.visiblePanes.indexOf('editor') < 0;
|
||||
|
||||
function renderEditor() {
|
||||
|
||||
return (
|
||||
<div style={cellEditorStyle}>
|
||||
<Editor
|
||||
value={props.content}
|
||||
searchMarkers={props.searchMarkers}
|
||||
ref={editorRef}
|
||||
mode={props.contentMarkupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'xml' : 'joplin-markdown'}
|
||||
theme={styles.editor.codeMirrorTheme}
|
||||
codeMirrorTheme={styles.editor.codeMirrorTheme}
|
||||
style={styles.editor}
|
||||
readOnly={props.visiblePanes.indexOf('editor') < 0}
|
||||
autoMatchBraces={Setting.value('editor.autoMatchingBraces')}
|
||||
@@ -580,9 +614,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
<div style={styles.root} ref={rootRef}>
|
||||
<div style={styles.rowToolbar}>
|
||||
<Toolbar
|
||||
theme={props.theme}
|
||||
themeId={props.themeId}
|
||||
dispatch={props.dispatch}
|
||||
disabled={editorReadOnly}
|
||||
/>
|
||||
{props.noteToolbar}
|
||||
</div>
|
||||
|
@@ -17,18 +17,17 @@ import useCursorUtils from './utils/useCursorUtils';
|
||||
import useLineSorting from './utils/useLineSorting';
|
||||
import useEditorSearch from './utils/useEditorSearch';
|
||||
import useJoplinMode from './utils/useJoplinMode';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
|
||||
import 'codemirror/keymap/emacs';
|
||||
import 'codemirror/keymap/vim';
|
||||
import 'codemirror/keymap/sublime'; // Used for swapLineUp and swapLineDown
|
||||
|
||||
import 'codemirror/mode/meta';
|
||||
const { shim } = require('lib/shim.js');
|
||||
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
// Based on http://pypl.github.io/PYPL.html
|
||||
// +XML (HTML) +CSS and Markdown added
|
||||
const topLanguages = [
|
||||
'python',
|
||||
'clike',
|
||||
@@ -51,8 +50,16 @@ const topLanguages = [
|
||||
'haskell',
|
||||
'pascal',
|
||||
'css',
|
||||
'xml',
|
||||
|
||||
// Additional languages, not in the PYPL list
|
||||
'xml', // For HTML too
|
||||
'markdown',
|
||||
'yaml',
|
||||
'shell',
|
||||
'dockerfile',
|
||||
'diff',
|
||||
'erlang',
|
||||
'sql',
|
||||
];
|
||||
// Load Top Modes
|
||||
for (let i = 0; i < topLanguages.length; i++) {
|
||||
@@ -67,9 +74,10 @@ for (let i = 0; i < topLanguages.length; i++) {
|
||||
|
||||
export interface EditorProps {
|
||||
value: string,
|
||||
searchMarkers: any,
|
||||
mode: string,
|
||||
style: any,
|
||||
theme: any,
|
||||
codeMirrorTheme: any,
|
||||
readOnly: boolean,
|
||||
autoMatchBraces: boolean,
|
||||
keyMap: string,
|
||||
@@ -91,6 +99,7 @@ function Editor(props: EditorProps, ref: any) {
|
||||
useLineSorting(CodeMirror);
|
||||
useEditorSearch(CodeMirror);
|
||||
useJoplinMode(CodeMirror);
|
||||
useKeymap(CodeMirror);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return editor;
|
||||
@@ -133,94 +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',
|
||||
};
|
||||
// Add some of the Joplin smart list handling to emacs mode
|
||||
CodeMirror.keyMap.emacs['Tab'] = 'smartListIndent';
|
||||
CodeMirror.keyMap.emacs['Enter'] = 'insertListElement';
|
||||
CodeMirror.keyMap.emacs['Shift-Tab'] = 'smartListUnindent';
|
||||
|
||||
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(() => {
|
||||
if (!editorParent.current) return () => {};
|
||||
|
||||
const cmOptions = {
|
||||
value: props.value,
|
||||
screenReaderLabel: props.value,
|
||||
theme: props.theme,
|
||||
theme: props.codeMirrorTheme,
|
||||
mode: props.mode,
|
||||
readOnly: props.readOnly,
|
||||
autoCloseBrackets: props.autoMatchBraces,
|
||||
@@ -242,6 +170,11 @@ function Editor(props: EditorProps, ref: any) {
|
||||
cm.on('drop', editor_drop);
|
||||
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 () => {
|
||||
// Clean up codemirror
|
||||
cm.off('change', editor_change);
|
||||
@@ -269,9 +202,9 @@ function Editor(props: EditorProps, ref: any) {
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
editor.setOption('theme', props.theme);
|
||||
editor.setOption('theme', props.codeMirrorTheme);
|
||||
}
|
||||
}, [props.theme]);
|
||||
}, [props.codeMirrorTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
|
@@ -1,23 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import CommandService from '../../../../lib/services/CommandService';
|
||||
|
||||
const ToolbarBase = require('../../../Toolbar.min.js');
|
||||
const { buildStyle, themeStyle } = require('lib/theme');
|
||||
import CommandService from 'lib/services/CommandService';
|
||||
import ToolbarBase from '../../../ToolbarBase';
|
||||
const { buildStyle } = require('lib/theme');
|
||||
|
||||
interface ToolbarProps {
|
||||
theme: number,
|
||||
themeId: number,
|
||||
dispatch: Function,
|
||||
disabled: boolean,
|
||||
}
|
||||
|
||||
function styles_(props:ToolbarProps) {
|
||||
return buildStyle('CodeMirrorToolbar', props.theme, (/* theme:any*/) => {
|
||||
const theme = themeStyle(props.theme);
|
||||
return buildStyle('CodeMirrorToolbar', props.themeId, () => {
|
||||
return {
|
||||
root: {
|
||||
flex: 1,
|
||||
marginBottom: 0,
|
||||
borderTop: `1px solid ${theme.dividerColor}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -29,6 +25,11 @@ export default function Toolbar(props:ToolbarProps) {
|
||||
const cmdService = CommandService.instance();
|
||||
|
||||
const toolbarItems = [
|
||||
cmdService.commandToToolbarButton('historyBackward'),
|
||||
cmdService.commandToToolbarButton('historyForward'),
|
||||
cmdService.commandToToolbarButton('startExternalEditing'),
|
||||
|
||||
{ type: 'separator' },
|
||||
cmdService.commandToToolbarButton('textBold'),
|
||||
cmdService.commandToToolbarButton('textItalic'),
|
||||
{ type: 'separator' },
|
||||
@@ -42,7 +43,9 @@ export default function Toolbar(props:ToolbarProps) {
|
||||
cmdService.commandToToolbarButton('textHeading'),
|
||||
cmdService.commandToToolbarButton('textHorizontalRule'),
|
||||
cmdService.commandToToolbarButton('insertDateTime'),
|
||||
|
||||
cmdService.commandToToolbarButton('toggleEditors'),
|
||||
];
|
||||
|
||||
return <ToolbarBase disabled={props.disabled} style={styles.root} items={toolbarItems} />;
|
||||
return <ToolbarBase style={styles.root} items={toolbarItems} />;
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { NoteBodyEditorProps } from '../../../utils/types';
|
||||
const { buildStyle } = require('lib/theme');
|
||||
|
||||
export default function styles(props: NoteBodyEditorProps) {
|
||||
return buildStyle('CodeMirror', props.theme, (theme: any) => {
|
||||
return buildStyle('CodeMirror', props.themeId, (theme: any) => {
|
||||
return {
|
||||
root: {
|
||||
position: 'relative',
|
||||
|
@@ -119,7 +119,8 @@ export default function useEditorSearch(CodeMirror: any) {
|
||||
// We only want to highlight all matches when there is only 1 search term
|
||||
if (keywords.length !== 1 || keywords[0].value == '') {
|
||||
clearOverlay(this);
|
||||
setPreviousKeywordValue('');
|
||||
const prev = keywords.length > 1 ? keywords[0].value : '';
|
||||
setPreviousKeywordValue(prev);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,8 @@ export default function useJoplinMode(CodeMirror: any) {
|
||||
|
||||
const inlineKatexOpenRE = /(?<!\S)\$(?=[^\s$].*?[^\\\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
|
||||
// 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 closing = false;
|
||||
|
||||
const blockPos = findToken(stream, blockKatexRE);
|
||||
|
||||
if (state.openCharacter) {
|
||||
currentMode = stex;
|
||||
currentState = state.inner;
|
||||
tokenLabel = 'katex-marker-close';
|
||||
closing = true;
|
||||
|
||||
const blockPos = findToken(stream, blockKatexCloseRE);
|
||||
const inlinePos = findToken(stream, inlineKatexCloseRE);
|
||||
|
||||
if (state.openCharacter === '$$' && blockPos !== -1) nextTokenPos = blockPos;
|
||||
if (state.openCharacter === '$' && inlinePos !== -1) nextTokenPos = inlinePos;
|
||||
} else {
|
||||
} else if (!currentState.code) {
|
||||
const blockPos = findToken(stream, blockKatexOpenRE);
|
||||
const inlinePos = findToken(stream, inlineKatexOpenRE);
|
||||
|
||||
if (blockPos !== -1) nextTokenPos = blockPos;
|
||||
|
@@ -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();
|
||||
}, []);
|
||||
}
|
@@ -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) {
|
||||
if (cm.getOption('disableInput')) return CodeMirror.Pass;
|
||||
|
||||
|
@@ -3,15 +3,18 @@ import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHand
|
||||
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||
import { resourcesStatus, commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
|
||||
import useScroll from './utils/useScroll';
|
||||
import styles_ from './styles';
|
||||
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 taboverride = require('taboverride');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { _, closestSupportedLocale } = require('lib/locale');
|
||||
const BaseItem = require('lib/models/BaseItem');
|
||||
const Resource = require('lib/models/Resource');
|
||||
const { themeStyle, buildStyle } = require('lib/theme');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
const { clipboard } = require('electron');
|
||||
const supportedLocales = require('./supportedLocales');
|
||||
|
||||
@@ -112,31 +115,6 @@ const joplinCommandToTinyMceCommands:JoplinCommandToTinyMceCommands = {
|
||||
'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 loadedJsFiles_:string[] = [];
|
||||
let dispatchDidUpdateIID_:any = null;
|
||||
@@ -170,7 +148,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
editorRef.current = editor;
|
||||
|
||||
const styles = styles_(props);
|
||||
const theme = themeStyle(props.theme);
|
||||
// const theme = themeStyle(props.themeId);
|
||||
|
||||
const { scrollToPercent } = useScroll({ editor, onScroll: props.onScroll });
|
||||
|
||||
@@ -368,10 +346,17 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
useEffect(() => {
|
||||
if (!editorReady) return () => {};
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const element = document.createElement('style');
|
||||
element.setAttribute('id', 'tinyMceStyle');
|
||||
document.head.appendChild(element);
|
||||
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__overflow,
|
||||
.tox .tox-toolbar__primary,
|
||||
@@ -388,8 +373,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
}
|
||||
|
||||
.tox .tox-editor-header {
|
||||
border-top: 1px solid ${theme.dividerColor};
|
||||
border-bottom: 1px solid ${theme.dividerColor};
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tox .tox-tbtn,
|
||||
@@ -401,8 +385,8 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
.tox input,
|
||||
.tox .tox-label,
|
||||
.tox .tox-toolbar-label {
|
||||
color: ${theme.iconColor} !important;
|
||||
fill: ${theme.iconColor} !important;
|
||||
color: ${theme.color3} !important;
|
||||
fill: ${theme.color3} !important;
|
||||
}
|
||||
|
||||
.tox .tox-statusbar a,
|
||||
@@ -424,32 +408,59 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
}
|
||||
|
||||
.tox .tox-tbtn:hover {
|
||||
background-color: ${theme.backgroundHover};
|
||||
color: ${theme.colorHover};
|
||||
fill: ${theme.colorHover};
|
||||
color: ${theme.colorHover3} !important;
|
||||
fill: ${theme.colorHover3} !important;
|
||||
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__overflow {
|
||||
background: none;
|
||||
background-color: ${theme.backgroundColor3} !important;
|
||||
}
|
||||
|
||||
.tox-tinymce,
|
||||
.tox .tox-toolbar__group,
|
||||
.tox.tox-tinymce-aux .tox-toolbar__overflow,
|
||||
.tox .tox-dialog__footer {
|
||||
border-color: ${theme.dividerColor} !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.tox-tinymce {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.joplin-tinymce .tox-toolbar__group {
|
||||
background-color: ${theme.backgroundColor3};
|
||||
padding-top: ${theme.toolbarPadding}px;
|
||||
padding-bottom: ${theme.toolbarPadding}px;
|
||||
}
|
||||
`));
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(element);
|
||||
};
|
||||
}, [editorReady, props.theme]);
|
||||
}, [editorReady, props.themeId]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Enable or disable the editor
|
||||
@@ -499,6 +510,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
menubar: false,
|
||||
relative_urls: false,
|
||||
branding: false,
|
||||
statusbar: false,
|
||||
target_list: false,
|
||||
table_resize_bars: false,
|
||||
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
|
||||
// code would check this and either append the CSS or replace.
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
let docHead_:any = null;
|
||||
|
||||
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
|
||||
// as it is quite complex and probably rarely used.
|
||||
function renderDisabledOverlay() {
|
||||
const status = resourcesStatus(props.resourceInfos);
|
||||
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 statusComp = draggingStarted ? null : <p style={theme.textStyleMinor}>{`Status: ${status}`}</p>;
|
||||
return (
|
||||
@@ -1056,8 +1122,10 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.rootStyle}>
|
||||
<div style={styles.rootStyle} className="joplin-tinymce">
|
||||
{renderDisabledOverlay()}
|
||||
{renderLeftExtraToolbarButtons()}
|
||||
{renderRightExtraToolbarButtons()}
|
||||
<div style={{ width: '100%', height: '100%' }} id={rootIdRef.current}/>
|
||||
</div>
|
||||
);
|
||||
|
@@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
@@ -13,13 +13,18 @@ import useMessageHandler from './utils/useMessageHandler';
|
||||
import useWindowCommandHandler from './utils/useWindowCommandHandler';
|
||||
import useDropHandler from './utils/useDropHandler';
|
||||
import useMarkupToHtml from './utils/useMarkupToHtml';
|
||||
import useNoteToolbarButtons from './utils/useNoteToolbarButtons';
|
||||
import useFormNote, { OnLoadEvent } from './utils/useFormNote';
|
||||
import useFolder from './utils/useFolder';
|
||||
import styles_ from './styles';
|
||||
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types';
|
||||
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 { substrWithEllipsis } = require('lib/string-utils');
|
||||
const NoteSearchBar = require('../NoteSearchBar.min.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
@@ -70,6 +75,8 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
const formNoteRef = useRef<FormNote>();
|
||||
formNoteRef.current = { ...formNote };
|
||||
|
||||
const formNoteFolder = useFolder({ folderId: formNote.parent_id });
|
||||
|
||||
const {
|
||||
localSearch,
|
||||
onChange: localSearch_change,
|
||||
@@ -133,17 +140,17 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
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 theme = themeStyle(props.theme);
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const markupToHtml = markupLanguageUtils.newMarkupToHtml({
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
});
|
||||
|
||||
return markupToHtml.allAssets(markupLanguage, theme);
|
||||
}, [props.theme]);
|
||||
}, [props.themeId]);
|
||||
|
||||
const handleProvisionalFlag = useCallback(() => {
|
||||
if (props.isProvisional) {
|
||||
@@ -336,20 +343,36 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
};
|
||||
|
||||
return <NoteToolbar
|
||||
theme={props.theme}
|
||||
themeId={props.themeId}
|
||||
note={formNote}
|
||||
style={toolbarStyle}
|
||||
/>;
|
||||
}
|
||||
|
||||
function renderTagButton() {
|
||||
const info = CommandService.instance().commandToToolbarButton('setTags');
|
||||
return <ToolbarButton
|
||||
themeId={props.themeId}
|
||||
toolbarButtonInfo={info}
|
||||
/>;
|
||||
}
|
||||
|
||||
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() {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const titleBarDate = <span style={styles.titleDate}>{time.formatMsToLocal(formNote.user_updated_time)}</span>;
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', height: theme.topRowHeight }}>
|
||||
<input
|
||||
type="text"
|
||||
ref={titleInputRef}
|
||||
@@ -360,6 +383,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
value={formNote.title}
|
||||
/>
|
||||
{titleBarDate}
|
||||
{renderNoteToolbar()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -381,7 +405,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
markupToHtml: markupToHtml,
|
||||
allAssets: allAssets,
|
||||
disabled: false,
|
||||
theme: props.theme,
|
||||
themeId: props.themeId,
|
||||
dispatch: props.dispatch,
|
||||
noteToolbar: null,// renderNoteToolbar(),
|
||||
onScroll: onScroll,
|
||||
@@ -391,6 +415,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
keyboardMode: Setting.value('editor.keyboardMode'),
|
||||
locale: Setting.value('locale'),
|
||||
onDrop: onDrop,
|
||||
noteToolbarButtonInfos: useNoteToolbarButtons(),
|
||||
};
|
||||
|
||||
let editor = null;
|
||||
@@ -414,10 +439,10 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
}, []);
|
||||
|
||||
if (showRevisions) {
|
||||
const theme = themeStyle(props.theme);
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const revStyle = {
|
||||
...props.style,
|
||||
const revStyle:any = {
|
||||
// ...props.style,
|
||||
display: 'inline-flex',
|
||||
padding: theme.margin,
|
||||
verticalAlign: 'top',
|
||||
@@ -433,19 +458,18 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
if (props.selectedNoteIds.length > 1) {
|
||||
return <MultiNoteActions
|
||||
theme={props.theme}
|
||||
themeId={props.themeId}
|
||||
selectedNoteIds={props.selectedNoteIds}
|
||||
notes={props.notes}
|
||||
dispatch={props.dispatch}
|
||||
watchedNoteFiles={props.watchedNoteFiles}
|
||||
style={props.style}
|
||||
/>;
|
||||
}
|
||||
|
||||
function renderSearchBar() {
|
||||
if (!showLocalSearch) return false;
|
||||
|
||||
const theme = themeStyle(props.theme);
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
return (
|
||||
<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) {
|
||||
return renderNoNotes(styles.root);
|
||||
}
|
||||
@@ -488,15 +536,17 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
{renderResourceWatchingNotification()}
|
||||
{renderTitleBar()}
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
{renderNoteToolbar()}{renderTagBar()}
|
||||
</div>
|
||||
{renderSearchInfo()}
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
{editor}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
{renderSearchBar()}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', height: 40 }}>
|
||||
{renderTagButton()}
|
||||
{renderTagBar()}
|
||||
</div>
|
||||
{wysiwygBanner}
|
||||
</div>
|
||||
</div>
|
||||
@@ -518,7 +568,7 @@ const mapStateToProps = (state: any) => {
|
||||
isProvisional: state.provisionalNoteIds.includes(noteId),
|
||||
editorNoteStatuses: state.editorNoteStatuses,
|
||||
syncStarted: state.syncStarted,
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
notesParentType: state.notesParentType,
|
||||
selectedNoteTags: state.selectedNoteTags,
|
||||
|
@@ -28,57 +28,57 @@ const declarations:CommandDeclaration[] = [
|
||||
{
|
||||
name: 'textBold',
|
||||
label: () => _('Bold'),
|
||||
iconName: 'fa-bold',
|
||||
iconName: 'icon-bold',
|
||||
},
|
||||
{
|
||||
name: 'textItalic',
|
||||
label: () => _('Italic'),
|
||||
iconName: 'fa-italic',
|
||||
iconName: 'icon-italic',
|
||||
},
|
||||
{
|
||||
name: 'textLink',
|
||||
label: () => _('Hyperlink'),
|
||||
iconName: 'fa-link',
|
||||
iconName: 'icon-link',
|
||||
},
|
||||
{
|
||||
name: 'textCode',
|
||||
label: () => _('Code'),
|
||||
iconName: 'fa-code',
|
||||
iconName: 'icon-code',
|
||||
},
|
||||
{
|
||||
name: 'attachFile',
|
||||
label: () => _('Attach file'),
|
||||
iconName: 'fa-paperclip',
|
||||
iconName: 'icon-attachment',
|
||||
},
|
||||
{
|
||||
name: 'textNumberedList',
|
||||
label: () => _('Numbered List'),
|
||||
iconName: 'fa-list-ol',
|
||||
iconName: 'icon-numbered-list',
|
||||
},
|
||||
{
|
||||
name: 'textBulletedList',
|
||||
label: () => _('Bulleted List'),
|
||||
iconName: 'fa-list-ul',
|
||||
iconName: 'icon-bulleted-list',
|
||||
},
|
||||
{
|
||||
name: 'textCheckbox',
|
||||
label: () => _('Checkbox'),
|
||||
iconName: 'fa-check-square',
|
||||
iconName: 'icon-to-do-list',
|
||||
},
|
||||
{
|
||||
name: 'textHeading',
|
||||
label: () => _('Heading'),
|
||||
iconName: 'fa-heading',
|
||||
iconName: 'icon-heading',
|
||||
},
|
||||
{
|
||||
name: 'textHorizontalRule',
|
||||
label: () => _('Horizontal Rule'),
|
||||
iconName: 'fa-ellipsis-h',
|
||||
iconName: 'fas fa-ellipsis-h',
|
||||
},
|
||||
{
|
||||
name: 'insertDateTime',
|
||||
label: () => _('Insert Date Time'),
|
||||
iconName: 'fa-calendar-plus',
|
||||
iconName: 'icon-add-date',
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -3,16 +3,18 @@ import { NoteEditorProps } from '../utils/types';
|
||||
const { buildStyle } = require('lib/theme');
|
||||
|
||||
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 {
|
||||
root: {
|
||||
...props.style,
|
||||
// ...props.style,
|
||||
boxSizing: 'border-box',
|
||||
paddingLeft: 10,
|
||||
paddingTop: 5,
|
||||
paddingLeft: theme.mainPadding,
|
||||
paddingTop: 0,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: theme.dividerColor,
|
||||
borderLeftStyle: 'solid',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
titleInput: {
|
||||
flex: 1,
|
||||
@@ -20,16 +22,15 @@ export default function styles(props: NoteEditorProps) {
|
||||
paddingTop: 5,
|
||||
minHeight: 35,
|
||||
boxSizing: 'border-box',
|
||||
fontWeight: 'bold',
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 8,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 8,
|
||||
marginLeft: 5,
|
||||
// marginRight: theme.paddingLeft,
|
||||
color: theme.textStyle.color,
|
||||
fontSize: theme.textStyle.fontSize * 1.25,
|
||||
fontSize: Math.round(theme.textStyle.fontSize * 1.5),
|
||||
backgroundColor: theme.backgroundColor,
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
border: 'none',
|
||||
},
|
||||
warningBanner: {
|
||||
background: theme.warningBackgroundColor,
|
||||
|
@@ -1,10 +1,15 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import AsyncActionQueue from '../../../lib/AsyncActionQueue';
|
||||
import { ToolbarButtonInfo } from 'lib/services/CommandService';
|
||||
|
||||
export interface ToolbarButtonInfos {
|
||||
[key:string]: ToolbarButtonInfo;
|
||||
}
|
||||
|
||||
export interface NoteEditorProps {
|
||||
style: any;
|
||||
// style: any;
|
||||
noteId: string;
|
||||
theme: number;
|
||||
themeId: number;
|
||||
dispatch: Function;
|
||||
selectedNoteIds: string[];
|
||||
notes: any[];
|
||||
@@ -29,7 +34,7 @@ export interface NoteEditorProps {
|
||||
export interface NoteBodyEditorProps {
|
||||
style: any;
|
||||
ref: any,
|
||||
theme: number;
|
||||
themeId: number;
|
||||
content: string,
|
||||
contentKey: string,
|
||||
contentMarkupLanguage: number,
|
||||
@@ -51,6 +56,7 @@ export interface NoteBodyEditorProps {
|
||||
resourceInfos: ResourceInfos,
|
||||
locale: string,
|
||||
onDrop: Function,
|
||||
noteToolbarButtonInfos: ToolbarButtonInfos,
|
||||
}
|
||||
|
||||
export interface FormNote {
|
||||
|
29
ElectronClient/gui/NoteEditor/utils/useFolder.ts
Normal file
29
ElectronClient/gui/NoteEditor/utils/useFolder.ts
Normal 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;
|
||||
}
|
33
ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.ts
Normal file
33
ElectronClient/gui/NoteEditor/utils/useNoteToolbarButtons.ts
Normal 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;
|
||||
}
|
@@ -44,6 +44,7 @@ function editorCommandRuntime(declaration:CommandDeclaration, editorRef:any):Com
|
||||
}
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
if (props.routeName !== 'Main' || props.isDialogVisible) return false;
|
||||
if (props.markdownEditorViewerOnly) return false;
|
||||
if (!props.noteId) return false;
|
||||
const note = BaseModel.byId(props.notes, props.noteId);
|
||||
@@ -58,6 +59,8 @@ function editorCommandRuntime(declaration:CommandDeclaration, editorRef:any):Com
|
||||
noteVisiblePanes: state.noteVisiblePanes,
|
||||
notes: state.notes,
|
||||
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||
routeName: state.route.routeName,
|
||||
isDialogVisible: !!Object.keys(state.visibleDialogs).length,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
@@ -12,11 +12,18 @@ const Setting = require('lib/models/Setting');
|
||||
const NoteListUtils = require('../utils/NoteListUtils');
|
||||
const NoteListItem = require('../NoteListItem').default;
|
||||
const CommandService = require('lib/services/CommandService.js').default;
|
||||
const styled = require('styled-components').default;
|
||||
|
||||
const commands = [
|
||||
require('./commands/focusElementNoteList'),
|
||||
];
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: ${(props:any) => props.theme.backgroundColor3};
|
||||
`;
|
||||
|
||||
class NoteListComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -27,12 +34,15 @@ class NoteListComponent extends React.Component {
|
||||
|
||||
this.state = {
|
||||
dragOverTargetNoteIndex: null,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
this.noteListRef = React.createRef();
|
||||
this.itemListRef = React.createRef();
|
||||
this.itemAnchorRefs_ = {};
|
||||
|
||||
this.itemRenderer = this.itemRenderer.bind(this);
|
||||
this.renderItem = this.renderItem.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.noteItem_titleClick = this.noteItem_titleClick.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.unregisterGlobalDragEndEvent_ = this.unregisterGlobalDragEndEvent_.bind(this);
|
||||
this.itemContextMenu = this.itemContextMenu.bind(this);
|
||||
this.resizableLayout_resize = this.resizableLayout_resize.bind(this);
|
||||
}
|
||||
|
||||
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 = {
|
||||
root: {
|
||||
@@ -85,12 +96,12 @@ class NoteListComponent extends React.Component {
|
||||
};
|
||||
|
||||
this.styleCache_ = {};
|
||||
this.styleCache_[this.props.theme] = style;
|
||||
this.styleCache_[this.props.themeId] = style;
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
itemContextMenu(event) {
|
||||
itemContextMenu(event:any) {
|
||||
const currentItemId = event.currentTarget.getAttribute('data-id');
|
||||
if (!currentItemId) return;
|
||||
|
||||
@@ -128,11 +139,11 @@ class NoteListComponent extends React.Component {
|
||||
document.removeEventListener('dragend', this.onGlobalDrop_);
|
||||
}
|
||||
|
||||
dragTargetNoteIndex_(event) {
|
||||
return Math.abs(Math.round((event.clientY - this.itemListRef.current.offsetTop()) / this.itemHeight));
|
||||
dragTargetNoteIndex_(event:any) {
|
||||
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;
|
||||
|
||||
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.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 newNote = {
|
||||
id: item.id,
|
||||
@@ -182,7 +193,7 @@ class NoteListComponent extends React.Component {
|
||||
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) {
|
||||
event.preventDefault();
|
||||
this.props.dispatch({
|
||||
@@ -203,7 +214,7 @@ class NoteListComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
noteItem_dragStart(event) {
|
||||
noteItem_dragStart(event:any) {
|
||||
let noteIds = [];
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
itemRenderer(item, index) {
|
||||
renderItem(item:any, index:number) {
|
||||
const highlightedWords = () => {
|
||||
if (this.props.notesParentType === 'Search') {
|
||||
const query = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
|
||||
@@ -240,11 +251,12 @@ class NoteListComponent extends React.Component {
|
||||
return <NoteListItem
|
||||
ref={ref}
|
||||
key={item.id}
|
||||
style={this.style(this.props.theme)}
|
||||
style={this.style()}
|
||||
item={item}
|
||||
index={index}
|
||||
theme={this.props.theme}
|
||||
width={this.props.style.width}
|
||||
themeId={this.props.themeId}
|
||||
width={this.state.width}
|
||||
height={this.itemHeight}
|
||||
dragItemIndex={this.state.dragOverTargetNoteIndex}
|
||||
highlightedWords={highlightedWords()}
|
||||
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;
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps:any) {
|
||||
if (prevProps.selectedNoteIds !== this.props.selectedNoteIds && this.props.selectedNoteIds.length === 1) {
|
||||
const id = this.props.selectedNoteIds[0];
|
||||
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) {
|
||||
// Page Up
|
||||
@@ -314,7 +330,7 @@ class NoteListComponent extends React.Component {
|
||||
return noteIndex;
|
||||
}
|
||||
|
||||
async onKeyDown(event) {
|
||||
async onKeyDown(event:any) {
|
||||
const keyCode = event.keyCode;
|
||||
const noteIds = this.props.selectedNoteIds;
|
||||
|
||||
@@ -350,7 +366,7 @@ class NoteListComponent extends React.Component {
|
||||
event.preventDefault();
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
// list is scrolled and items within it are being rebuilt.
|
||||
// - 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() {
|
||||
if (this.focusItemIID_) {
|
||||
clearInterval(this.focusItemIID_);
|
||||
this.focusItemIID_ = null;
|
||||
}
|
||||
|
||||
this.props.resizableLayoutEventEmitter.off('resize', this.resizableLayout_resize);
|
||||
|
||||
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() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
if (!this.props.size) throw new Error('props.size is required');
|
||||
|
||||
if (!this.props.notes.length) {
|
||||
const padding = 10;
|
||||
const emptyDivStyle = Object.assign(
|
||||
{
|
||||
padding: `${padding}px`,
|
||||
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}
|
||||
/>;
|
||||
return (
|
||||
<StyledRoot ref={this.noteListRef}>
|
||||
{this.renderEmptyList()}
|
||||
{this.renderItemList(this.props.size)}
|
||||
</StyledRoot>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state:any) => {
|
||||
return {
|
||||
notes: state.notes,
|
||||
folders: state.folders,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
notesParentType: state.notesParentType,
|
||||
searches: state.searches,
|
||||
selectedSearchId: state.selectedSearchId,
|
||||
@@ -462,6 +508,4 @@ const mapStateToProps = state => {
|
||||
};
|
||||
};
|
||||
|
||||
const NoteList = connect(mapStateToProps)(NoteListComponent);
|
||||
|
||||
module.exports = { NoteList };
|
||||
export default connect(mapStateToProps)(NoteListComponent);
|
76
ElectronClient/gui/NoteListControls/NoteListControls.tsx
Normal file
76
ElectronClient/gui/NoteListControls/NoteListControls.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -6,10 +6,10 @@ export const declaration:CommandDeclaration = {
|
||||
label: () => _('Search in all the notes'),
|
||||
};
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
export const runtime = (searchBarRef:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async () => {
|
||||
if (comp.searchElement_) comp.searchElement_.focus();
|
||||
if (searchBarRef.current) searchBarRef.current.focus();
|
||||
},
|
||||
};
|
||||
};
|
@@ -5,10 +5,45 @@ const Mark = require('mark.js/dist/mark.min.js');
|
||||
const markJsUtils = require('lib/markJsUtils');
|
||||
const Note = require('lib/models/Note');
|
||||
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 {
|
||||
theme: number,
|
||||
themeId: number,
|
||||
width: number,
|
||||
height: number,
|
||||
style: any,
|
||||
dragItemIndex: number,
|
||||
highlightedWords: string[],
|
||||
@@ -28,8 +63,8 @@ interface NoteListItemProps {
|
||||
|
||||
function NoteListItem(props:NoteListItemProps, ref:any) {
|
||||
const item = props.item;
|
||||
const theme = themeStyle(props.theme);
|
||||
const hPadding = 10;
|
||||
const theme = themeStyle(props.themeId);
|
||||
const hPadding = 16;
|
||||
|
||||
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);
|
||||
|
||||
if (props.isSelected) rootStyle = Object.assign(rootStyle, props.style.listItemSelected);
|
||||
|
||||
let dragItemPosition = '';
|
||||
if (props.dragItemIndex === props.index) {
|
||||
rootStyle.borderTop = `2px solid ${theme.color}`;
|
||||
dragItemPosition = 'top';
|
||||
} else if (props.index === props.itemCount - 1 && props.dragItemIndex >= props.itemCount) {
|
||||
rootStyle.borderBottom = `2px solid ${theme.color}`;
|
||||
dragItemPosition = 'bottom';
|
||||
}
|
||||
|
||||
const onTitleClick = useCallback((event) => {
|
||||
@@ -65,7 +97,7 @@ function NoteListItem(props:NoteListItemProps, ref:any) {
|
||||
if (!item.is_todo) return null;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', height: rootStyle.height, alignItems: 'center', paddingLeft: hPadding }}>
|
||||
<div style={{ display: 'flex', height: props.height, alignItems: 'center', paddingLeft: hPadding }}>
|
||||
<input
|
||||
style={{ margin: 0, marginBottom: 1, marginRight: 5 }}
|
||||
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>;
|
||||
|
||||
// key={`${item.id}_${item.todo_completed}`}
|
||||
|
||||
// Need to include "todo_completed" in key so that checkbox is updated when
|
||||
// item is changed via sync.
|
||||
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()}
|
||||
<a
|
||||
ref={anchorRef}
|
||||
@@ -138,7 +177,7 @@ function NoteListItem(props:NoteListItemProps, ref:any) {
|
||||
{watchedIcon}
|
||||
{titleComp}
|
||||
</a>
|
||||
</div>
|
||||
</StyledRoot>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -224,8 +224,8 @@ class NotePropertiesDialog extends React.Component {
|
||||
}
|
||||
|
||||
createNoteField(key, value) {
|
||||
const styles = this.styles(this.props.theme);
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const styles = this.styles(this.props.themeId);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const labelComp = <label style={Object.assign({}, theme.textStyle, theme.controlBoxLabel)}>{this.formatLabel(key)}</label>;
|
||||
let controlComp = null;
|
||||
let editComp = null;
|
||||
@@ -356,7 +356,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const formNote = this.state.formNote;
|
||||
|
||||
const noteComps = [];
|
||||
@@ -374,7 +374,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
<div style={theme.dialogBox}>
|
||||
<div style={theme.dialogTitle}>{_('Note properties')}</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>
|
||||
);
|
||||
|
@@ -2,7 +2,7 @@ const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const NoteTextViewer = require('./NoteTextViewer.min');
|
||||
const NoteTextViewer = require('./NoteTextViewer').default;
|
||||
const HelpButton = require('./HelpButton.min');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const Revision = require('lib/models/Revision');
|
||||
@@ -38,7 +38,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
}
|
||||
|
||||
style() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const style = {
|
||||
root: {
|
||||
@@ -114,7 +114,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
this.setState({ note: note });
|
||||
}
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const markupToHtml = markupLanguageUtils.newMarkupToHtml({
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
@@ -164,7 +164,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = this.style();
|
||||
|
||||
const revisionListItems = [];
|
||||
@@ -213,7 +213,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -17,7 +17,7 @@ class NoteSearchBarComponent extends React.Component {
|
||||
}
|
||||
|
||||
style() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const style = {
|
||||
root: Object.assign({}, theme.textStyle, {
|
||||
@@ -34,7 +34,7 @@ class NoteSearchBarComponent extends React.Component {
|
||||
}
|
||||
|
||||
buttonIconComponent(iconName, clickHandler, isEnabled) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const searchButton = {
|
||||
paddingLeft: 4,
|
||||
@@ -119,7 +119,7 @@ class NoteSearchBarComponent extends React.Component {
|
||||
// backgroundColor needs to cached to a local variable to prevent the
|
||||
// colour from blinking.
|
||||
// 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.resultCount === 0 && query.length > 0) {
|
||||
this.backgroundColor = theme.warningBackgroundColor;
|
||||
@@ -181,7 +181,7 @@ class NoteSearchBarComponent extends React.Component {
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -5,7 +5,7 @@ const { themeStyle } = require('lib/theme');
|
||||
|
||||
class NoteStatusBarComponent extends React.Component {
|
||||
style() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const style = {
|
||||
root: Object.assign({}, theme.textStyle, {
|
||||
@@ -28,7 +28,7 @@ const mapStateToProps = state => {
|
||||
// notes: state.notes,
|
||||
// folders: state.folders,
|
||||
// selectedNoteIds: state.selectedNoteIds,
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -1,15 +1,24 @@
|
||||
const React = require('react');
|
||||
import * as React from 'react';
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
class NoteTextViewerComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
interface Props {
|
||||
onDomReady: Function,
|
||||
onIpcMessage: Function,
|
||||
viewerStyle: any,
|
||||
}
|
||||
|
||||
this.initialized_ = false;
|
||||
this.domReady_ = false;
|
||||
class NoteTextViewerComponent extends React.Component<Props, any> {
|
||||
|
||||
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.webviewListeners_ = null;
|
||||
|
||||
this.webview_domReady = this.webview_domReady.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);
|
||||
}
|
||||
|
||||
webview_domReady(event) {
|
||||
webview_domReady(event:any) {
|
||||
this.domReady_ = true;
|
||||
if (this.props.onDomReady) this.props.onDomReady(event);
|
||||
}
|
||||
|
||||
webview_ipcMessage(event) {
|
||||
webview_ipcMessage(event:any) {
|
||||
if (this.props.onIpcMessage) this.props.onIpcMessage(event);
|
||||
}
|
||||
|
||||
webview_load() {
|
||||
this.webview_domReady();
|
||||
this.webview_domReady({});
|
||||
}
|
||||
|
||||
webview_message(event) {
|
||||
webview_message(event:any) {
|
||||
if (!event.data || event.data.target !== 'main') return;
|
||||
|
||||
const callName = event.data.name;
|
||||
@@ -78,7 +87,14 @@ class NoteTextViewerComponent extends React.Component {
|
||||
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.domReady_ = false;
|
||||
@@ -107,7 +123,7 @@ class NoteTextViewerComponent extends React.Component {
|
||||
// Wrap WebView functions
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
send(channel, arg0 = null, arg1 = null) {
|
||||
send(channel:string, arg0:any = null, arg1:any = null) {
|
||||
const win = this.webviewRef_.current.contentWindow;
|
||||
|
||||
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)
|
||||
// ----------------------------------------------------------------
|
||||
@@ -167,9 +153,9 @@ class NoteTextViewerComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state:any) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -180,4 +166,4 @@ const NoteTextViewer = connect(
|
||||
{ withRef: true }
|
||||
)(NoteTextViewerComponent);
|
||||
|
||||
module.exports = NoteTextViewer;
|
||||
export default NoteTextViewer;
|
@@ -1,19 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import CommandService from '../../lib/services/CommandService';
|
||||
import ToolbarBase from '../ToolbarBase';
|
||||
const { connect } = require('react-redux');
|
||||
const { buildStyle } = require('lib/theme');
|
||||
const Toolbar = require('../Toolbar.min.js');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const { _ } = require('lib/locale');
|
||||
const { substrWithEllipsis } = require('lib/string-utils');
|
||||
// const Folder = require('lib/models/Folder');
|
||||
// const { _ } = require('lib/locale');
|
||||
// const { substrWithEllipsis } = require('lib/string-utils');
|
||||
|
||||
interface ButtonClickEvent {
|
||||
name: string,
|
||||
}
|
||||
|
||||
interface NoteToolbarProps {
|
||||
theme: number,
|
||||
themeId: number,
|
||||
style: any,
|
||||
folders: any[],
|
||||
watchedNoteFiles: string[],
|
||||
@@ -26,11 +26,12 @@ interface NoteToolbarProps {
|
||||
}
|
||||
|
||||
function styles_(props:NoteToolbarProps) {
|
||||
return buildStyle('NoteToolbar', props.theme, (/* theme:any*/) => {
|
||||
return buildStyle('NoteToolbar', props.themeId, (theme:any) => {
|
||||
return {
|
||||
root: {
|
||||
...props.style,
|
||||
borderBottom: 'none',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -39,52 +40,18 @@ function styles_(props:NoteToolbarProps) {
|
||||
function NoteToolbar(props:NoteToolbarProps) {
|
||||
const styles = styles_(props);
|
||||
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 updateToolbarItems = useCallback(() => {
|
||||
function updateToolbarItems() {
|
||||
const output = [];
|
||||
|
||||
output.push(
|
||||
cmdService.commandToToolbarButton('historyBackward')
|
||||
);
|
||||
|
||||
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('editAlarm'));
|
||||
output.push(cmdService.commandToToolbarButton('toggleVisiblePanes'));
|
||||
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);
|
||||
}, [props.note.id, folderId, folderTitle, props.watchedNoteFiles, props.notesParentType]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateToolbarItems();
|
||||
@@ -92,9 +59,9 @@ function NoteToolbar(props:NoteToolbarProps) {
|
||||
return () => {
|
||||
cmdService.off('commandsEnabledStateChange', updateToolbarItems);
|
||||
};
|
||||
}, [updateToolbarItems]);
|
||||
}, []);
|
||||
|
||||
return <Toolbar style={styles.root} items={toolbarItems} />;
|
||||
return <ToolbarBase style={styles.root} items={toolbarItems} />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state:any) => {
|
||||
|
@@ -1,16 +1,21 @@
|
||||
const React = require('react');
|
||||
import * as React from 'react';
|
||||
import ButtonBar from './ConfigScreen/ButtonBar';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const Setting = require('lib/models/Setting');
|
||||
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 { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js');
|
||||
|
||||
class OneDriveLoginScreenComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
interface Props {
|
||||
themeId: string,
|
||||
}
|
||||
|
||||
class OneDriveLoginScreenComponent extends React.Component<any, any> {
|
||||
constructor(props:Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
authLog: [],
|
||||
@@ -18,8 +23,8 @@ class OneDriveLoginScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const log = (s) => {
|
||||
this.setState(state => {
|
||||
const log = (s:any) => {
|
||||
this.setState((state:any) => {
|
||||
const authLog = state.authLog.slice();
|
||||
authLog.push({ key: (Date.now() + Math.random()).toString(), text: s });
|
||||
return { authLog: authLog };
|
||||
@@ -30,7 +35,7 @@ class OneDriveLoginScreenComponent extends React.Component {
|
||||
const syncTarget = reg.syncTarget(syncTargetId);
|
||||
const oneDriveApiUtils = new OneDriveApiNodeUtils(syncTarget.api());
|
||||
const auth = await oneDriveApiUtils.oauthDance({
|
||||
log: (s) => log(s),
|
||||
log: (s:any) => log(s),
|
||||
});
|
||||
|
||||
Setting.setValue(`sync.${syncTargetId}.auth`, auth ? JSON.stringify(auth) : null);
|
||||
@@ -52,9 +57,7 @@ class OneDriveLoginScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width });
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const logComps = [];
|
||||
for (const l of this.state.authLog) {
|
||||
@@ -66,22 +69,23 @@ class OneDriveLoginScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header style={headerStyle}/>
|
||||
<div style={{ padding: 10 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ padding: theme.configScreenPadding, flex: 1 }}>
|
||||
{logComps}
|
||||
</div>
|
||||
<ButtonBar
|
||||
onCancelClick={() => this.props.dispatch({ type: 'NAV_BACK' })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state:any) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
const OneDriveLoginScreen = connect(mapStateToProps)(OneDriveLoginScreenComponent);
|
||||
export default connect(mapStateToProps)(OneDriveLoginScreenComponent);
|
||||
|
||||
module.exports = { OneDriveLoginScreen };
|
@@ -164,10 +164,10 @@ class PromptDialog extends React.Component {
|
||||
|
||||
render() {
|
||||
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 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) => {
|
||||
if (this.props.onClose) {
|
||||
|
173
ElectronClient/gui/ResizableLayout/ResizableLayout.tsx
Normal file
173
ElectronClient/gui/ResizableLayout/ResizableLayout.tsx
Normal 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;
|
@@ -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]);
|
||||
}
|
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import ButtonBar from './ConfigScreen/ButtonBar';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { Header } = require('./Header/Header.min.js');
|
||||
const prettyBytes = require('pretty-bytes');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
|
||||
@@ -14,8 +14,9 @@ interface Style {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
theme: any;
|
||||
style: Style
|
||||
themeId: number;
|
||||
style: Style,
|
||||
dispatch: Function,
|
||||
}
|
||||
|
||||
interface Resource {
|
||||
@@ -37,7 +38,7 @@ interface ResourceTable {
|
||||
onResourceClick: (resource: Resource) => any
|
||||
onResourceDelete: (resource: Resource) => any
|
||||
onToggleSorting: (order: SortingOrder) => any
|
||||
theme: any
|
||||
themeId: number
|
||||
style: Style
|
||||
}
|
||||
|
||||
@@ -50,17 +51,19 @@ interface ActiveSorting {
|
||||
}
|
||||
|
||||
const ResourceTable: React.FC<ResourceTable> = (props: ResourceTable) => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const sortOrderEngagedMarker = (s: SortingOrder) => {
|
||||
return (
|
||||
<a href="#"
|
||||
style={{ color: props.theme.urlColor }}
|
||||
style={{ color: theme.urlColor }}
|
||||
onClick={() => props.onToggleSorting(s)}>{
|
||||
(props.sorting.order === s && props.sorting.type === 'desc') ? '▾' : '▴'}</a>
|
||||
);
|
||||
};
|
||||
|
||||
const titleCellStyle = {
|
||||
...props.theme.textStyle,
|
||||
...theme.textStyle,
|
||||
textOverflow: 'ellipsis',
|
||||
overflowX: 'hidden',
|
||||
maxWidth: 1,
|
||||
@@ -69,14 +72,14 @@ const ResourceTable: React.FC<ResourceTable> = (props: ResourceTable) => {
|
||||
};
|
||||
|
||||
const cellStyle = {
|
||||
...props.theme.textStyle,
|
||||
...theme.textStyle,
|
||||
whiteSpace: 'nowrap',
|
||||
color: props.theme.colorFaded,
|
||||
color: theme.colorFaded,
|
||||
width: 1,
|
||||
};
|
||||
|
||||
const headerStyle = {
|
||||
...props.theme.textStyle,
|
||||
...theme.textStyle,
|
||||
whiteSpace: 'nowrap',
|
||||
width: 1,
|
||||
fontWeight: 'bold',
|
||||
@@ -97,7 +100,7 @@ const ResourceTable: React.FC<ResourceTable> = (props: ResourceTable) => {
|
||||
<tr key={index}>
|
||||
<td style={titleCellStyle} className="titleCell">
|
||||
<a
|
||||
style={{ color: props.theme.urlColor }}
|
||||
style={{ color: theme.urlColor }}
|
||||
href="#"
|
||||
onClick={() => props.onResourceClick(resource)}>{resource.title || `(${_('Untitled')})`}
|
||||
</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">{resource.id}</td>
|
||||
<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>
|
||||
</tr>
|
||||
)}
|
||||
@@ -202,8 +205,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width });
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const rootStyle:any = {
|
||||
...style,
|
||||
@@ -211,13 +213,16 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
color: theme.color,
|
||||
padding: 20,
|
||||
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;
|
||||
|
||||
const containerHeight = style.height;
|
||||
|
||||
return (
|
||||
<div style={{ ...theme.containerStyle, fontFamily: theme.fontFamily }}>
|
||||
<Header style={headerStyle} />
|
||||
<div style={{ ...theme.containerStyle, fontFamily: theme.fontFamily, height: containerHeight, display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={rootStyle}>
|
||||
<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.')
|
||||
@@ -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>
|
||||
}
|
||||
{this.state.resources && <ResourceTable
|
||||
theme={theme}
|
||||
themeId={this.props.themeId}
|
||||
style={style}
|
||||
resources={this.state.resources}
|
||||
sorting={this.state.sorting}
|
||||
@@ -243,13 +248,16 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ButtonBar
|
||||
onCancelClick={() => this.props.dispatch({ type: 'NAV_BACK' })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any) => ({
|
||||
theme: state.settings.theme,
|
||||
themeId: state.settings.theme,
|
||||
});
|
||||
|
||||
const ResourceScreen = connect(mapStateToProps)(ResourceScreenComponent);
|
||||
|
@@ -5,20 +5,30 @@ const { connect, Provider } = require('react-redux');
|
||||
const { _ } = require('lib/locale.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 { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
|
||||
const { DropboxLoginScreen } = require('./DropboxLoginScreen.min.js');
|
||||
const { StatusScreen } = require('./StatusScreen.min.js');
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ConfigScreen } = require('./ConfigScreen.min.js');
|
||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
const { Navigator } = require('./Navigator.min.js');
|
||||
const WelcomeUtils = require('lib/WelcomeUtils');
|
||||
const { app } = require('../app');
|
||||
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
|
||||
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() {
|
||||
this.wcsTimeoutId_ = null;
|
||||
|
||||
@@ -84,6 +94,8 @@ class RootComponent extends React.Component {
|
||||
height: this.props.size.height / this.props.zoomFactor,
|
||||
};
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const screens = {
|
||||
Main: { screen: MainScreen },
|
||||
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
|
||||
@@ -94,7 +106,14 @@ class RootComponent extends React.Component {
|
||||
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,
|
||||
zoomFactor: state.settings.windowContentZoomFactor / 100,
|
||||
appState: state.appState,
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
|
87
ElectronClient/gui/SearchBar/SearchBar.tsx
Normal file
87
ElectronClient/gui/SearchBar/SearchBar.tsx
Normal 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);
|
17
ElectronClient/gui/SearchBar/hooks/useSearch.ts
Normal file
17
ElectronClient/gui/SearchBar/hooks/useSearch.ts
Normal 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]);
|
||||
}
|
28
ElectronClient/gui/SearchBar/styles/index.ts
Normal file
28
ElectronClient/gui/SearchBar/styles/index.ts
Normal 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;
|
||||
`;
|
@@ -12,7 +12,7 @@ const { reg } = require('lib/registry.js');
|
||||
const { clipboard } = require('electron');
|
||||
|
||||
interface ShareNoteDialogProps {
|
||||
theme: number,
|
||||
themeId: number,
|
||||
noteIds: Array<string>,
|
||||
onClose: Function,
|
||||
}
|
||||
@@ -22,7 +22,7 @@ interface SharesMap {
|
||||
}
|
||||
|
||||
function styles_(props:ShareNoteDialogProps) {
|
||||
return buildStyle('ShareNoteDialog', props.theme, (theme:any) => {
|
||||
return buildStyle('ShareNoteDialog', props.themeId, (theme:any) => {
|
||||
return {
|
||||
noteList: {
|
||||
marginBottom: 10,
|
||||
@@ -67,7 +67,7 @@ export default function ShareNoteDialog(props:ShareNoteDialogProps) {
|
||||
const [shares, setShares] = useState<SharesMap>({});
|
||||
|
||||
const noteCount = notes.length;
|
||||
const theme = themeStyle(props.theme);
|
||||
const theme = themeStyle(props.themeId);
|
||||
const styles = styles_(props);
|
||||
|
||||
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>
|
||||
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
|
||||
{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>
|
||||
);
|
||||
|
@@ -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 };
|
678
ElectronClient/gui/SideBar/SideBar.tsx
Normal file
678
ElectronClient/gui/SideBar/SideBar.tsx
Normal 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 };
|
129
ElectronClient/gui/SideBar/styles/index.ts
Normal file
129
ElectronClient/gui/SideBar/styles/index.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
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;
|
||||
`;
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
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%;
|
||||
`;
|
@@ -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 };
|
163
ElectronClient/gui/StatusScreen/StatusScreen.tsx
Normal file
163
ElectronClient/gui/StatusScreen/StatusScreen.tsx
Normal 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);
|
||||
|
@@ -4,7 +4,7 @@ const { themeStyle } = require('lib/theme');
|
||||
|
||||
class TagItemComponent extends React.Component {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = Object.assign({}, theme.tagStyle);
|
||||
const title = this.props.title;
|
||||
|
||||
@@ -13,7 +13,7 @@ class TagItemComponent extends React.Component {
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return { theme: state.settings.theme };
|
||||
return { themeId: state.settings.theme };
|
||||
};
|
||||
|
||||
const TagItem = connect(mapStateToProps)(TagItemComponent);
|
||||
|
@@ -6,7 +6,7 @@ const TagItem = require('./TagItem.min.js');
|
||||
class TagListComponent extends React.Component {
|
||||
render() {
|
||||
const style = Object.assign({}, this.props.style);
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const tags = this.props.items;
|
||||
|
||||
style.display = 'flex';
|
||||
@@ -15,11 +15,13 @@ class TagListComponent extends React.Component {
|
||||
style.boxSizing = 'border-box';
|
||||
style.fontSize = theme.fontSize;
|
||||
style.whiteSpace = 'nowrap';
|
||||
style.height = 25;
|
||||
// style.height = 40;
|
||||
style.paddingTop = 8;
|
||||
style.paddingBottom = 8;
|
||||
|
||||
const tagItems = [];
|
||||
if (tags && tags.length > 0) {
|
||||
// Sort by id for now, but probably needs to be changed in the future.
|
||||
|
||||
tags.sort((a, b) => {
|
||||
return a.title < b.title ? -1 : +1;
|
||||
});
|
||||
@@ -42,7 +44,7 @@ class TagListComponent extends React.Component {
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return { theme: state.settings.theme };
|
||||
return { themeId: state.settings.theme };
|
||||
};
|
||||
|
||||
const TagList = connect(mapStateToProps)(TagListComponent);
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
68
ElectronClient/gui/ToggleEditorsButton/styles/index.ts
Normal file
68
ElectronClient/gui/ToggleEditorsButton/styles/index.ts
Normal 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;
|
||||
});
|
||||
}
|
@@ -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;
|
91
ElectronClient/gui/ToolbarBase.tsx
Normal file
91
ElectronClient/gui/ToolbarBase.tsx
Normal 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);
|
@@ -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;
|
63
ElectronClient/gui/ToolbarButton/ToolbarButton.tsx
Normal file
63
ElectronClient/gui/ToolbarButton/ToolbarButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
40
ElectronClient/gui/ToolbarButton/styles/index.ts
Normal file
40
ElectronClient/gui/ToolbarButton/styles/index.ts
Normal 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}`;
|
@@ -3,7 +3,7 @@ const { themeStyle } = require('lib/theme');
|
||||
|
||||
class ToolbarSpace extends React.Component {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = Object.assign({}, theme.toolbarStyle);
|
||||
style.minWidth = style.height / 2;
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { createSelector } = require('reselect');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
|
||||
const themeSelector = (state, props) => themeStyle(props.theme);
|
||||
const themeSelector = (state, props) => themeStyle(props.themeId);
|
||||
|
||||
const style = createSelector(
|
||||
themeSelector,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { createSelector } = require('reselect');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
|
||||
const themeSelector = (state, props) => themeStyle(props.theme);
|
||||
const themeSelector = (state, props) => themeStyle(props.themeId);
|
||||
|
||||
const style = createSelector(
|
||||
themeSelector,
|
||||
|
25
ElectronClient/gui/style/StyledInput.tsx
Normal file
25
ElectronClient/gui/style/StyledInput.tsx
Normal 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;
|
7
ElectronClient/gui/style/StyledTextInput.tsx
Normal file
7
ElectronClient/gui/style/StyledTextInput.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
const styled = require('styled-components').default;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
|
||||
`;
|
||||
|
||||
export default StyledInput;
|
@@ -9,9 +9,12 @@
|
||||
-->
|
||||
<title>Joplin</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="style/icons/style.css">
|
||||
<!-- TODO: Remove once all icons have been swapped -->
|
||||
<link rel="stylesheet" href="node_modules/@fortawesome/fontawesome-free/css/all.min.css">
|
||||
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
|
||||
<link rel="stylesheet" href="node_modules/smalltalk/css/smalltalk.css">
|
||||
<link rel="stylesheet" href="node_modules/roboto-fontface/css/roboto/roboto-fontface.css">
|
||||
<link rel="stylesheet" href="node_modules/codemirror/lib/codemirror.css">
|
||||
|
||||
<style>
|
||||
|
763
ElectronClient/package-lock.json
generated
763
ElectronClient/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user