1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

48 Commits

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

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

View File

@@ -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
@@ -116,6 +122,7 @@ ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.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 +132,40 @@ 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/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/ToolbarButton/styles/index.js
ElectronClient/gui/ToolbarButton/ToolbarButton.js
ReactNativeClient/lib/AsyncActionQueue.js
ReactNativeClient/lib/checkPermissions.js
ReactNativeClient/lib/commands/historyBackward.js
@@ -176,6 +204,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

46
.gitignore vendored
View File

@@ -50,8 +50,8 @@ joplin-webclipper-source.zip
Tools/commit_hook.txt
.vscode/*
*.map
ReactNativeClient/lib/sql-extensions/
!ReactNativeClient/lib/sql-extensions/spellfix.dll
ReactNativeClient/lib/sql-extensions/spellfix.so
ReactNativeClient/lib/sql-extensions/spellfix.dylib
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
CliClient/app/LinkSelector.js
@@ -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
@@ -109,6 +115,7 @@ ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.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 +125,40 @@ 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/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/ToolbarButton/styles/index.js
ElectronClient/gui/ToolbarButton/ToolbarButton.js
ReactNativeClient/lib/AsyncActionQueue.js
ReactNativeClient/lib/checkPermissions.js
ReactNativeClient/lib/commands/historyBackward.js
@@ -169,6 +197,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

View File

@@ -11,6 +11,7 @@ Note that all the applications share the same library, which, for historical rea
- macOS, Linux: Install rsync - https://nodejs.org/en/
- macOS: 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

View File

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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "joplin",
"version": "1.0.168",
"version": "1.2.0",
"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",

View File

@@ -28,7 +28,7 @@
],
"owner": "Laurent Cozic"
},
"version": "1.0.168",
"version": "1.2.0",
"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",

View File

@@ -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');

View File

@@ -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'",

View File

@@ -37,13 +37,13 @@ const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/red
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 +58,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 +99,7 @@ const appDefaultState = Object.assign({}, defaultState, {
watchedNoteFiles: [],
lastEditorScrollPercents: {},
devToolsVisible: false,
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
});
class Application extends BaseApplication {
@@ -279,6 +281,16 @@ 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;
}
} catch (error) {
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
@@ -286,10 +298,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 +388,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 +532,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 +663,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 +712,7 @@ class Application extends BaseApplication {
submenu: [
newNoteItem,
newTodoItem,
newNotebookItem, {
newFolderItem, {
label: _('Close Window'),
platforms: ['darwin'],
accelerator: shim.isMac() && keymapService.getAccelerator('closeWindow'),
@@ -738,7 +751,6 @@ class Application extends BaseApplication {
const separator = () => {
return {
type: 'separator',
screens: ['Main'],
};
};
@@ -986,6 +998,8 @@ class Application extends BaseApplication {
Menu.setApplicationMenu(menu);
this.lastMenuScreen_ = screen;
if (updateStates) await this.updateMenuItemStates();
}
async updateMenuItemStates(state = null) {
@@ -1128,7 +1142,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

View File

@@ -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,
};
},
};
};

View File

@@ -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 };
},
};
};

View File

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

View File

@@ -40,10 +40,12 @@ class ClipperConfigScreenComponent extends React.Component {
}
render() {
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'],

View File

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

View File

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

View File

@@ -1,48 +1,63 @@
const React = require('react');
import * as React from 'react';
import SideBar from './SideBar';
import ButtonBar from './ButtonBar';
import Button, { ButtonLevel } from '../Button/Button';
const { connect } = require('react-redux');
const 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 {
&nbsp;&nbsp;
{showLogButton}
&nbsp;&nbsp;
<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}/>
&nbsp;&nbsp;
<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,16 +508,18 @@ 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);
};
@@ -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 };

View File

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

View File

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

View File

@@ -1,16 +1,24 @@
const React = require('react');
import * as React from 'react';
import ButtonBar from './ConfigScreen/ButtonBar';
const { connect } = require('react-redux');
const { 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);

View File

@@ -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'],

View File

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

View File

@@ -14,7 +14,7 @@ class HelpButtonComponent extends React.Component {
}
render() {
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,
};
};

View File

@@ -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,

View File

@@ -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,
};
};

View File

@@ -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();
}

View File

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

View File

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

View File

@@ -1,24 +1,26 @@
const React = require('react');
import * as React from 'react';
import ResizableLayout, { findItemByKey, LayoutItem, LayoutItemDirection } from '../ResizableLayout/ResizableLayout';
import NoteList from '../NoteList/NoteList.js';
import NoteEditor from '../NoteEditor/NoteEditor.js';
import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog.js';
import ShareNoteDialog from '../ShareNoteDialog.js';
import NoteListControls from '../NoteListControls/NoteListControls.js';
import CommandService from 'lib/services/CommandService';
const produce = require('immer').default;
const { connect } = require('react-redux');
const { 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} />;
}
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,
@@ -519,6 +653,4 @@ const mapStateToProps = state => {
};
};
const MainScreen = connect(mapStateToProps)(MainScreenComponent);
module.exports = { MainScreen };
export default connect(mapStateToProps)(MainScreenComponent);

View File

@@ -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 => {

View File

@@ -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',
};

View File

@@ -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,
// });
// }
}
},
};

View File

@@ -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 => {

View File

@@ -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 => {

View File

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

View File

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

View File

@@ -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',

View File

@@ -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>
);

View File

@@ -37,7 +37,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
const [webviewReady, setWebviewReady] = useState(false);
const previousRenderedBody = usePrevious(renderedBody);
const previousContent = usePrevious(props.content);
const previousSearchMarkers = usePrevious(props.searchMarkers);
const previousContentKey = usePrevious(props.contentKey);
@@ -48,7 +48,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 +350,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);
@@ -420,7 +421,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
return () => {
document.head.removeChild(element);
};
}, [props.theme]);
}, [props.themeId]);
const webview_domReady = useCallback(() => {
setWebviewReady(true);
@@ -478,7 +479,14 @@ 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
const textChanged = props.searchMarkers.keywords.length > 0 && props.content !== previousContent;
if (props.searchMarkers !== previousSearchMarkers || textChanged) {
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
if (editorRef.current) {
@@ -487,7 +495,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
props.setLocalSearchResultCount(matches);
}
}
}, [props.searchMarkers, props.setLocalSearchResultCount, renderedBody]);
}, [props.searchMarkers, previousSearchMarkers, props.setLocalSearchResultCount, props.content, previousContent]);
const cellEditorStyle = useMemo(() => {
const output = { ...styles.cellEditor };
@@ -547,9 +555,10 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
<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,7 +589,7 @@ 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}
/>

View File

@@ -28,7 +28,6 @@ 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,
@@ -152,6 +160,10 @@ function Editor(props: EditorProps, ref: any) {
'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 = {
@@ -216,7 +228,7 @@ function Editor(props: EditorProps, ref: any) {
const cmOptions = {
value: props.value,
screenReaderLabel: props.value,
theme: props.theme,
theme: props.codeMirrorTheme,
mode: props.mode,
readOnly: props.readOnly,
autoCloseBrackets: props.autoMatchBraces,
@@ -238,6 +250,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);
@@ -265,9 +282,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) {

View File

@@ -2,22 +2,20 @@ import * as React from 'react';
import CommandService from '../../../../lib/services/CommandService';
const ToolbarBase = require('../../../Toolbar.min.js');
const { buildStyle, themeStyle } = require('lib/theme');
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 +27,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,6 +45,8 @@ 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} />;

View File

@@ -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',

View File

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

View File

@@ -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;

View File

@@ -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>
);

View File

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

View File

@@ -13,13 +13,18 @@ import useMessageHandler from './utils/useMessageHandler';
import useWindowCommandHandler from './utils/useWindowCommandHandler';
import 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) {
@@ -331,25 +338,49 @@ function NoteEditor(props: NoteEditorProps) {
}
function renderNoteToolbar() {
// const theme = themeStyle(props.themeId);
const toolbarStyle = {
marginBottom: 0,
// paddingTop: theme.mainPadding,
// paddingBottom: theme.mainPadding,
};
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);
let control = null;
if (!props.selectedNoteTags.length) {
const noteIds = [formNote.id];
control = <span onClick={() => { CommandService.instance().execute('setTags', { noteIds }); }} style={theme.clickableTextStyle}>Click to add some tags...</span>;
} else {
control = <TagList items={props.selectedNoteTags} />;
}
return (
<div style={{ paddingLeft: 8 }}>{control}</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 +391,7 @@ function NoteEditor(props: NoteEditorProps) {
value={formNote.title}
/>
{titleBarDate}
{renderNoteToolbar()}
</div>
);
}
@@ -381,7 +413,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 +423,7 @@ function NoteEditor(props: NoteEditorProps) {
keyboardMode: Setting.value('editor.keyboardMode'),
locale: Setting.value('locale'),
onDrop: onDrop,
noteToolbarButtonInfos: useNoteToolbarButtons(),
};
let editor = null;
@@ -414,10 +447,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 +466,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 +511,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 +544,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 +576,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,

View File

@@ -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',
},
];

View File

@@ -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,

View File

@@ -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 {

View File

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

View File

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

View File

@@ -44,6 +44,7 @@ function editorCommandRuntime(declaration:CommandDeclaration, editorRef:any):Com
}
},
isEnabled: (props:any) => {
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,
};
},
};

View File

@@ -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);

View File

@@ -0,0 +1,58 @@
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;
const StyledRoot = styled.div`
width: 100%;
/*height: 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;
`;
export default function NoteListControls() {
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');
}
return (
<StyledRoot>
<SearchBar inputRef={searchBarRef}/>
<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}
/>
</StyledRoot>
);
}

View File

@@ -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();
},
};
};

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -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,
};
};

View File

@@ -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,
};
};

View File

@@ -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,
};
};

View File

@@ -169,7 +169,7 @@ class NoteTextViewerComponent extends React.Component {
const mapStateToProps = state => {
return {
theme: state.settings.theme,
themeId: state.settings.theme,
};
};

View File

@@ -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';
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,27 @@ 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 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('showNoteProperties'));
if (props.watchedNoteFiles.indexOf(props.note.id) >= 0) {
output.push(cmdService.commandToToolbarButton('stopExternalEditing'));
} else {
output.push(cmdService.commandToToolbarButton('startExternalEditing'));
}
// 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'));
output.push(cmdService.commandToToolbarButton('toggleVisiblePanes'));
output.push(cmdService.commandToToolbarButton('showNoteProperties'));
setToolbarItems(output);
}, [props.note.id, folderId, folderTitle, props.watchedNoteFiles, props.notesParentType]);
}
useEffect(() => {
updateToolbarItems();
@@ -92,7 +68,7 @@ function NoteToolbar(props:NoteToolbarProps) {
return () => {
cmdService.off('commandsEnabledStateChange', updateToolbarItems);
};
}, [updateToolbarItems]);
}, []);
return <Toolbar style={styles.root} items={toolbarItems} />;
}

View File

@@ -1,16 +1,21 @@
const React = require('react');
import * as React from 'react';
import ButtonBar from './ConfigScreen/ButtonBar';
const { connect } = require('react-redux');
const { 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 };

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import * as React from 'react';
import 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);

View File

@@ -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,
};
};

View File

@@ -0,0 +1,51 @@
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,
}
function SearchBar(props:Props) {
const [query, setQuery] = useState('');
const iconName = !query ? CommandService.instance().iconName('search') : 'fa fa-times';
const onChange = (event:any) => {
setQuery(event.currentTarget.value);
};
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}/>
<SearchButton onClick={onSearchButtonClick}>
<SearchButtonIcon className={iconName}/>
</SearchButton>
</Root>
);
}
const mapStateToProps = (state:any) => {
return {
notesParentType: state.notesParentType,
};
};
export default connect(mapStateToProps)(SearchBar);

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ const { reg } = require('lib/registry.js');
const { clipboard } = require('electron');
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>
);

View File

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

View File

@@ -0,0 +1,646 @@
import * as React from 'react';
import { StyledRoot, StyledAddButton, StyledHeader, StyledHeaderIcon, 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'),
];
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);
}
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(folder:any) {
this.props.dispatch({
type: 'FOLDER_SELECT',
id: folder ? folder.id : 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 <StyledNoteCount>{count}</StyledNoteCount>;
}
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>
<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 isExpanded = this.props.collapsedFolderIds.indexOf(folder.id) < 0;
const expandIcon = this.renderExpandIcon(isExpanded, hasChildren);
const expandLink = hasChildren ? (
<StyledExpandLink href="#" data-folder-id={folder.id} onClick={this.onFolderToggleClick_}>
{expandIcon}
</StyledExpandLink>
) : (
<StyledExpandLink>{expandIcon}</StyledExpandLink>
);
const anchorRef = this.anchorItemRef('folder', folder.id);
const noteCount = folder.note_count ? this.renderNoteCount(folder.note_count) : '';
return (
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth}`} key={folder.id} onDragStart={this.onFolderDragStart_} onDragOver={this.onFolderDragOver_} onDrop={this.onFolderDrop_} draggable={true} data-folder-id={folder.id}>
{expandLink}
<StyledListItemAnchor
ref={anchorRef}
className="list-item"
isConflictFolder={folder.id === Folder.conflictFolderId()}
href="#"
selected={selected}
data-id={folder.id}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={(event:any) => this.itemContextMenu(event)}
data-folder-id={folder.id}
onClick={() => {
this.folderItem_click(folder);
}}
onDoubleClick={this.onFolderToggleClick_}
>
{Folder.displayTitle(folder)} {noteCount}
</StyledListItemAnchor>
</StyledListItem>
);
}
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={(event:any) => this.itemContextMenu(event)}
onClick={() => {
this.tagItem_click(tag);
}}
>
{Tag.displayTitle(tag)} {noteCount}
</StyledListItemAnchor>
</StyledListItem>
);
}
makeDivider(key:string) {
return <div style={{ height: 2, backgroundColor: 'blue' }} key={key} />;
}
renderHeader(key:string, label:string, iconName:string, contextMenuHandler:Function = null, onPlusButtonClick:Function = null, extraProps:any = {}) {
const headerClick = extraProps.onClick || null;
delete extraProps.onClick;
const ref = this.anchorItemRef('headers', key);
return (
<div key={key} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<StyledHeader
ref={ref}
{...extraProps}
onContextMenu={contextMenuHandler}
onClick={(event:any) => {
// if a custom click event is attached, trigger that.
if (headerClick) {
headerClick(key, event);
}
this.onHeaderClick_(key);
}}
>
<StyledHeaderIcon className={iconName}/>
<StyledHeaderLabel>{label}</StyledHeaderLabel>
</StyledHeader>
{ onPlusButtonClick && <StyledAddButton onClick={onPlusButtonClick} iconName="fas fa-plus" level={ButtonLevel.SideBarSecondary}/> }
</div>
);
}
selectedItem() {
if (this.props.notesParentType === 'Folder' && this.props.selectedFolderId) {
return { type: 'folder', id: this.props.selectedFolderId };
} else if (this.props.notesParentType === 'Tag' && this.props.selectedTagId) {
return { type: 'tag', id: this.props.selectedTagId };
}
return null;
}
onKeyDown(event:any) {
const keyCode = event.keyCode;
const selectedItem = this.selectedItem();
if (keyCode === 40 || keyCode === 38) {
// DOWN / UP
event.preventDefault();
const focusItems = [];
for (let i = 0; i < this.folderItemsOrder_.length; i++) {
const id = this.folderItemsOrder_[i];
focusItems.push({ id: id, ref: this.anchorItemRefs['folder'][id], type: 'folder' });
}
for (let i = 0; i < this.tagItemsOrder_.length; i++) {
const id = this.tagItemsOrder_[i];
focusItems.push({ id: id, ref: this.anchorItemRefs['tag'][id], type: 'tag' });
}
let currentIndex = 0;
for (let i = 0; i < focusItems.length; i++) {
if (!selectedItem || focusItems[i].id === selectedItem.id) {
currentIndex = i;
break;
}
}
const inc = keyCode === 38 ? -1 : +1;
let newIndex = currentIndex + inc;
if (newIndex < 0) newIndex = 0;
if (newIndex > focusItems.length - 1) newIndex = focusItems.length - 1;
const focusItem = focusItems[newIndex];
const actionName = `${focusItem.type.toUpperCase()}_SELECT`;
this.props.dispatch({
type: actionName,
id: focusItem.id,
});
focusItem.ref.current.focus();
}
if (keyCode === 9) {
// TAB
event.preventDefault();
if (event.shiftKey) {
CommandService.instance().execute('focusElement', { target: 'noteBody' });
} else {
CommandService.instance().execute('focusElement', { target: 'noteList' });
}
}
if (selectedItem && selectedItem.type === 'folder' && keyCode === 32) {
// SPACE
event.preventDefault();
this.props.dispatch({
type: 'FOLDER_TOGGLE',
id: selectedItem.id,
});
}
if (keyCode === 65 && (event.ctrlKey || event.metaKey)) {
// Ctrl+A key
event.preventDefault();
}
}
onHeaderClick_(key:string) {
const toggleKey = `${key}IsExpanded`;
const isExpanded = (this.state as any)[toggleKey];
const newState:any = { [toggleKey]: !isExpanded };
this.setState(newState);
Setting.setValue(toggleKey, !isExpanded);
}
onAllNotesClick_() {
this.props.dispatch({
type: 'SMART_FILTER_SELECT',
id: ALL_NOTES_FILTER_ID,
});
}
renderSynchronizeButton(type:string) {
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : '';
return (
<StyledSynchronizeButton
level={ButtonLevel.SideBarSecondary}
iconName="icon-sync"
key="sync_button"
iconAnimation={iconAnimation}
title={label}
onClick={() => {
CommandService.instance().execute('synchronize', { syncStarted: type !== 'sync' });
}}
/>
);
}
onAddFolderButtonClick() {
CommandService.instance().execute('newFolder');
}
render() {
const theme = themeStyle(this.props.themeId);
const items = [];
items.push(
this.renderHeader('folderHeader', _('Notebooks'), 'icon-notebooks', this.header_contextMenu, this.onAddFolderButtonClick, {
onDrop: this.onFolderDrop_,
['data-folder-id']: '',
toggleblock: 1,
})
);
if (this.props.folders.length) {
const allNotesSelected = this.props.notesParentType === 'SmartFilter' && this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID;
const result = shared.renderFolders(this.props, this.renderFolderItem.bind(this));
const folderItems = [this.renderAllNotesItem(allNotesSelected)].concat(result.items);
this.folderItemsOrder_ = result.order;
items.push(
<div className="folders" key="folder_items" style={{ display: this.state.folderHeaderIsExpanded ? 'block' : 'none', paddingBottom: 10 }}>
{folderItems}
</div>
);
}
items.push(
this.renderHeader('tagHeader', _('Tags'), 'icon-tags', null, null, {
toggleblock: 1,
})
);
if (this.props.tags.length) {
const result = shared.renderTags(this.props, this.renderTag.bind(this));
const tagItems = result.items;
this.tagItemsOrder_ = result.order;
items.push(
<div className="tags" key="tag_items" style={{ display: this.state.tagHeaderIsExpanded ? 'block' : 'none' }}>
{tagItems}
</div>
);
}
let decryptionReportText = '';
if (this.props.decryptionWorker && this.props.decryptionWorker.state !== 'idle' && this.props.decryptionWorker.itemCount) {
decryptionReportText = _('Decrypting items: %d/%d', this.props.decryptionWorker.itemIndex + 1, this.props.decryptionWorker.itemCount);
}
let resourceFetcherText = '';
if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) {
resourceFetcherText = _('Fetching resources: %d/%d', this.props.resourceFetcher.fetchingCount, this.props.resourceFetcher.toFetchCount);
}
const lines = Synchronizer.reportToLines(this.props.syncReport);
if (resourceFetcherText) lines.push(resourceFetcherText);
if (decryptionReportText) lines.push(decryptionReportText);
const syncReportText = [];
for (let i = 0; i < lines.length; i++) {
syncReportText.push(
<StyledSyncReportText key={i}>
{lines[i]}
</StyledSyncReportText>
);
}
const syncButton = this.renderSynchronizeButton(this.props.syncStarted ? 'cancel' : 'sync');
const syncReportComp = !syncReportText.length ? null : (
<StyledSyncReport key="sync_report">
{syncReportText}
</StyledSyncReport>
);
return (
<StyledRoot ref={this.rootRef} onKeyDown={this.onKeyDown} className="side-bar">
<div style={{ flex: 1, overflowX: 'hidden', overflowY: 'auto' }}>{items}</div>
<div style={{ flex: 0, padding: theme.mainPadding }}>
{syncReportComp}
{syncButton}
</div>
</StyledRoot>
);
}
}
const mapStateToProps = (state:any) => {
return {
folders: state.folders,
tags: state.tags,
searches: state.searches,
syncStarted: state.syncStarted,
syncReport: state.syncReport,
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
selectedSmartFilterId: state.selectedSmartFilterId,
notesParentType: state.notesParentType,
locale: state.settings.locale,
themeId: state.settings.theme,
collapsedFolderIds: state.collapsedFolderIds,
decryptionWorker: state.decryptionWorker,
resourceFetcher: state.resourceFetcher,
sidebarVisibility: state.sidebarVisibility,
noteListVisibility: state.noteListVisibility,
};
};
const SideBar = connect(mapStateToProps)(SideBarComponent);
module.exports = { SideBar };

View File

@@ -0,0 +1,122 @@
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 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;
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%;
`;

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ const { themeStyle } = require('lib/theme');
class TagItemComponent extends React.Component {
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);

View File

@@ -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);

View File

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

View File

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

View File

@@ -1,22 +1,32 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('lib/theme');
const ToolbarButton = require('./ToolbarButton.min.js');
const ToolbarButton = require('./ToolbarButton/ToolbarButton.js').default;
const ToolbarSpace = require('./ToolbarSpace.min.js');
const ToggleEditorsButton = require('./ToggleEditorsButton/ToggleEditorsButton.js').default;
class ToolbarComponent extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
const style = Object.assign({
// height: theme.toolbarHeight,
display: 'flex',
flexDirection: 'row',
borderBottom: `1px solid ${theme.dividerColor}`,
boxSizing: 'border-box',
backgroundColor: theme.backgroundColor3,
padding: theme.toolbarPadding,
paddingRight: theme.mainPadding,
}, this.props.style);
const itemComps = [];
const groupStyle = {
display: 'flex',
flexDirection: 'row',
boxSizing: 'border-box',
};
const leftItemComps = [];
const centerItemComps = [];
const rightItemComps = [];
if (this.props.items) {
for (let i = 0; i < this.props.items.length; i++) {
@@ -30,31 +40,47 @@ class ToolbarComponent extends React.Component {
const props = Object.assign(
{
key: key,
theme: this.props.theme,
themeId: this.props.themeId,
},
o
);
if (this.props.disabled) props.disabled = true;
if (itemType === 'button') {
itemComps.push(<ToolbarButton {...props} />);
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') {
itemComps.push(<ToolbarSpace {...props} />);
centerItemComps.push(<ToolbarSpace {...props} />);
}
}
}
return (
<div className="editor-toolbar" style={style}>
{itemComps}
<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 => {
return { theme: state.settings.theme };
return { themeId: state.settings.theme };
};
const Toolbar = connect(mapStateToProps)(ToolbarComponent);

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -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,

View File

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

View File

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

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.1.1",
"version": "1.2.1",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {
@@ -125,6 +125,7 @@
"color": "^3.1.2",
"compare-versions": "^3.2.1",
"countable": "^3.0.1",
"debounce": "^1.2.0",
"diacritics": "^1.3.0",
"diff-match-patch": "^1.0.4",
"electron-context-menu": "^0.15.0",
@@ -168,7 +169,7 @@
"md5": "^2.2.1",
"md5-file": "^4.0.0",
"memory-cache": "^0.2.0",
"mermaid": "^8.4.6",
"mermaid": "^8.8.0",
"moment": "^2.22.2",
"multiparty": "^4.2.1",
"mustache": "^3.0.1",
@@ -177,6 +178,7 @@
"pretty-bytes": "^5.3.0",
"promise": "^8.0.1",
"query-string": "^5.1.1",
"re-resizable": "^6.5.4",
"react": "^16.9.0",
"react-datetime": "^2.14.0",
"react-dom": "^16.9.0",
@@ -188,13 +190,17 @@
"redux": "^3.7.2",
"relative": "^3.0.2",
"reselect": "^4.0.0",
"roboto-fontface": "^0.10.0",
"sax": "^1.2.4",
"server-destroy": "^1.0.1",
"slug": "^3.3.4",
"smalltalk": "^2.5.1",
"sprintf-js": "^1.1.1",
"sqlite3": "^4.1.1",
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.1",
"styled-components": "^5.1.1",
"styled-system": "^5.1.5",
"syswide-cas": "^5.1.0",
"taboverride": "^4.0.3",
"tar": "^4.4.4",
@@ -202,7 +208,6 @@
"tinymce": "^5.2.0",
"uglifycss": "0.0.29",
"url-parse": "^1.4.3",
"uslug": "^1.0.4",
"uuid": "^3.2.1",
"valid-url": "^1.0.9",
"xml2js": "^0.4.19"

View File

@@ -33,6 +33,8 @@ class Dialog extends React.PureComponent {
constructor() {
super();
this.fuzzy_ = false;
this.state = {
query: '',
results: [],
@@ -58,11 +60,11 @@ class Dialog extends React.PureComponent {
}
style() {
const styleKey = [this.props.theme, this.state.resultsInBody ? '1' : '0'].join('-');
const styleKey = [this.props.themeId, this.state.resultsInBody ? '1' : '0'].join('-');
if (this.styles_[styleKey]) return this.styles_[styleKey];
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
const itemHeight = this.state.resultsInBody ? 84 : 64;
@@ -118,11 +120,21 @@ class Dialog extends React.PureComponent {
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
this.props.dispatch({
type: 'VISIBLE_DIALOGS_ADD',
name: 'gotoAnything',
});
}
componentWillUnmount() {
if (this.listUpdateIID_) clearTimeout(this.listUpdateIID_);
document.removeEventListener('keydown', this.onKeyDown);
this.props.dispatch({
type: 'VISIBLE_DIALOGS_REMOVE',
name: 'gotoAnything',
});
}
onKeyDown(event) {
@@ -178,7 +190,7 @@ class Dialog extends React.PureComponent {
}
async keywords(searchQuery) {
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, false);
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, this.fuzzy_);
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
}
@@ -215,7 +227,7 @@ class Dialog extends React.PureComponent {
} else { // Note TITLE or BODY
listType = BaseModel.TYPE_NOTE;
searchQuery = this.makeSearchQuery(this.state.query);
results = await SearchEngine.instance().search(searchQuery);
results = await SearchEngine.instance().search(searchQuery, { fuzzy: this.fuzzy_ });
resultsInBody = !!results.find(row => row.fields.includes('body'));
@@ -341,7 +353,7 @@ class Dialog extends React.PureComponent {
}
listItemRenderer(item) {
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
const style = this.style();
const rowStyle = item.id === this.state.selectedItemId ? style.rowSelected : style.row;
const titleHtml = item.fragments
@@ -430,7 +442,7 @@ class Dialog extends React.PureComponent {
}
render() {
const theme = themeStyle(this.props.theme);
const theme = themeStyle(this.props.themeId);
const style = this.style();
const helpComp = !this.state.showHelp ? null : <div style={style.help}>{_('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.')}</div>;
@@ -453,7 +465,7 @@ class Dialog extends React.PureComponent {
const mapStateToProps = (state) => {
return {
folders: state.folders,
theme: state.settings.theme,
themeId: state.settings.theme,
showCompletedTodos: state.settings.showCompletedTodos,
highlightedWords: state.highlightedWords,
};

View File

@@ -38,18 +38,18 @@ a {
::-webkit-scrollbar-track {
border: none;
}
::-webkit-scrollbar-thumb {
background: rgba(100, 100, 100, 0.3);
background: rgba(100, 100, 100, 0.3);
border-radius: 5px;
}
::-webkit-scrollbar-track:hover {
background: rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(100, 100, 100, 0.7);
background: rgba(100, 100, 100, 0.7);
}
.fade_out {
@@ -64,20 +64,13 @@ a {
opacity: 1;
}
/*
.note-list .list-item-container:hover {
background-color: rgba(0,160,255,0.1) !important;
}
*/
/*.side-bar .list-item:hover,
.side-bar .synchronize-button:hover {
background-color: #01427B;
}
.side-bar .list-item:active,
.side-bar .synchronize-button:active {
background-color: #0465BB;
}*/
/*
.editor-toolbar .button:not(.disabled):hover,
.header .button:not(.disabled):hover {
background-color: rgba(0,160,255,0.1);
@@ -91,6 +84,7 @@ a {
border: 1px solid rgba(0,160,255,0.7);
box-sizing: 'border-box';
}
*/
.editor-toolbar .button,
.header .button {
@@ -163,3 +157,12 @@ a {
from {transform: rotate(0deg);}
to {transform: rotate(360deg);}
}
/* .joplin-tinymce .tox-editor-header {
padding-left: 88px;
padding-right: 150px;
} */
*:focus {
outline: none;
}

Binary file not shown.

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