You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-01-05 00:12:33 +02:00
Compare commits
72 Commits
v2.4.2
...
theme_to_c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99b5847244 | ||
|
|
4ad7e1b245 | ||
|
|
b6f889baca | ||
|
|
54fb4b0946 | ||
|
|
80762572cf | ||
|
|
8e5d209d3c | ||
|
|
f71dad6d09 | ||
|
|
f5891dfae8 | ||
|
|
736bbbd8ed | ||
|
|
973121addd | ||
|
|
95ad4c3177 | ||
|
|
71c470f59d | ||
|
|
c529b972e3 | ||
|
|
e6bff3f2e0 | ||
|
|
1bc674a1f9 | ||
|
|
f371bb8e59 | ||
|
|
7c85889c1f | ||
|
|
ab134807ea | ||
|
|
a5b3bb6058 | ||
|
|
8ab1cd984c | ||
|
|
e387d9a91b | ||
|
|
0793b1be59 | ||
|
|
19225abbcf | ||
|
|
85fa3288ab | ||
|
|
17f82c426a | ||
|
|
82331c9b93 | ||
|
|
886b6d1126 | ||
|
|
1a703c4ecd | ||
|
|
1126899769 | ||
|
|
a571d38862 | ||
|
|
4ab2fb73b7 | ||
|
|
78c7b79299 | ||
|
|
d97ba57dda | ||
|
|
9c44133bd0 | ||
|
|
dc008ecf64 | ||
|
|
93a4ad09bb | ||
|
|
9c1dc7898a | ||
|
|
6520a481ca | ||
|
|
5805a41249 | ||
|
|
b88b747ba6 | ||
|
|
ce89ee5bab | ||
|
|
596f679b1f | ||
|
|
78d5fd1385 | ||
|
|
0a98854e43 | ||
|
|
a8a0dd2dd6 | ||
|
|
ad931a738f | ||
|
|
3b6e6e45cf | ||
|
|
dec0a08954 | ||
|
|
b269c2fdb9 | ||
|
|
ad51090cdb | ||
|
|
bd4714037c | ||
|
|
f42fd0ecce | ||
|
|
62c5f433d7 | ||
|
|
e73a4b7286 | ||
|
|
6c18c6ddc7 | ||
|
|
20d1f74ee4 | ||
|
|
2386abea3e | ||
|
|
90621a8417 | ||
|
|
b02baa6891 | ||
|
|
ee46978389 | ||
|
|
2b6b4dd916 | ||
|
|
f0361bf80d | ||
|
|
ecf718005d | ||
|
|
305d0ffc49 | ||
|
|
f454c4e33b | ||
|
|
047883bd27 | ||
|
|
f118f5250f | ||
|
|
61161039c8 | ||
|
|
00504898f2 | ||
|
|
0a6390ed96 | ||
|
|
f909fe6670 | ||
|
|
b17d8bc533 |
@@ -64,6 +64,7 @@ packages/tools/PortableAppsLauncher
|
||||
packages/fork-*
|
||||
plugin_types/
|
||||
readme/
|
||||
**/commands/index.ts
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
packages/app-cli/app/LinkSelector.d.ts
|
||||
@@ -117,6 +118,12 @@ packages/app-desktop/InteropServiceHelper.js.map
|
||||
packages/app-desktop/app.d.ts
|
||||
packages/app-desktop/app.js
|
||||
packages/app-desktop/app.js.map
|
||||
packages/app-desktop/app.reducer.d.ts
|
||||
packages/app-desktop/app.reducer.js
|
||||
packages/app-desktop/app.reducer.js.map
|
||||
packages/app-desktop/app.reducer.test.d.ts
|
||||
packages/app-desktop/app.reducer.test.js
|
||||
packages/app-desktop/app.reducer.test.js.map
|
||||
packages/app-desktop/bridge.d.ts
|
||||
packages/app-desktop/bridge.js
|
||||
packages/app-desktop/bridge.js.map
|
||||
@@ -135,6 +142,9 @@ packages/app-desktop/commands/exportNotes.js.map
|
||||
packages/app-desktop/commands/focusElement.d.ts
|
||||
packages/app-desktop/commands/focusElement.js
|
||||
packages/app-desktop/commands/focusElement.js.map
|
||||
packages/app-desktop/commands/index.d.ts
|
||||
packages/app-desktop/commands/index.js
|
||||
packages/app-desktop/commands/index.js.map
|
||||
packages/app-desktop/commands/openProfileDirectory.d.ts
|
||||
packages/app-desktop/commands/openProfileDirectory.js
|
||||
packages/app-desktop/commands/openProfileDirectory.js.map
|
||||
@@ -240,6 +250,9 @@ packages/app-desktop/gui/MainScreen/commands/gotoAnything.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/hideModalMessage.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/MainScreen/commands/hideModalMessage.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/index.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/index.js
|
||||
packages/app-desktop/gui/MainScreen/commands/index.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/moveToFolder.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/moveToFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/moveToFolder.js.map
|
||||
@@ -396,21 +409,24 @@ packages/app-desktop/gui/NoteEditor/NoteEditor.js.map
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/index.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/index.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/index.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/showRevisions.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js.map
|
||||
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js
|
||||
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js.map
|
||||
packages/app-desktop/gui/NoteEditor/styles/index.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/styles/index.js
|
||||
packages/app-desktop/gui/NoteEditor/styles/index.js.map
|
||||
@@ -465,12 +481,18 @@ packages/app-desktop/gui/NoteList/NoteList.js.map
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.d.ts
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js.map
|
||||
packages/app-desktop/gui/NoteList/commands/index.d.ts
|
||||
packages/app-desktop/gui/NoteList/commands/index.js
|
||||
packages/app-desktop/gui/NoteList/commands/index.js.map
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.d.ts
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js.map
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.d.ts
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js.map
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.d.ts
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js.map
|
||||
packages/app-desktop/gui/NoteListItem.d.ts
|
||||
packages/app-desktop/gui/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem.js.map
|
||||
@@ -567,12 +589,18 @@ packages/app-desktop/gui/Sidebar/Sidebar.js.map
|
||||
packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.d.ts
|
||||
packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js
|
||||
packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js.map
|
||||
packages/app-desktop/gui/Sidebar/commands/index.d.ts
|
||||
packages/app-desktop/gui/Sidebar/commands/index.js
|
||||
packages/app-desktop/gui/Sidebar/commands/index.js.map
|
||||
packages/app-desktop/gui/Sidebar/styles/index.d.ts
|
||||
packages/app-desktop/gui/Sidebar/styles/index.js
|
||||
packages/app-desktop/gui/Sidebar/styles/index.js.map
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.d.ts
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.js
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.js.map
|
||||
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.d.ts
|
||||
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js
|
||||
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.js
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.js.map
|
||||
@@ -861,6 +889,9 @@ packages/lib/JoplinServerApi.js.map
|
||||
packages/lib/Logger.d.ts
|
||||
packages/lib/Logger.js
|
||||
packages/lib/Logger.js.map
|
||||
packages/lib/ObjectUtils.d.ts
|
||||
packages/lib/ObjectUtils.js
|
||||
packages/lib/ObjectUtils.js.map
|
||||
packages/lib/PoorManIntervals.d.ts
|
||||
packages/lib/PoorManIntervals.js
|
||||
packages/lib/PoorManIntervals.js.map
|
||||
@@ -894,6 +925,9 @@ packages/lib/commands/historyBackward.js.map
|
||||
packages/lib/commands/historyForward.d.ts
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/historyForward.js.map
|
||||
packages/lib/commands/index.d.ts
|
||||
packages/lib/commands/index.js
|
||||
packages/lib/commands/index.js.map
|
||||
packages/lib/commands/synchronize.d.ts
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/synchronize.js.map
|
||||
@@ -936,6 +970,9 @@ packages/lib/fs-driver-node.js.map
|
||||
packages/lib/fsDriver.test.d.ts
|
||||
packages/lib/fsDriver.test.js
|
||||
packages/lib/fsDriver.test.js.map
|
||||
packages/lib/hooks/useAsyncEffect.d.ts
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useAsyncEffect.js.map
|
||||
packages/lib/hooks/useElementSize.d.ts
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useElementSize.js.map
|
||||
@@ -990,6 +1027,9 @@ packages/lib/models/Folder.test.js.map
|
||||
packages/lib/models/ItemChange.d.ts
|
||||
packages/lib/models/ItemChange.js
|
||||
packages/lib/models/ItemChange.js.map
|
||||
packages/lib/models/ItemChange.test.d.ts
|
||||
packages/lib/models/ItemChange.test.js
|
||||
packages/lib/models/ItemChange.test.js.map
|
||||
packages/lib/models/MasterKey.d.ts
|
||||
packages/lib/models/MasterKey.js
|
||||
packages/lib/models/MasterKey.js.map
|
||||
@@ -1404,6 +1444,12 @@ packages/lib/services/rest/actionApi.desktop.js.map
|
||||
packages/lib/services/rest/routes/auth.d.ts
|
||||
packages/lib/services/rest/routes/auth.js
|
||||
packages/lib/services/rest/routes/auth.js.map
|
||||
packages/lib/services/rest/routes/events.d.ts
|
||||
packages/lib/services/rest/routes/events.js
|
||||
packages/lib/services/rest/routes/events.js.map
|
||||
packages/lib/services/rest/routes/events.test.d.ts
|
||||
packages/lib/services/rest/routes/events.test.js
|
||||
packages/lib/services/rest/routes/events.test.js.map
|
||||
packages/lib/services/rest/routes/folders.d.ts
|
||||
packages/lib/services/rest/routes/folders.js
|
||||
packages/lib/services/rest/routes/folders.js.map
|
||||
@@ -1485,6 +1531,21 @@ packages/lib/services/spellChecker/SpellCheckerService.js.map
|
||||
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.d.ts
|
||||
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.js
|
||||
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.js.map
|
||||
packages/lib/services/style/cssToTheme.d.ts
|
||||
packages/lib/services/style/cssToTheme.js
|
||||
packages/lib/services/style/cssToTheme.js.map
|
||||
packages/lib/services/style/cssToTheme.test.d.ts
|
||||
packages/lib/services/style/cssToTheme.test.js
|
||||
packages/lib/services/style/cssToTheme.test.js.map
|
||||
packages/lib/services/style/loadCssToTheme.d.ts
|
||||
packages/lib/services/style/loadCssToTheme.js
|
||||
packages/lib/services/style/loadCssToTheme.js.map
|
||||
packages/lib/services/style/themeToCss.d.ts
|
||||
packages/lib/services/style/themeToCss.js
|
||||
packages/lib/services/style/themeToCss.js.map
|
||||
packages/lib/services/style/themeToCss.test.d.ts
|
||||
packages/lib/services/style/themeToCss.test.js
|
||||
packages/lib/services/style/themeToCss.test.js.map
|
||||
packages/lib/services/synchronizer/ItemUploader.d.ts
|
||||
packages/lib/services/synchronizer/ItemUploader.js
|
||||
packages/lib/services/synchronizer/ItemUploader.js.map
|
||||
@@ -1635,6 +1696,12 @@ packages/plugin-repo-cli/lib/gitCompareUrl.js.map
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.test.d.ts
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.test.js
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.test.js.map
|
||||
packages/plugin-repo-cli/lib/overrideUtils.d.ts
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js.map
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.d.ts
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js.map
|
||||
packages/plugin-repo-cli/lib/types.d.ts
|
||||
packages/plugin-repo-cli/lib/types.js
|
||||
packages/plugin-repo-cli/lib/types.js.map
|
||||
@@ -1644,6 +1711,9 @@ packages/plugin-repo-cli/lib/updateReadme.js.map
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.d.ts
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js.map
|
||||
packages/plugin-repo-cli/lib/utils.d.ts
|
||||
packages/plugin-repo-cli/lib/utils.js
|
||||
packages/plugin-repo-cli/lib/utils.js.map
|
||||
packages/plugins/ToggleSidebars/api/index.d.ts
|
||||
packages/plugins/ToggleSidebars/api/index.js
|
||||
packages/plugins/ToggleSidebars/api/index.js.map
|
||||
@@ -1716,6 +1786,9 @@ packages/renderer/MdToHtml/setupLinkify.js.map
|
||||
packages/renderer/MdToHtml/validateLinks.d.ts
|
||||
packages/renderer/MdToHtml/validateLinks.js
|
||||
packages/renderer/MdToHtml/validateLinks.js.map
|
||||
packages/renderer/headerAnchor.d.ts
|
||||
packages/renderer/headerAnchor.js
|
||||
packages/renderer/headerAnchor.js.map
|
||||
packages/renderer/htmlUtils.d.ts
|
||||
packages/renderer/htmlUtils.js
|
||||
packages/renderer/htmlUtils.js.map
|
||||
@@ -1734,12 +1807,18 @@ packages/renderer/utils.js.map
|
||||
packages/tools/buildServerDocker.d.ts
|
||||
packages/tools/buildServerDocker.js
|
||||
packages/tools/buildServerDocker.js.map
|
||||
packages/tools/convertThemesToCss.d.ts
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/convertThemesToCss.js.map
|
||||
packages/tools/generate-database-types.d.ts
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-database-types.js.map
|
||||
packages/tools/generate-images.d.ts
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/generate-images.js.map
|
||||
packages/tools/git-changelog.d.ts
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/git-changelog.js.map
|
||||
packages/tools/lerna-add.d.ts
|
||||
packages/tools/lerna-add.js
|
||||
packages/tools/lerna-add.js.map
|
||||
|
||||
8
.github/workflows/github-actions-main.yml
vendored
8
.github/workflows/github-actions-main.yml
vendored
@@ -35,6 +35,14 @@ jobs:
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
|
||||
# the next line enables multi-architecture support for docker, it basically makes it use qemu for non native platforms
|
||||
# See https://hub.docker.com/r/tonistiigi/binfmt for more info
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
|
||||
# this just prints the info about what platforms are supported in the builder (can help debugging if something isn't working right)
|
||||
# and also proves the above worked properly
|
||||
sudo docker buildx ls
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: olegtarasov/get-tag@v2.1
|
||||
- uses: actions/setup-node@v2
|
||||
|
||||
85
.gitignore
vendored
85
.gitignore
vendored
@@ -49,6 +49,7 @@ packages/tools/commit_hook.txt
|
||||
packages/tools/github_oauth_token.txt
|
||||
lerna-debug.log
|
||||
.env
|
||||
docs/**/*.mustache
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
packages/app-cli/app/LinkSelector.d.ts
|
||||
@@ -102,6 +103,12 @@ packages/app-desktop/InteropServiceHelper.js.map
|
||||
packages/app-desktop/app.d.ts
|
||||
packages/app-desktop/app.js
|
||||
packages/app-desktop/app.js.map
|
||||
packages/app-desktop/app.reducer.d.ts
|
||||
packages/app-desktop/app.reducer.js
|
||||
packages/app-desktop/app.reducer.js.map
|
||||
packages/app-desktop/app.reducer.test.d.ts
|
||||
packages/app-desktop/app.reducer.test.js
|
||||
packages/app-desktop/app.reducer.test.js.map
|
||||
packages/app-desktop/bridge.d.ts
|
||||
packages/app-desktop/bridge.js
|
||||
packages/app-desktop/bridge.js.map
|
||||
@@ -120,6 +127,9 @@ packages/app-desktop/commands/exportNotes.js.map
|
||||
packages/app-desktop/commands/focusElement.d.ts
|
||||
packages/app-desktop/commands/focusElement.js
|
||||
packages/app-desktop/commands/focusElement.js.map
|
||||
packages/app-desktop/commands/index.d.ts
|
||||
packages/app-desktop/commands/index.js
|
||||
packages/app-desktop/commands/index.js.map
|
||||
packages/app-desktop/commands/openProfileDirectory.d.ts
|
||||
packages/app-desktop/commands/openProfileDirectory.js
|
||||
packages/app-desktop/commands/openProfileDirectory.js.map
|
||||
@@ -225,6 +235,9 @@ packages/app-desktop/gui/MainScreen/commands/gotoAnything.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/hideModalMessage.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/MainScreen/commands/hideModalMessage.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/index.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/index.js
|
||||
packages/app-desktop/gui/MainScreen/commands/index.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/moveToFolder.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/moveToFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/moveToFolder.js.map
|
||||
@@ -381,21 +394,24 @@ packages/app-desktop/gui/NoteEditor/NoteEditor.js.map
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/index.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/index.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/index.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js.map
|
||||
packages/app-desktop/gui/NoteEditor/commands/showRevisions.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js.map
|
||||
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js
|
||||
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js.map
|
||||
packages/app-desktop/gui/NoteEditor/styles/index.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/styles/index.js
|
||||
packages/app-desktop/gui/NoteEditor/styles/index.js.map
|
||||
@@ -450,12 +466,18 @@ packages/app-desktop/gui/NoteList/NoteList.js.map
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.d.ts
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js.map
|
||||
packages/app-desktop/gui/NoteList/commands/index.d.ts
|
||||
packages/app-desktop/gui/NoteList/commands/index.js
|
||||
packages/app-desktop/gui/NoteList/commands/index.js.map
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.d.ts
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js.map
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.d.ts
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js.map
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.d.ts
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js.map
|
||||
packages/app-desktop/gui/NoteListItem.d.ts
|
||||
packages/app-desktop/gui/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem.js.map
|
||||
@@ -552,12 +574,18 @@ packages/app-desktop/gui/Sidebar/Sidebar.js.map
|
||||
packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.d.ts
|
||||
packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js
|
||||
packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js.map
|
||||
packages/app-desktop/gui/Sidebar/commands/index.d.ts
|
||||
packages/app-desktop/gui/Sidebar/commands/index.js
|
||||
packages/app-desktop/gui/Sidebar/commands/index.js.map
|
||||
packages/app-desktop/gui/Sidebar/styles/index.d.ts
|
||||
packages/app-desktop/gui/Sidebar/styles/index.js
|
||||
packages/app-desktop/gui/Sidebar/styles/index.js.map
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.d.ts
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.js
|
||||
packages/app-desktop/gui/StatusScreen/StatusScreen.js.map
|
||||
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.d.ts
|
||||
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js
|
||||
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.js
|
||||
packages/app-desktop/gui/SyncWizard/Dialog.js.map
|
||||
@@ -846,6 +874,9 @@ packages/lib/JoplinServerApi.js.map
|
||||
packages/lib/Logger.d.ts
|
||||
packages/lib/Logger.js
|
||||
packages/lib/Logger.js.map
|
||||
packages/lib/ObjectUtils.d.ts
|
||||
packages/lib/ObjectUtils.js
|
||||
packages/lib/ObjectUtils.js.map
|
||||
packages/lib/PoorManIntervals.d.ts
|
||||
packages/lib/PoorManIntervals.js
|
||||
packages/lib/PoorManIntervals.js.map
|
||||
@@ -879,6 +910,9 @@ packages/lib/commands/historyBackward.js.map
|
||||
packages/lib/commands/historyForward.d.ts
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/historyForward.js.map
|
||||
packages/lib/commands/index.d.ts
|
||||
packages/lib/commands/index.js
|
||||
packages/lib/commands/index.js.map
|
||||
packages/lib/commands/synchronize.d.ts
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/synchronize.js.map
|
||||
@@ -921,6 +955,9 @@ packages/lib/fs-driver-node.js.map
|
||||
packages/lib/fsDriver.test.d.ts
|
||||
packages/lib/fsDriver.test.js
|
||||
packages/lib/fsDriver.test.js.map
|
||||
packages/lib/hooks/useAsyncEffect.d.ts
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useAsyncEffect.js.map
|
||||
packages/lib/hooks/useElementSize.d.ts
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useElementSize.js.map
|
||||
@@ -975,6 +1012,9 @@ packages/lib/models/Folder.test.js.map
|
||||
packages/lib/models/ItemChange.d.ts
|
||||
packages/lib/models/ItemChange.js
|
||||
packages/lib/models/ItemChange.js.map
|
||||
packages/lib/models/ItemChange.test.d.ts
|
||||
packages/lib/models/ItemChange.test.js
|
||||
packages/lib/models/ItemChange.test.js.map
|
||||
packages/lib/models/MasterKey.d.ts
|
||||
packages/lib/models/MasterKey.js
|
||||
packages/lib/models/MasterKey.js.map
|
||||
@@ -1389,6 +1429,12 @@ packages/lib/services/rest/actionApi.desktop.js.map
|
||||
packages/lib/services/rest/routes/auth.d.ts
|
||||
packages/lib/services/rest/routes/auth.js
|
||||
packages/lib/services/rest/routes/auth.js.map
|
||||
packages/lib/services/rest/routes/events.d.ts
|
||||
packages/lib/services/rest/routes/events.js
|
||||
packages/lib/services/rest/routes/events.js.map
|
||||
packages/lib/services/rest/routes/events.test.d.ts
|
||||
packages/lib/services/rest/routes/events.test.js
|
||||
packages/lib/services/rest/routes/events.test.js.map
|
||||
packages/lib/services/rest/routes/folders.d.ts
|
||||
packages/lib/services/rest/routes/folders.js
|
||||
packages/lib/services/rest/routes/folders.js.map
|
||||
@@ -1470,6 +1516,21 @@ packages/lib/services/spellChecker/SpellCheckerService.js.map
|
||||
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.d.ts
|
||||
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.js
|
||||
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.js.map
|
||||
packages/lib/services/style/cssToTheme.d.ts
|
||||
packages/lib/services/style/cssToTheme.js
|
||||
packages/lib/services/style/cssToTheme.js.map
|
||||
packages/lib/services/style/cssToTheme.test.d.ts
|
||||
packages/lib/services/style/cssToTheme.test.js
|
||||
packages/lib/services/style/cssToTheme.test.js.map
|
||||
packages/lib/services/style/loadCssToTheme.d.ts
|
||||
packages/lib/services/style/loadCssToTheme.js
|
||||
packages/lib/services/style/loadCssToTheme.js.map
|
||||
packages/lib/services/style/themeToCss.d.ts
|
||||
packages/lib/services/style/themeToCss.js
|
||||
packages/lib/services/style/themeToCss.js.map
|
||||
packages/lib/services/style/themeToCss.test.d.ts
|
||||
packages/lib/services/style/themeToCss.test.js
|
||||
packages/lib/services/style/themeToCss.test.js.map
|
||||
packages/lib/services/synchronizer/ItemUploader.d.ts
|
||||
packages/lib/services/synchronizer/ItemUploader.js
|
||||
packages/lib/services/synchronizer/ItemUploader.js.map
|
||||
@@ -1620,6 +1681,12 @@ packages/plugin-repo-cli/lib/gitCompareUrl.js.map
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.test.d.ts
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.test.js
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.test.js.map
|
||||
packages/plugin-repo-cli/lib/overrideUtils.d.ts
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js.map
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.d.ts
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js.map
|
||||
packages/plugin-repo-cli/lib/types.d.ts
|
||||
packages/plugin-repo-cli/lib/types.js
|
||||
packages/plugin-repo-cli/lib/types.js.map
|
||||
@@ -1629,6 +1696,9 @@ packages/plugin-repo-cli/lib/updateReadme.js.map
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.d.ts
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js.map
|
||||
packages/plugin-repo-cli/lib/utils.d.ts
|
||||
packages/plugin-repo-cli/lib/utils.js
|
||||
packages/plugin-repo-cli/lib/utils.js.map
|
||||
packages/plugins/ToggleSidebars/api/index.d.ts
|
||||
packages/plugins/ToggleSidebars/api/index.js
|
||||
packages/plugins/ToggleSidebars/api/index.js.map
|
||||
@@ -1701,6 +1771,9 @@ packages/renderer/MdToHtml/setupLinkify.js.map
|
||||
packages/renderer/MdToHtml/validateLinks.d.ts
|
||||
packages/renderer/MdToHtml/validateLinks.js
|
||||
packages/renderer/MdToHtml/validateLinks.js.map
|
||||
packages/renderer/headerAnchor.d.ts
|
||||
packages/renderer/headerAnchor.js
|
||||
packages/renderer/headerAnchor.js.map
|
||||
packages/renderer/htmlUtils.d.ts
|
||||
packages/renderer/htmlUtils.js
|
||||
packages/renderer/htmlUtils.js.map
|
||||
@@ -1719,12 +1792,18 @@ packages/renderer/utils.js.map
|
||||
packages/tools/buildServerDocker.d.ts
|
||||
packages/tools/buildServerDocker.js
|
||||
packages/tools/buildServerDocker.js.map
|
||||
packages/tools/convertThemesToCss.d.ts
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/convertThemesToCss.js.map
|
||||
packages/tools/generate-database-types.d.ts
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-database-types.js.map
|
||||
packages/tools/generate-images.d.ts
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/generate-images.js.map
|
||||
packages/tools/git-changelog.d.ts
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/git-changelog.js.map
|
||||
packages/tools/lerna-add.d.ts
|
||||
packages/tools/lerna-add.js
|
||||
packages/tools/lerna-add.js.map
|
||||
|
||||
@@ -29,7 +29,7 @@ ol, ul {
|
||||
#main-container {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 225px;
|
||||
padding-bottom: 127px; /* Needs to be the height of the footer */
|
||||
}
|
||||
|
||||
.help-page-container img {
|
||||
@@ -583,7 +583,7 @@ div.navbar-mobile-content a.sponsor-button {
|
||||
|
||||
/* footer section */
|
||||
footer {
|
||||
padding-top: 50px;
|
||||
padding-top: 30px;
|
||||
padding-bottom: 30px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -594,6 +594,7 @@ footer a,
|
||||
footer p {
|
||||
color: #90b1d9;
|
||||
text-decoration: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
@@ -617,6 +618,35 @@ footer .right-links a {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
footer .footer-right {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
footer .social-links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #315885;
|
||||
}
|
||||
|
||||
footer .social-links a:hover {
|
||||
color: #e8f3ff;
|
||||
}
|
||||
|
||||
footer .social-links i {
|
||||
margin-left: 15px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
footer .bottom-links-row {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
footer .bottom-links-row p {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
IN THE PRESS
|
||||
The "In the press" section height needs to be adjusted as the
|
||||
@@ -679,7 +709,7 @@ footer .right-links a {
|
||||
#main-container {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 260px;
|
||||
padding-bottom: 130px;
|
||||
}
|
||||
|
||||
.front-page h1 {
|
||||
@@ -866,7 +896,7 @@ footer .right-links a {
|
||||
|
||||
.help-page-container {
|
||||
padding-top: 90px;
|
||||
padding-bottom: 50px;
|
||||
padding-bottom: 70px;
|
||||
}
|
||||
|
||||
.donate-links {
|
||||
@@ -880,7 +910,7 @@ footer .right-links a {
|
||||
border-radius: 20px;
|
||||
padding: 30px 20px;
|
||||
padding-bottom: 30px;
|
||||
margin-bottom: 50px;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
@@ -923,6 +953,11 @@ footer .right-links a {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.plan-group .joplin-cloud-login-info {
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.plan-group .plan-price-yearly-per-year {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -368,40 +368,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="darkblue-bg">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-3 d-none d-md-block">
|
||||
<img src="{{imageBaseUrl}}/logo-text.svg" alt="" width="150" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<a href="{{baseUrl}}">
|
||||
<img
|
||||
src="{{imageBaseUrl}}/logo-text.svg"
|
||||
width="120"
|
||||
class="img-center d-block d-md-none"
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
<br class="d-block d-md-none" />
|
||||
<p class="text-center-sm">Copyright © 2016-{{yyyy}} Laurent Cozic</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p class="text-right text-center-sm right-links">
|
||||
<a href="https://github.com/laurent22/joplin/" class="github-link"><i class="fab fa-github"></i> GitHub Repository</a>
|
||||
<a href="{{baseUrl}}/privacy/">Privacy Policy</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
{{> footer}}
|
||||
</div>
|
||||
|
||||
<script
|
||||
|
||||
@@ -75,38 +75,8 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="darkblue-bg">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-3 d-none d-md-block">
|
||||
<img src="{{imageBaseUrl}}/logo-text.svg" alt="" width="150" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<img
|
||||
src="{{imageBaseUrl}}/logo-text.svg"
|
||||
width="120"
|
||||
class="img-center d-block d-md-none"
|
||||
alt=""
|
||||
/>
|
||||
<br class="d-block d-md-none" />
|
||||
<p class="text-center-sm">Copyright © 2016-{{yyyy}} Laurent Cozic</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 right-links">
|
||||
<p class="text-right text-center-sm">
|
||||
<a href="https://github.com/laurent22/joplin/" class="github-link"><i class="fab fa-github"></i> GitHub Repository</a>
|
||||
<a href="{{baseUrl}}/privacy/">Privacy Policy</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{{> footer}}
|
||||
</div>
|
||||
|
||||
<script src="{{jsBaseUrl}}/script.js?t={{buildTime}}"></script>
|
||||
|
||||
26
Assets/WebsiteAssets/templates/partials/footer.mustache
Normal file
26
Assets/WebsiteAssets/templates/partials/footer.mustache
Normal file
@@ -0,0 +1,26 @@
|
||||
<footer class="darkblue-bg">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-12 social-links">
|
||||
<a href="https://twitter.com/joplinapp" title="Twitter feed"><i class="fab fa-twitter"></i></a>
|
||||
<a href="https://github.com/laurent22/joplin/" title="GitHub repository"><i class="fab fa-github"></i></a>
|
||||
<a href="https://www.patreon.com/joplin" title="Patreon blog"><i class="fab fa-patreon"></i></a>
|
||||
<a href="https://discordapp.com/invite/d2HMPwE" title="Discord chat"><i class="fab fa-discord"></i></a>
|
||||
<a href="https://www.reddit.com/r/joplinapp/" title="Subreddit"><i class="fab fa-reddit"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row bottom-links-row">
|
||||
<div class="col-12 col-md-6">
|
||||
<p class="text-center-sm">Copyright © 2016-{{yyyy}} Laurent Cozic</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p class="text-right text-center-sm right-links">
|
||||
<span class="footer-right">
|
||||
<a href="{{baseUrl}}/privacy/">Privacy Policy</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -6,7 +6,7 @@
|
||||
Joplin Cloud <span class="frame-bg frame-bg-yellow">plans</span>
|
||||
</h1>
|
||||
<p class="text-center sub-title">
|
||||
Joplin Cloud allows you to synchronise your notes across devices. It also lets you publish notes, and collaborate on notebooks with your friends, family or colleagues.
|
||||
<a href="https://joplincloud.com">Joplin Cloud</a> allows you to synchronise your notes across devices. It also lets you publish notes, and collaborate on notebooks with your friends, family or colleagues.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,6 +45,8 @@
|
||||
{{#plans.business}}
|
||||
{{> plan}}
|
||||
{{/plans.business}}
|
||||
|
||||
<p class="joplin-cloud-login-info">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -184,7 +184,7 @@ if command -v lsb_release &> /dev/null; then
|
||||
# Check for "The SUID sandbox helper binary was found, but is not configured correctly" problem.
|
||||
# It is present in Debian 1X. A (temporary) patch will be applied at .desktop file
|
||||
# Linux Mint 4 Debbie is based on Debian 10 and requires the same param handling.
|
||||
if [ $DISTVER =~ Debian1. ] || [ "$DISTVER" = "Linuxmint4" ] && [ "$DISTCODENAME" = "debbie" ]
|
||||
if [[ $DISTVER =~ Debian1. ]] || [ "$DISTVER" = "Linuxmint4" ] && [ "$DISTCODENAME" = "debbie" ]
|
||||
then
|
||||
SANDBOXPARAM=" --no-sandbox"
|
||||
fi
|
||||
|
||||
12
README.md
12
README.md
@@ -511,7 +511,7 @@ Current translations:
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 54%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
@@ -527,7 +527,7 @@ Current translations:
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 87%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | Alexander Dawson | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 68%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 67%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Nicolas Suzuki](mailto:nicolas.suzuki@pm.me) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 90%
|
||||
@@ -536,13 +536,13 @@ Current translations:
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 42%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vi.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 92%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 81%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Po-Chiang Chao](mailto:BobChao%29%20%28bobchao@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Po-Chiang Chao](mailto:BobChao%29%20%28bobchao@gmail.com) | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 95%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
@@ -5,6 +5,7 @@ const tasks = {
|
||||
// copyLib: require('./packages/tools/gulp/tasks/copyLib'),
|
||||
// tsc: require('./packages/tools/gulp/tasks/tsc'),
|
||||
updateIgnoredTypeScriptBuild: require('./packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
buildCommandIndex: require('./packages/tools/gulp/tasks/buildCommandIndex'),
|
||||
// deleteBuildDirs: require('./packages/tools/gulp/tasks/deleteBuildDirs'),
|
||||
completePublishAll: {
|
||||
fn: async () => {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"bootstrapServerOnly": "lerna bootstrap --force-local --no-ci --include-dependents --include-dependencies --scope @joplin/server",
|
||||
"build": "lerna run build && npm run tsc",
|
||||
"buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md",
|
||||
"buildCommandIndex": "gulp buildCommandIndex",
|
||||
"buildDoc": "./packages/tools/build-all.sh",
|
||||
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out docs/api/references/plugin_api packages/lib/services/plugins/api/",
|
||||
"buildSettingJsonSchema": "npm start --prefix=packages/app-cli -- settingschema ../../docs/schema/settings.json",
|
||||
|
||||
@@ -381,6 +381,30 @@ async function fetchAllNotes() {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const tableFields = reg.db().tableFields('item_changes', { includeDescription: true });
|
||||
|
||||
lines.push('# Events');
|
||||
lines.push('');
|
||||
lines.push('This end point can be used to retrieve the latest note changes. Currently only note changes are tracked.');
|
||||
lines.push('');
|
||||
lines.push('## Properties');
|
||||
lines.push('');
|
||||
lines.push(this.createPropertiesTable(tableFields));
|
||||
lines.push('');
|
||||
lines.push('## GET /events');
|
||||
lines.push('');
|
||||
lines.push('Returns a paginated list of recent events. A `cursor` property should be provided, which tells from what point in time the events should be returned. The API will return a `cursor` property, to tell from where to resume retrieving events, as well as an `has_more` (tells if more changes can be retrieved) and `items` property, which will contain the list of events. Events are kept for up to 90 days.');
|
||||
lines.push('');
|
||||
lines.push('If no `cursor` property is provided, the API will respond with the latest change ID. That can be used to retrieve future events later on.');
|
||||
lines.push('');
|
||||
lines.push('The results are paginated so will need to may multiple calls to retrieve all the events. Use the `has_more` property to know if more can be retrieved.');
|
||||
lines.push('');
|
||||
lines.push('## GET /events/:id');
|
||||
lines.push('');
|
||||
lines.push('Returns the event with the given ID.');
|
||||
}
|
||||
|
||||
const outFilePath = args['file'];
|
||||
|
||||
await shim.fsDriver().writeFile(outFilePath, lines.join('\n'), 'utf8');
|
||||
|
||||
@@ -237,7 +237,7 @@ export default class ElectronAppWrapper {
|
||||
const iid = setInterval(() => {
|
||||
if (this.electronApp().isReady()) {
|
||||
clearInterval(iid);
|
||||
resolve();
|
||||
resolve(null);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
|
||||
47
packages/app-desktop/app.reducer.test.ts
Normal file
47
packages/app-desktop/app.reducer.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { AppState } from './app.reducer';
|
||||
import appReducer, { createAppDefaultState } from './app.reducer';
|
||||
|
||||
describe('app.reducer', function() {
|
||||
|
||||
it('DIALOG_OPEN', async () => {
|
||||
const state: AppState = createAppDefaultState({}, {});
|
||||
|
||||
let newState = appReducer(state, {
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'syncWizard',
|
||||
});
|
||||
|
||||
expect(newState.dialogs.length).toBe(1);
|
||||
expect(newState.dialogs[0].name).toBe('syncWizard');
|
||||
|
||||
expect(() => appReducer(newState, {
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'syncWizard',
|
||||
})).toThrow();
|
||||
|
||||
newState = appReducer(newState, {
|
||||
type: 'DIALOG_CLOSE',
|
||||
name: 'syncWizard',
|
||||
});
|
||||
|
||||
expect(newState.dialogs.length).toBe(0);
|
||||
|
||||
expect(() => appReducer(newState, {
|
||||
type: 'DIALOG_CLOSE',
|
||||
name: 'syncWizard',
|
||||
})).toThrow();
|
||||
|
||||
newState = appReducer(newState, {
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'syncWizard',
|
||||
});
|
||||
|
||||
newState = appReducer(newState, {
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'setPassword',
|
||||
});
|
||||
|
||||
expect(newState.dialogs).toEqual([{ name: 'syncWizard' }, { name: 'setPassword' }]);
|
||||
});
|
||||
|
||||
});
|
||||
316
packages/app-desktop/app.reducer.ts
Normal file
316
packages/app-desktop/app.reducer.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import produce from 'immer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { defaultState, State } from '@joplin/lib/reducer';
|
||||
import iterateItems from './gui/ResizableLayout/utils/iterateItems';
|
||||
import { LayoutItem } from './gui/ResizableLayout/utils/types';
|
||||
import validateLayout from './gui/ResizableLayout/utils/validateLayout';
|
||||
|
||||
export interface AppStateRoute {
|
||||
type: string;
|
||||
routeName: string;
|
||||
props: any;
|
||||
}
|
||||
|
||||
export enum AppStateDialogName {
|
||||
SyncWizard = 'syncWizard',
|
||||
MasterPassword = 'masterPassword',
|
||||
}
|
||||
|
||||
export interface AppStateDialog {
|
||||
name: AppStateDialogName;
|
||||
}
|
||||
|
||||
export interface AppState extends State {
|
||||
route: AppStateRoute;
|
||||
navHistory: any[];
|
||||
noteVisiblePanes: string[];
|
||||
windowContentSize: any;
|
||||
watchedNoteFiles: string[];
|
||||
lastEditorScrollPercents: any;
|
||||
devToolsVisible: boolean;
|
||||
visibleDialogs: any; // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: string;
|
||||
layoutMoveMode: boolean;
|
||||
startupPluginsLoaded: boolean;
|
||||
|
||||
// Extra reducer keys go here
|
||||
watchedResources: any;
|
||||
mainLayout: LayoutItem;
|
||||
dialogs: AppStateDialog[];
|
||||
}
|
||||
|
||||
export function createAppDefaultState(windowContentSize: any, resourceEditWatcherDefaultState: any): AppState {
|
||||
return {
|
||||
...defaultState,
|
||||
route: {
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Main',
|
||||
props: {},
|
||||
},
|
||||
navHistory: [],
|
||||
noteVisiblePanes: ['editor', 'viewer'],
|
||||
windowContentSize, // bridge().windowContentSize(),
|
||||
watchedNoteFiles: [],
|
||||
lastEditorScrollPercents: {},
|
||||
devToolsVisible: false,
|
||||
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: null,
|
||||
layoutMoveMode: false,
|
||||
mainLayout: null,
|
||||
startupPluginsLoaded: false,
|
||||
dialogs: [],
|
||||
...resourceEditWatcherDefaultState,
|
||||
};
|
||||
}
|
||||
|
||||
export default function(state: AppState, action: any) {
|
||||
let newState = state;
|
||||
|
||||
try {
|
||||
switch (action.type) {
|
||||
|
||||
case 'NAV_BACK':
|
||||
case 'NAV_GO':
|
||||
|
||||
{
|
||||
const goingBack = action.type === 'NAV_BACK';
|
||||
|
||||
if (goingBack && !state.navHistory.length) break;
|
||||
|
||||
const currentRoute = state.route;
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
const newNavHistory = state.navHistory.slice();
|
||||
|
||||
if (goingBack) {
|
||||
let newAction = null;
|
||||
while (newNavHistory.length) {
|
||||
newAction = newNavHistory.pop();
|
||||
if (newAction.routeName !== state.route.routeName) break;
|
||||
}
|
||||
|
||||
if (!newAction) break;
|
||||
|
||||
action = newAction;
|
||||
}
|
||||
|
||||
if (!goingBack) newNavHistory.push(currentRoute);
|
||||
newState.navHistory = newNavHistory;
|
||||
newState.route = action;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'STARTUP_PLUGINS_LOADED':
|
||||
|
||||
// When all startup plugins have loaded, we also recreate the
|
||||
// main layout to ensure that it is updated in the UI. There's
|
||||
// probably a cleaner way to do this, but for now that will do.
|
||||
if (state.startupPluginsLoaded !== action.value) {
|
||||
newState = {
|
||||
...newState,
|
||||
startupPluginsLoaded: action.value,
|
||||
mainLayout: JSON.parse(JSON.stringify(newState.mainLayout)),
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case 'WINDOW_CONTENT_SIZE_SET':
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.windowContentSize = action.size;
|
||||
break;
|
||||
|
||||
case 'NOTE_VISIBLE_PANES_TOGGLE':
|
||||
|
||||
{
|
||||
const getNextLayout = (currentLayout: any) => {
|
||||
currentLayout = panes.length === 2 ? 'both' : currentLayout[0];
|
||||
|
||||
let paneOptions;
|
||||
if (state.settings.layoutButtonSequence === Setting.LAYOUT_EDITOR_VIEWER) {
|
||||
paneOptions = ['editor', 'viewer'];
|
||||
} else if (state.settings.layoutButtonSequence === Setting.LAYOUT_EDITOR_SPLIT) {
|
||||
paneOptions = ['editor', 'both'];
|
||||
} else if (state.settings.layoutButtonSequence === Setting.LAYOUT_VIEWER_SPLIT) {
|
||||
paneOptions = ['viewer', 'both'];
|
||||
} else {
|
||||
paneOptions = ['editor', 'viewer', 'both'];
|
||||
}
|
||||
|
||||
const currentLayoutIndex = paneOptions.indexOf(currentLayout);
|
||||
const nextLayoutIndex = currentLayoutIndex === paneOptions.length - 1 ? 0 : currentLayoutIndex + 1;
|
||||
|
||||
const nextLayout = paneOptions[nextLayoutIndex];
|
||||
return nextLayout === 'both' ? ['editor', 'viewer'] : [nextLayout];
|
||||
};
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
|
||||
const panes = state.noteVisiblePanes.slice();
|
||||
newState.noteVisiblePanes = getNextLayout(panes);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_VISIBLE_PANES_SET':
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.noteVisiblePanes = action.panes;
|
||||
break;
|
||||
|
||||
case 'MAIN_LAYOUT_SET':
|
||||
|
||||
newState = {
|
||||
...state,
|
||||
mainLayout: action.value,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'MAIN_LAYOUT_SET_ITEM_PROP':
|
||||
|
||||
{
|
||||
let newLayout = produce(state.mainLayout, (draftLayout: LayoutItem) => {
|
||||
iterateItems(draftLayout, (_itemIndex: number, item: LayoutItem, _parent: LayoutItem) => {
|
||||
if (item.key === action.itemKey) {
|
||||
(item as any)[action.propName] = action.propValue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
if (newLayout !== state.mainLayout) newLayout = validateLayout(newLayout);
|
||||
|
||||
newState = {
|
||||
...state,
|
||||
mainLayout: newLayout,
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'NOTE_FILE_WATCHER_ADD':
|
||||
|
||||
if (newState.watchedNoteFiles.indexOf(action.id) < 0) {
|
||||
newState = Object.assign({}, state);
|
||||
const watchedNoteFiles = newState.watchedNoteFiles.slice();
|
||||
watchedNoteFiles.push(action.id);
|
||||
newState.watchedNoteFiles = watchedNoteFiles;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_FILE_WATCHER_REMOVE':
|
||||
|
||||
{
|
||||
newState = Object.assign({}, state);
|
||||
const idx = newState.watchedNoteFiles.indexOf(action.id);
|
||||
if (idx >= 0) {
|
||||
const watchedNoteFiles = newState.watchedNoteFiles.slice();
|
||||
watchedNoteFiles.splice(idx, 1);
|
||||
newState.watchedNoteFiles = watchedNoteFiles;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_FILE_WATCHER_CLEAR':
|
||||
|
||||
if (state.watchedNoteFiles.length) {
|
||||
newState = Object.assign({}, state);
|
||||
newState.watchedNoteFiles = [];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'EDITOR_SCROLL_PERCENT_SET':
|
||||
|
||||
{
|
||||
newState = Object.assign({}, state);
|
||||
const newPercents = Object.assign({}, newState.lastEditorScrollPercents);
|
||||
newPercents[action.noteId] = action.percent;
|
||||
newState.lastEditorScrollPercents = newPercents;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_DEVTOOLS_TOGGLE':
|
||||
newState = Object.assign({}, state);
|
||||
newState.devToolsVisible = !newState.devToolsVisible;
|
||||
break;
|
||||
|
||||
case 'NOTE_DEVTOOLS_SET':
|
||||
newState = Object.assign({}, state);
|
||||
newState.devToolsVisible = action.value;
|
||||
break;
|
||||
|
||||
case 'VISIBLE_DIALOGS_ADD':
|
||||
newState = Object.assign({}, state);
|
||||
newState.visibleDialogs = Object.assign({}, newState.visibleDialogs);
|
||||
newState.visibleDialogs[action.name] = true;
|
||||
break;
|
||||
|
||||
case 'VISIBLE_DIALOGS_REMOVE':
|
||||
newState = Object.assign({}, state);
|
||||
newState.visibleDialogs = Object.assign({}, newState.visibleDialogs);
|
||||
delete newState.visibleDialogs[action.name];
|
||||
break;
|
||||
|
||||
case 'FOCUS_SET':
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.focusedField = action.field;
|
||||
break;
|
||||
|
||||
case 'FOCUS_CLEAR':
|
||||
|
||||
// A field can only clear its own state
|
||||
if (action.field === state.focusedField) {
|
||||
newState = Object.assign({}, state);
|
||||
newState.focusedField = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DIALOG_OPEN':
|
||||
case 'DIALOG_CLOSE':
|
||||
|
||||
{
|
||||
let isOpen = true;
|
||||
|
||||
if (action.type === 'DIALOG_CLOSE') {
|
||||
isOpen = false;
|
||||
} else { // DIALOG_OPEN
|
||||
isOpen = action.isOpen !== false;
|
||||
}
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
|
||||
if (isOpen) {
|
||||
const newDialogs = newState.dialogs.slice();
|
||||
|
||||
if (newDialogs.find(d => d.name === action.name)) throw new Error(`Trying to open a dialog is already open: ${action.name}`);
|
||||
|
||||
newDialogs.push({
|
||||
name: action.name,
|
||||
});
|
||||
|
||||
newState.dialogs = newDialogs;
|
||||
} else {
|
||||
if (!newState.dialogs.find(d => d.name === action.name)) throw new Error(`Trying to close a dialog that is not open: ${action.name}`);
|
||||
const newDialogs = newState.dialogs.slice().filter(d => d.name !== action.name);
|
||||
newState.dialogs = newDialogs;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LAYOUT_MOVE_MODE_SET':
|
||||
|
||||
newState = {
|
||||
...state,
|
||||
layoutMoveMode: action.value,
|
||||
};
|
||||
break;
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import CommandService from '@joplin/lib/services/CommandService';
|
||||
import KeymapService from '@joplin/lib/services/KeymapService';
|
||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import resourceEditWatcherReducer, { defaultState as resourceEditWatcherDefaultState } from '@joplin/lib/services/ResourceEditWatcher/reducer';
|
||||
import { defaultState, State } from '@joplin/lib/reducer';
|
||||
import PluginRunner from './services/plugins/PluginRunner';
|
||||
import PlatformImplementation from './services/plugins/PlatformImplementation';
|
||||
import shim from '@joplin/lib/shim';
|
||||
@@ -19,13 +18,10 @@ import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerS
|
||||
import SpellCheckerServiceDriverNative from './services/spellChecker/SpellCheckerServiceDriverNative';
|
||||
import bridge from './services/bridge';
|
||||
import menuCommandNames from './gui/menuCommandNames';
|
||||
import { LayoutItem } from './gui/ResizableLayout/utils/types';
|
||||
import stateToWhenClauseContext from './services/commands/stateToWhenClauseContext';
|
||||
import ResourceService from '@joplin/lib/services/ResourceService';
|
||||
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
|
||||
import produce from 'immer';
|
||||
import iterateItems from './gui/ResizableLayout/utils/iterateItems';
|
||||
import validateLayout from './gui/ResizableLayout/utils/validateLayout';
|
||||
import appReducer, { createAppDefaultState } from './app.reducer';
|
||||
const { FoldersScreenUtils } = require('@joplin/lib/folders-screen-utils.js');
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
const fs = require('fs-extra');
|
||||
@@ -40,125 +36,38 @@ const PluginManager = require('@joplin/lib/services/PluginManager');
|
||||
import RevisionService from '@joplin/lib/services/RevisionService';
|
||||
import MigrationService from '@joplin/lib/services/MigrationService';
|
||||
import { loadCustomCss, injectCustomStyles } from '@joplin/lib/CssUtils';
|
||||
import mainScreenCommands from './gui/MainScreen/commands/index';
|
||||
import noteEditorCommands from './gui/NoteEditor/commands/index';
|
||||
import noteListCommands from './gui/NoteList/commands/index';
|
||||
import noteListControlsCommands from './gui/NoteListControls/commands/index';
|
||||
import sidebarCommands from './gui/Sidebar/commands/index';
|
||||
import appCommands from './commands/index';
|
||||
import libCommands from '@joplin/lib/commands/index';
|
||||
// import populateDatabase from '@joplin/lib/services/debug/populateDatabase';
|
||||
|
||||
const commands = [
|
||||
require('./gui/MainScreen/commands/editAlarm'),
|
||||
require('./gui/MainScreen/commands/exportPdf'),
|
||||
require('./gui/MainScreen/commands/gotoAnything'),
|
||||
require('./gui/MainScreen/commands/commandPalette'),
|
||||
require('./gui/MainScreen/commands/hideModalMessage'),
|
||||
require('./gui/MainScreen/commands/moveToFolder'),
|
||||
require('./gui/MainScreen/commands/newFolder'),
|
||||
require('./gui/MainScreen/commands/newNote'),
|
||||
require('./gui/MainScreen/commands/newSubFolder'),
|
||||
require('./gui/MainScreen/commands/newTodo'),
|
||||
require('./gui/MainScreen/commands/openFolder'),
|
||||
require('./gui/MainScreen/commands/openNote'),
|
||||
require('./gui/MainScreen/commands/openTag'),
|
||||
require('./gui/MainScreen/commands/print'),
|
||||
require('./gui/MainScreen/commands/renameFolder'),
|
||||
require('./gui/MainScreen/commands/renameTag'),
|
||||
require('./gui/MainScreen/commands/search'),
|
||||
require('./gui/MainScreen/commands/setTags'),
|
||||
require('./gui/MainScreen/commands/showModalMessage'),
|
||||
require('./gui/MainScreen/commands/showNoteContentProperties'),
|
||||
require('./gui/MainScreen/commands/showNoteProperties'),
|
||||
require('./gui/MainScreen/commands/showPrompt'),
|
||||
require('./gui/MainScreen/commands/showShareFolderDialog'),
|
||||
require('./gui/MainScreen/commands/showShareNoteDialog'),
|
||||
require('./gui/MainScreen/commands/showSpellCheckerMenu'),
|
||||
require('./gui/MainScreen/commands/toggleEditors'),
|
||||
require('./gui/MainScreen/commands/toggleLayoutMoveMode'),
|
||||
require('./gui/MainScreen/commands/toggleNoteList'),
|
||||
require('./gui/MainScreen/commands/toggleSideBar'),
|
||||
require('./gui/MainScreen/commands/toggleVisiblePanes'),
|
||||
require('./gui/NoteEditor/commands/focusElementNoteBody'),
|
||||
require('./gui/NoteEditor/commands/focusElementNoteTitle'),
|
||||
require('./gui/NoteEditor/commands/showLocalSearch'),
|
||||
require('./gui/NoteEditor/commands/showRevisions'),
|
||||
require('./gui/NoteList/commands/focusElementNoteList'),
|
||||
require('./gui/NoteListControls/commands/focusSearch'),
|
||||
require('./gui/Sidebar/commands/focusElementSideBar'),
|
||||
];
|
||||
const commands = mainScreenCommands
|
||||
.concat(noteEditorCommands)
|
||||
.concat(noteListCommands)
|
||||
.concat(noteListControlsCommands)
|
||||
.concat(sidebarCommands);
|
||||
|
||||
// Commands that are not tied to any particular component.
|
||||
// The runtime for these commands can be loaded when the app starts.
|
||||
const globalCommands = [
|
||||
require('./commands/copyDevCommand'),
|
||||
require('./commands/exportFolders'),
|
||||
require('./commands/exportNotes'),
|
||||
require('./commands/focusElement'),
|
||||
require('./commands/openProfileDirectory'),
|
||||
require('./commands/replaceMisspelling'),
|
||||
require('./commands/startExternalEditing'),
|
||||
require('./commands/stopExternalEditing'),
|
||||
require('./commands/toggleExternalEditing'),
|
||||
require('./commands/toggleSafeMode'),
|
||||
require('./commands/restoreNoteRevision'),
|
||||
require('@joplin/lib/commands/historyBackward'),
|
||||
require('@joplin/lib/commands/historyForward'),
|
||||
require('@joplin/lib/commands/synchronize'),
|
||||
];
|
||||
const globalCommands = appCommands.concat(libCommands);
|
||||
|
||||
import editorCommandDeclarations from './gui/NoteEditor/commands/editorCommandDeclarations';
|
||||
import editorCommandDeclarations from './gui/NoteEditor/editorCommandDeclarations';
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
import checkForUpdates from './checkForUpdates';
|
||||
import { AppState } from './app.reducer';
|
||||
|
||||
const pluginClasses = [
|
||||
require('./plugins/GotoAnything').default,
|
||||
];
|
||||
|
||||
interface AppStateRoute {
|
||||
type: string;
|
||||
routeName: string;
|
||||
props: any;
|
||||
}
|
||||
|
||||
export interface AppStateDialog {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AppState extends State {
|
||||
route: AppStateRoute;
|
||||
navHistory: any[];
|
||||
noteVisiblePanes: string[];
|
||||
windowContentSize: any;
|
||||
watchedNoteFiles: string[];
|
||||
lastEditorScrollPercents: any;
|
||||
devToolsVisible: boolean;
|
||||
visibleDialogs: any; // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: string;
|
||||
layoutMoveMode: boolean;
|
||||
startupPluginsLoaded: boolean;
|
||||
|
||||
// Extra reducer keys go here
|
||||
watchedResources: any;
|
||||
mainLayout: LayoutItem;
|
||||
dialogs: AppStateDialog[];
|
||||
}
|
||||
|
||||
const appDefaultState: AppState = {
|
||||
...defaultState,
|
||||
route: {
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Main',
|
||||
props: {},
|
||||
},
|
||||
navHistory: [],
|
||||
noteVisiblePanes: ['editor', 'viewer'],
|
||||
windowContentSize: bridge().windowContentSize(),
|
||||
watchedNoteFiles: [],
|
||||
lastEditorScrollPercents: {},
|
||||
devToolsVisible: false,
|
||||
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: null,
|
||||
layoutMoveMode: false,
|
||||
mainLayout: null,
|
||||
startupPluginsLoaded: false,
|
||||
dialogs: [],
|
||||
...resourceEditWatcherDefaultState,
|
||||
};
|
||||
const appDefaultState = createAppDefaultState(
|
||||
bridge().windowContentSize(),
|
||||
resourceEditWatcherDefaultState
|
||||
);
|
||||
|
||||
class Application extends BaseApplication {
|
||||
|
||||
@@ -175,249 +84,9 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
reducer(state: AppState = appDefaultState, action: any) {
|
||||
let newState = state;
|
||||
|
||||
try {
|
||||
switch (action.type) {
|
||||
|
||||
case 'NAV_BACK':
|
||||
case 'NAV_GO':
|
||||
|
||||
{
|
||||
const goingBack = action.type === 'NAV_BACK';
|
||||
|
||||
if (goingBack && !state.navHistory.length) break;
|
||||
|
||||
const currentRoute = state.route;
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
const newNavHistory = state.navHistory.slice();
|
||||
|
||||
if (goingBack) {
|
||||
let newAction = null;
|
||||
while (newNavHistory.length) {
|
||||
newAction = newNavHistory.pop();
|
||||
if (newAction.routeName !== state.route.routeName) break;
|
||||
}
|
||||
|
||||
if (!newAction) break;
|
||||
|
||||
action = newAction;
|
||||
}
|
||||
|
||||
if (!goingBack) newNavHistory.push(currentRoute);
|
||||
newState.navHistory = newNavHistory;
|
||||
newState.route = action;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'STARTUP_PLUGINS_LOADED':
|
||||
|
||||
// When all startup plugins have loaded, we also recreate the
|
||||
// main layout to ensure that it is updated in the UI. There's
|
||||
// probably a cleaner way to do this, but for now that will do.
|
||||
if (state.startupPluginsLoaded !== action.value) {
|
||||
newState = {
|
||||
...newState,
|
||||
startupPluginsLoaded: action.value,
|
||||
mainLayout: JSON.parse(JSON.stringify(newState.mainLayout)),
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case 'WINDOW_CONTENT_SIZE_SET':
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.windowContentSize = action.size;
|
||||
break;
|
||||
|
||||
case 'NOTE_VISIBLE_PANES_TOGGLE':
|
||||
|
||||
{
|
||||
const getNextLayout = (currentLayout: any) => {
|
||||
currentLayout = panes.length === 2 ? 'both' : currentLayout[0];
|
||||
|
||||
let paneOptions;
|
||||
if (state.settings.layoutButtonSequence === Setting.LAYOUT_EDITOR_VIEWER) {
|
||||
paneOptions = ['editor', 'viewer'];
|
||||
} else if (state.settings.layoutButtonSequence === Setting.LAYOUT_EDITOR_SPLIT) {
|
||||
paneOptions = ['editor', 'both'];
|
||||
} else if (state.settings.layoutButtonSequence === Setting.LAYOUT_VIEWER_SPLIT) {
|
||||
paneOptions = ['viewer', 'both'];
|
||||
} else {
|
||||
paneOptions = ['editor', 'viewer', 'both'];
|
||||
}
|
||||
|
||||
const currentLayoutIndex = paneOptions.indexOf(currentLayout);
|
||||
const nextLayoutIndex = currentLayoutIndex === paneOptions.length - 1 ? 0 : currentLayoutIndex + 1;
|
||||
|
||||
const nextLayout = paneOptions[nextLayoutIndex];
|
||||
return nextLayout === 'both' ? ['editor', 'viewer'] : [nextLayout];
|
||||
};
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
|
||||
const panes = state.noteVisiblePanes.slice();
|
||||
newState.noteVisiblePanes = getNextLayout(panes);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_VISIBLE_PANES_SET':
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.noteVisiblePanes = action.panes;
|
||||
break;
|
||||
|
||||
case 'MAIN_LAYOUT_SET':
|
||||
|
||||
newState = {
|
||||
...state,
|
||||
mainLayout: action.value,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'MAIN_LAYOUT_SET_ITEM_PROP':
|
||||
|
||||
{
|
||||
let newLayout = produce(state.mainLayout, (draftLayout: LayoutItem) => {
|
||||
iterateItems(draftLayout, (_itemIndex: number, item: LayoutItem, _parent: LayoutItem) => {
|
||||
if (item.key === action.itemKey) {
|
||||
(item as any)[action.propName] = action.propValue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
if (newLayout !== state.mainLayout) newLayout = validateLayout(newLayout);
|
||||
|
||||
newState = {
|
||||
...state,
|
||||
mainLayout: newLayout,
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'NOTE_FILE_WATCHER_ADD':
|
||||
|
||||
if (newState.watchedNoteFiles.indexOf(action.id) < 0) {
|
||||
newState = Object.assign({}, state);
|
||||
const watchedNoteFiles = newState.watchedNoteFiles.slice();
|
||||
watchedNoteFiles.push(action.id);
|
||||
newState.watchedNoteFiles = watchedNoteFiles;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_FILE_WATCHER_REMOVE':
|
||||
|
||||
{
|
||||
newState = Object.assign({}, state);
|
||||
const idx = newState.watchedNoteFiles.indexOf(action.id);
|
||||
if (idx >= 0) {
|
||||
const watchedNoteFiles = newState.watchedNoteFiles.slice();
|
||||
watchedNoteFiles.splice(idx, 1);
|
||||
newState.watchedNoteFiles = watchedNoteFiles;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_FILE_WATCHER_CLEAR':
|
||||
|
||||
if (state.watchedNoteFiles.length) {
|
||||
newState = Object.assign({}, state);
|
||||
newState.watchedNoteFiles = [];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'EDITOR_SCROLL_PERCENT_SET':
|
||||
|
||||
{
|
||||
newState = Object.assign({}, state);
|
||||
const newPercents = Object.assign({}, newState.lastEditorScrollPercents);
|
||||
newPercents[action.noteId] = action.percent;
|
||||
newState.lastEditorScrollPercents = newPercents;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_DEVTOOLS_TOGGLE':
|
||||
newState = Object.assign({}, state);
|
||||
newState.devToolsVisible = !newState.devToolsVisible;
|
||||
break;
|
||||
|
||||
case 'NOTE_DEVTOOLS_SET':
|
||||
newState = Object.assign({}, state);
|
||||
newState.devToolsVisible = action.value;
|
||||
break;
|
||||
|
||||
case 'VISIBLE_DIALOGS_ADD':
|
||||
newState = Object.assign({}, state);
|
||||
newState.visibleDialogs = Object.assign({}, newState.visibleDialogs);
|
||||
newState.visibleDialogs[action.name] = true;
|
||||
break;
|
||||
|
||||
case 'VISIBLE_DIALOGS_REMOVE':
|
||||
newState = Object.assign({}, state);
|
||||
newState.visibleDialogs = Object.assign({}, newState.visibleDialogs);
|
||||
delete newState.visibleDialogs[action.name];
|
||||
break;
|
||||
|
||||
case 'FOCUS_SET':
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.focusedField = action.field;
|
||||
break;
|
||||
|
||||
case 'FOCUS_CLEAR':
|
||||
|
||||
// A field can only clear its own state
|
||||
if (action.field === state.focusedField) {
|
||||
newState = Object.assign({}, state);
|
||||
newState.focusedField = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DIALOG_OPEN':
|
||||
|
||||
{
|
||||
newState = Object.assign({}, state);
|
||||
const newDialogs = newState.dialogs.slice();
|
||||
|
||||
if (newDialogs.find(d => d.name === action.name)) throw new Error(`This dialog is already opened: ${action.name}`);
|
||||
|
||||
newDialogs.push({
|
||||
name: action.name,
|
||||
});
|
||||
newState.dialogs = newDialogs;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DIALOG_CLOSE':
|
||||
|
||||
{
|
||||
newState = Object.assign({}, state);
|
||||
const newDialogs = newState.dialogs.slice().filter(d => d.name !== action.name);
|
||||
newState.dialogs = newDialogs;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LAYOUT_MOVE_MODE_SET':
|
||||
|
||||
newState = {
|
||||
...state,
|
||||
layoutMoveMode: action.value,
|
||||
};
|
||||
break;
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||
throw error;
|
||||
}
|
||||
|
||||
let newState = appReducer(state, action);
|
||||
newState = resourceEditWatcherReducer(newState, action);
|
||||
newState = super.reducer(newState, action);
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
@@ -873,6 +542,16 @@ class Application extends BaseApplication {
|
||||
// });
|
||||
// }, 2000);
|
||||
|
||||
// setTimeout(() => {
|
||||
// this.dispatch({
|
||||
// type: 'NAV_GO',
|
||||
// routeName: 'Config',
|
||||
// props: {
|
||||
// defaultSection: 'plugins',
|
||||
// },
|
||||
// });
|
||||
// }, 2000);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
29
packages/app-desktop/commands/index.ts
Normal file
29
packages/app-desktop/commands/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
import * as copyDevCommand from './copyDevCommand';
|
||||
import * as exportFolders from './exportFolders';
|
||||
import * as exportNotes from './exportNotes';
|
||||
import * as focusElement from './focusElement';
|
||||
import * as openProfileDirectory from './openProfileDirectory';
|
||||
import * as replaceMisspelling from './replaceMisspelling';
|
||||
import * as restoreNoteRevision from './restoreNoteRevision';
|
||||
import * as startExternalEditing from './startExternalEditing';
|
||||
import * as stopExternalEditing from './stopExternalEditing';
|
||||
import * as toggleExternalEditing from './toggleExternalEditing';
|
||||
import * as toggleSafeMode from './toggleSafeMode';
|
||||
|
||||
const index:any[] = [
|
||||
copyDevCommand,
|
||||
exportFolders,
|
||||
exportNotes,
|
||||
focusElement,
|
||||
openProfileDirectory,
|
||||
replaceMisspelling,
|
||||
restoreNoteRevision,
|
||||
startExternalEditing,
|
||||
stopExternalEditing,
|
||||
toggleExternalEditing,
|
||||
toggleSafeMode,
|
||||
];
|
||||
|
||||
export default index;
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
@@ -1,5 +1,5 @@
|
||||
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { AppState } from '../app';
|
||||
import { AppState } from '../app.reducer';
|
||||
import bridge from '../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import ClipperServer from '@joplin/lib/ClipperServer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { AppState } from '../app';
|
||||
import { AppState } from '../app.reducer';
|
||||
|
||||
class ClipperConfigScreenComponent extends React.Component {
|
||||
constructor() {
|
||||
|
||||
@@ -133,6 +133,20 @@ const StyledDescription = styled.div`
|
||||
line-height: 1.6em;
|
||||
`;
|
||||
|
||||
const RecommendedBadge = styled.a`
|
||||
font-family: ${props => props.theme.fontFamily};
|
||||
color: ${props => props.theme.colorWarn};
|
||||
font-size: ${props => props.theme.fontSize}px;
|
||||
border: 1px solid ${props => props.theme.colorWarn};
|
||||
padding: 5px;
|
||||
border-radius: 50px;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function(props: Props) {
|
||||
const item = useMemo(() => {
|
||||
return props.item ? props.item : manifestToItem(props.manifest);
|
||||
@@ -144,6 +158,10 @@ export default function(props: Props) {
|
||||
bridge().openExternal(manifest.homepage_url);
|
||||
}, [item]);
|
||||
|
||||
const onRecommendedClick = useCallback(() => {
|
||||
bridge().openExternal('https://github.com/joplin/plugins/blob/master/readme/recommended.md#recommended-plugins');
|
||||
}, []);
|
||||
|
||||
// For plugins in dev mode things like enabling/disabling or
|
||||
// uninstalling them doesn't make sense, as that should be done by
|
||||
// adding/removing them from wherever they were loaded from.
|
||||
@@ -222,11 +240,18 @@ export default function(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderRecommendedBadge() {
|
||||
if (props.onToggle) return null;
|
||||
if (!item.manifest._recommended) return null;
|
||||
return <RecommendedBadge href="#" title={_('The Joplin team has vetted this plugin and it meets our standards for security and performance.')} onClick={onRecommendedClick}><i className="fas fa-crown"></i></RecommendedBadge>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CellRoot isCompatible={props.isCompatible}>
|
||||
<CellTop>
|
||||
<StyledNameAndVersion mb={'5px'}><StyledName onClick={onNameClick} href="#" style={{ marginRight: 5 }}>{item.manifest.name} {item.deleted ? _('(%s)', 'Deleted') : ''}</StyledName><StyledVersion>v{item.manifest.version}</StyledVersion></StyledNameAndVersion>
|
||||
{renderToggleButton()}
|
||||
{renderRecommendedBadge()}
|
||||
</CellTop>
|
||||
<CellContent>
|
||||
<StyledDescription>{item.manifest.description}</StyledDescription>
|
||||
|
||||
@@ -299,7 +299,7 @@ export default function(props: Props) {
|
||||
function renderRepoApiError() {
|
||||
if (!repoApiError) return null;
|
||||
|
||||
return <RepoApiErrorMessage maxWidth={maxWidth} type="error">{_('Could not connect to plugin repository')} - <StyledLink href="#" onClick={() => { setFetchManifestTime(Date.now()); }}>{_('Try again')}</StyledLink></RepoApiErrorMessage>;
|
||||
return <RepoApiErrorMessage maxWidth={maxWidth} type="error">{_('Could not connect to plugin repository.')}<br/><br/>- <StyledLink href="#" onClick={() => { setFetchManifestTime(Date.now()); }}>{_('Try again')}</StyledLink><br/><br/>- <StyledLink href="#" onClick={onBrowsePlugins}>{_('Browse all plugins')}</StyledLink></RepoApiErrorMessage>;
|
||||
}
|
||||
|
||||
function renderBottomArea() {
|
||||
|
||||
@@ -30,6 +30,14 @@ interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
function sortManifestResults(results: PluginManifest[]): PluginManifest[] {
|
||||
return results.sort((m1, m2) => {
|
||||
if (m1._recommended && !m2._recommended) return -1;
|
||||
if (!m1._recommended && m2._recommended) return +1;
|
||||
return m1.name.toLowerCase() < m2.name.toLowerCase() ? -1 : +1;
|
||||
});
|
||||
}
|
||||
|
||||
export default function(props: Props) {
|
||||
const [searchStarted, setSearchStarted] = useState(false);
|
||||
const [manifests, setManifests] = useState<PluginManifest[]>([]);
|
||||
@@ -47,7 +55,7 @@ export default function(props: Props) {
|
||||
setSearchResultCount(null);
|
||||
} else {
|
||||
const r = await props.repoApi().search(props.searchQuery);
|
||||
setManifests(r);
|
||||
setManifests(sortManifestResults(r));
|
||||
setSearchResultCount(r.length);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,8 +12,16 @@ import bridge from '../services/bridge';
|
||||
import shared from '@joplin/lib/components/shared/encryption-config-shared';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import MasterKey from '@joplin/lib/models/MasterKey';
|
||||
import StyledInput from './style/StyledInput';
|
||||
import Button, { ButtonLevel } from './Button/Button';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const MasterPasswordInput = styled(StyledInput)`
|
||||
min-width: 300px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface Props {}
|
||||
|
||||
@@ -45,6 +53,10 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
|
||||
private renderMasterKey(mk: MasterKeyEntity, isDefault: boolean) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const onToggleEnabledClick = () => {
|
||||
return shared.onToggleEnabledClick(this, mk);
|
||||
};
|
||||
|
||||
const passwordStyle = {
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
@@ -60,8 +72,23 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
|
||||
return shared.onPasswordChange(this, mk, event.target.value);
|
||||
};
|
||||
|
||||
const onToggleEnabledClick = () => {
|
||||
return shared.onToggleEnabledClick(this, mk);
|
||||
const renderPasswordInput = (masterKeyId: string) => {
|
||||
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
|
||||
return (
|
||||
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
|
||||
({_('Master password')})
|
||||
</td>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<td style={theme.textStyle}>
|
||||
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
|
||||
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
|
||||
{_('Save')}
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
|
||||
@@ -74,12 +101,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
|
||||
<td style={theme.textStyle}>{activeIcon}</td>
|
||||
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
|
||||
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
|
||||
<td style={theme.textStyle}>
|
||||
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
|
||||
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
|
||||
{_('Save')}
|
||||
</button>
|
||||
</td>
|
||||
{renderPasswordInput(mk.id)}
|
||||
<td style={theme.textStyle}>{passwordOk}</td>
|
||||
<td style={theme.textStyle}>
|
||||
<button disabled={isActive || isDefault} style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
|
||||
@@ -165,7 +187,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => shared.toggleShowDisabledMasterKeys(this) } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
|
||||
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{_('Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.')}</p> : null;
|
||||
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
|
||||
const tableComp = !showTable ? null : (
|
||||
<table>
|
||||
<tbody>
|
||||
@@ -195,6 +217,39 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
|
||||
return null;
|
||||
}
|
||||
|
||||
private renderMasterPassword() {
|
||||
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const onMasterPasswordSave = async () => {
|
||||
shared.onMasterPasswordSave(this);
|
||||
|
||||
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
|
||||
alert('Password is invalid. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
if (this.state.passwordChecks['master']) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<span style={theme.textStyle}>{_('Master password:')}</span>
|
||||
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}>✔ {_('Loaded')}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={theme.textStyle}>❌ {'The master password is not set or is invalid. Please type it below:'}</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
|
||||
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={this.state.masterPasswordInput} onChange={(event: any) => shared.onMasterPasswordChange(this, event.target.value)} />{' '}
|
||||
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
|
||||
@@ -215,7 +270,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
const isEnabled = getEncryptionEnabled();
|
||||
const masterKey = MasterKey.latest();
|
||||
const masterKey = getDefaultMasterKey();
|
||||
|
||||
let answer = null;
|
||||
if (isEnabled) {
|
||||
@@ -304,6 +359,7 @@ class EncryptionConfigScreenComponent extends React.Component<Props> {
|
||||
<p style={theme.textStyle}>
|
||||
{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
|
||||
</p>
|
||||
{this.renderMasterPassword()}
|
||||
{decryptedItemsInfo}
|
||||
{toggleButton}
|
||||
{needUpgradeSection}
|
||||
@@ -329,6 +385,7 @@ const mapStateToProps = (state: State) => {
|
||||
activeMasterKeyId: syncInfo.activeMasterKeyId,
|
||||
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
masterPassword: state.settings['encryption.masterPassword'],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { stateUtils } from '@joplin/lib/reducer';
|
||||
import InteropServiceHelper from '../../InteropServiceHelper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import NoteListWrapper from '../NoteListWrapper/NoteListWrapper';
|
||||
import { AppState } from '../../app';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { saveLayout, loadLayout } from '../ResizableLayout/utils/persist';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import produce from 'immer';
|
||||
@@ -37,6 +37,7 @@ import { reg } from '@joplin/lib/registry';
|
||||
import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems';
|
||||
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';
|
||||
import commands from './commands/index';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const { PromptDialog } = require('../PromptDialog.min.js');
|
||||
@@ -110,39 +111,6 @@ const defaultLayout: LayoutItem = {
|
||||
],
|
||||
};
|
||||
|
||||
const commands = [
|
||||
require('./commands/editAlarm'),
|
||||
require('./commands/exportPdf'),
|
||||
require('./commands/gotoAnything'),
|
||||
require('./commands/commandPalette'),
|
||||
require('./commands/hideModalMessage'),
|
||||
require('./commands/moveToFolder'),
|
||||
require('./commands/newFolder'),
|
||||
require('./commands/newNote'),
|
||||
require('./commands/newSubFolder'),
|
||||
require('./commands/newTodo'),
|
||||
require('./commands/openFolder'),
|
||||
require('./commands/openNote'),
|
||||
require('./commands/openTag'),
|
||||
require('./commands/print'),
|
||||
require('./commands/renameFolder'),
|
||||
require('./commands/renameTag'),
|
||||
require('./commands/search'),
|
||||
require('./commands/setTags'),
|
||||
require('./commands/showModalMessage'),
|
||||
require('./commands/showNoteContentProperties'),
|
||||
require('./commands/showNoteProperties'),
|
||||
require('./commands/showPrompt'),
|
||||
require('./commands/showShareFolderDialog'),
|
||||
require('./commands/showShareNoteDialog'),
|
||||
require('./commands/showSpellCheckerMenu'),
|
||||
require('./commands/toggleEditors'),
|
||||
require('./commands/toggleLayoutMoveMode'),
|
||||
require('./commands/toggleNoteList'),
|
||||
require('./commands/toggleSideBar'),
|
||||
require('./commands/toggleVisiblePanes'),
|
||||
];
|
||||
|
||||
class MainScreenComponent extends React.Component<Props, State> {
|
||||
|
||||
private waitForNotesSavedIID_: any;
|
||||
|
||||
67
packages/app-desktop/gui/MainScreen/commands/index.ts
Normal file
67
packages/app-desktop/gui/MainScreen/commands/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
import * as commandPalette from './commandPalette';
|
||||
import * as editAlarm from './editAlarm';
|
||||
import * as exportPdf from './exportPdf';
|
||||
import * as gotoAnything from './gotoAnything';
|
||||
import * as hideModalMessage from './hideModalMessage';
|
||||
import * as moveToFolder from './moveToFolder';
|
||||
import * as newFolder from './newFolder';
|
||||
import * as newNote from './newNote';
|
||||
import * as newSubFolder from './newSubFolder';
|
||||
import * as newTodo from './newTodo';
|
||||
import * as openFolder from './openFolder';
|
||||
import * as openNote from './openNote';
|
||||
import * as openTag from './openTag';
|
||||
import * as print from './print';
|
||||
import * as renameFolder from './renameFolder';
|
||||
import * as renameTag from './renameTag';
|
||||
import * as search from './search';
|
||||
import * as setTags from './setTags';
|
||||
import * as showModalMessage from './showModalMessage';
|
||||
import * as showNoteContentProperties from './showNoteContentProperties';
|
||||
import * as showNoteProperties from './showNoteProperties';
|
||||
import * as showPrompt from './showPrompt';
|
||||
import * as showShareFolderDialog from './showShareFolderDialog';
|
||||
import * as showShareNoteDialog from './showShareNoteDialog';
|
||||
import * as showSpellCheckerMenu from './showSpellCheckerMenu';
|
||||
import * as toggleEditors from './toggleEditors';
|
||||
import * as toggleLayoutMoveMode from './toggleLayoutMoveMode';
|
||||
import * as toggleNoteList from './toggleNoteList';
|
||||
import * as toggleSideBar from './toggleSideBar';
|
||||
import * as toggleVisiblePanes from './toggleVisiblePanes';
|
||||
|
||||
const index:any[] = [
|
||||
commandPalette,
|
||||
editAlarm,
|
||||
exportPdf,
|
||||
gotoAnything,
|
||||
hideModalMessage,
|
||||
moveToFolder,
|
||||
newFolder,
|
||||
newNote,
|
||||
newSubFolder,
|
||||
newTodo,
|
||||
openFolder,
|
||||
openNote,
|
||||
openTag,
|
||||
print,
|
||||
renameFolder,
|
||||
renameTag,
|
||||
search,
|
||||
setTags,
|
||||
showModalMessage,
|
||||
showNoteContentProperties,
|
||||
showNoteProperties,
|
||||
showPrompt,
|
||||
showShareFolderDialog,
|
||||
showShareNoteDialog,
|
||||
showSpellCheckerMenu,
|
||||
toggleEditors,
|
||||
toggleLayoutMoveMode,
|
||||
toggleNoteList,
|
||||
toggleSideBar,
|
||||
toggleVisiblePanes,
|
||||
];
|
||||
|
||||
export default index;
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
@@ -2,7 +2,7 @@ import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../../../services/bridge';
|
||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||
import { AppState } from '../../../app';
|
||||
import { AppState } from '../../../app.reducer';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { AppState } from '../../../app';
|
||||
import { AppState } from '../../../app.reducer';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'toggleLayoutMoveMode',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import setLayoutItemProps from '../../ResizableLayout/utils/setLayoutItemProps';
|
||||
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
|
||||
import { AppState } from '../../../app';
|
||||
import { AppState } from '../../../app.reducer';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'toggleNoteList',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import setLayoutItemProps from '../../ResizableLayout/utils/setLayoutItemProps';
|
||||
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
|
||||
import { AppState } from '../../../app';
|
||||
import { AppState } from '../../../app.reducer';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'toggleSideBar',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { AppState } from '../app';
|
||||
import { AppState } from '../app.reducer';
|
||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
|
||||
@@ -3,7 +3,7 @@ import CommandService from '@joplin/lib/services/CommandService';
|
||||
import ToolbarBase from '../../../ToolbarBase';
|
||||
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../../../app';
|
||||
import { AppState } from '../../../../app.reducer';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import stateToWhenClauseContext from '../../../../services/commands/stateToWhenClauseContext';
|
||||
const { buildStyle } = require('@joplin/lib/theme');
|
||||
|
||||
@@ -20,7 +20,7 @@ import CommandService from '@joplin/lib/services/CommandService';
|
||||
import ToolbarButton from '../ToolbarButton/ToolbarButton';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import { AppState } from '../../app';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import ToolbarButtonUtils from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import TagList from '../TagList';
|
||||
|
||||
15
packages/app-desktop/gui/NoteEditor/commands/index.ts
Normal file
15
packages/app-desktop/gui/NoteEditor/commands/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
import * as focusElementNoteBody from './focusElementNoteBody';
|
||||
import * as focusElementNoteTitle from './focusElementNoteTitle';
|
||||
import * as showLocalSearch from './showLocalSearch';
|
||||
import * as showRevisions from './showRevisions';
|
||||
|
||||
const index:any[] = [
|
||||
focusElementNoteBody,
|
||||
focusElementNoteTitle,
|
||||
showLocalSearch,
|
||||
showRevisions,
|
||||
];
|
||||
|
||||
export default index;
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { FormNote, ScrollOptionTypes } from './types';
|
||||
import editorCommandDeclarations from '../commands/editorCommandDeclarations';
|
||||
import editorCommandDeclarations from '../editorCommandDeclarations';
|
||||
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import time from '@joplin/lib/time';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppState } from '../../app';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import NoteListUtils from '../utils/NoteListUtils';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
9
packages/app-desktop/gui/NoteList/commands/index.ts
Normal file
9
packages/app-desktop/gui/NoteList/commands/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
import * as focusElementNoteList from './focusElementNoteList';
|
||||
|
||||
const index:any[] = [
|
||||
focusElementNoteList,
|
||||
];
|
||||
|
||||
export default index;
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
import * as focusSearch from './focusSearch';
|
||||
|
||||
const index:any[] = [
|
||||
focusSearch,
|
||||
];
|
||||
|
||||
export default index;
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
@@ -1,4 +1,5 @@
|
||||
import app, { AppState, AppStateDialog } from '../app';
|
||||
import app from '../app';
|
||||
import { AppState, AppStateDialog } from '../app.reducer';
|
||||
import MainScreen from './MainScreen/MainScreen';
|
||||
import ConfigScreen from './ConfigScreen/ConfigScreen';
|
||||
import StatusScreen from './StatusScreen/StatusScreen';
|
||||
@@ -19,6 +20,7 @@ import DialogTitle from './DialogTitle';
|
||||
import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './DialogButtonRow';
|
||||
import Dialog from './Dialog';
|
||||
import SyncWizardDialog from './SyncWizard/Dialog';
|
||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
const { Navigator } = require('./Navigator.min.js');
|
||||
@@ -207,6 +209,7 @@ class RootComponent extends React.Component<Props, any> {
|
||||
return (
|
||||
<StyleSheetManager disableVendorPrefixes>
|
||||
<ThemeProvider theme={theme}>
|
||||
<StyleSheetContainer themeId={this.props.themeId}></StyleSheetContainer>
|
||||
<MenuBar/>
|
||||
<GlobalStyle/>
|
||||
<Navigator style={navigatorStyle} screens={screens} />
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ShareUserStatus, StateShare, StateShareUser } from '@joplin/lib/service
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
|
||||
const logger = Logger.create('ShareFolderDialog');
|
||||
|
||||
@@ -100,20 +101,6 @@ interface RecipientDeleteEvent {
|
||||
shareUserId: string;
|
||||
}
|
||||
|
||||
interface AsyncEffectEvent {
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
function useAsyncEffect(effect: Function, dependencies: any[]) {
|
||||
useEffect(() => {
|
||||
const event = { cancelled: false };
|
||||
effect(event);
|
||||
return () => {
|
||||
event.cancelled = true;
|
||||
};
|
||||
}, dependencies);
|
||||
}
|
||||
|
||||
enum ShareState {
|
||||
Idle = 0,
|
||||
Synchronizing = 1,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { StateShare } from '@joplin/lib/services/share/reducer';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import Button from './Button/Button';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../app';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { getEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
const { clipboard } = require('electron');
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import InteropServiceHelper from '../../InteropServiceHelper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
import { AppState } from '../../app';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
|
||||
import { AppState } from '../../../app';
|
||||
import { AppState } from '../../../app.reducer';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'focusElementSideBar',
|
||||
|
||||
9
packages/app-desktop/gui/Sidebar/commands/index.ts
Normal file
9
packages/app-desktop/gui/Sidebar/commands/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
import * as focusElementSideBar from './focusElementSideBar';
|
||||
|
||||
const index:any[] = [
|
||||
focusElementSideBar,
|
||||
];
|
||||
|
||||
export default index;
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
41
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx
Normal file
41
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
// This component is perhaps a bit of a hack but the approach should be
|
||||
// reliable. It converts the current (JS) theme to CSS, and add it to the HEAD
|
||||
// tag. The component itself doesn't render anything where it's located (just an
|
||||
// empty invisible DIV), so it means it could be put anywhere and would have the
|
||||
// same effect.
|
||||
//
|
||||
// It's still reliable because the lifecyle of adding the CSS and removing on
|
||||
// unmout is handled properly. There should only be one such component on the
|
||||
// page.
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
||||
import { themeById } from '@joplin/lib/theme';
|
||||
|
||||
interface Props {
|
||||
themeId: any;
|
||||
}
|
||||
|
||||
export default function(props: Props): any {
|
||||
const [styleSheetContent, setStyleSheetContent] = useState('');
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const theme = themeById(props.themeId);
|
||||
const themeCss = themeToCss(theme);
|
||||
if (event.cancelled) return;
|
||||
setStyleSheetContent(themeCss);
|
||||
}, [props.themeId]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.createElement('style');
|
||||
element.setAttribute('id', 'main-theme-stylesheet-container');
|
||||
document.head.appendChild(element);
|
||||
element.appendChild(document.createTextNode(styleSheetContent));
|
||||
return () => {
|
||||
document.head.removeChild(element);
|
||||
};
|
||||
}, [styleSheetContent]);
|
||||
|
||||
return <div style={{ display: 'none' }}></div>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { AppState } from '../app';
|
||||
import { AppState } from '../app.reducer';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
|
||||
@@ -19,6 +19,7 @@ const tasks = {
|
||||
},
|
||||
tsc: require('@joplin/tools/gulp/tasks/tsc'),
|
||||
updateIgnoredTypeScriptBuild: require('@joplin/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
buildCommandIndex: require('@joplin/tools/gulp/tasks/buildCommandIndex'),
|
||||
};
|
||||
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
@@ -28,7 +29,8 @@ const buildParallel = [
|
||||
'compilePackageInfo',
|
||||
'copyPluginAssets',
|
||||
'copyTinyMceLangs',
|
||||
// 'updateIgnoredTypeScriptBuild',
|
||||
'updateIgnoredTypeScriptBuild',
|
||||
'buildCommandIndex',
|
||||
];
|
||||
|
||||
gulp.task('build', gulp.parallel(...buildParallel));
|
||||
|
||||
4
packages/app-desktop/package-lock.json
generated
4
packages/app-desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.2",
|
||||
"version": "2.4.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.2",
|
||||
"version": "2.4.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.2",
|
||||
"version": "2.4.4",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { AppState } from '../app';
|
||||
import { AppState } from '../app.reducer';
|
||||
import CommandService, { SearchResult as CommandSearchResult } from '@joplin/lib/services/CommandService';
|
||||
import KeymapService from '@joplin/lib/services/KeymapService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
@@ -337,6 +337,10 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
// @ts-ignore
|
||||
const notesById = notes.reduce((obj, { id, body, markup_language }) => ((obj[[id]] = { id, body, markup_language }), obj), {});
|
||||
|
||||
// Filter out search results that are associated with non-existing notes.
|
||||
// https://github.com/laurent22/joplin/issues/5417
|
||||
results = results.filter(r => !!notesById[r.id]);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const row = results[i];
|
||||
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// general, any desktop component should import this file, and not the lib
|
||||
// one.
|
||||
|
||||
import { AppState } from '../../app';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import libStateToWhenClauseContext, { WhenClauseContextOptions } from '@joplin/lib/services/commands/stateToWhenClauseContext';
|
||||
import layoutItemProp from '../../gui/ResizableLayout/utils/layoutItemProp';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppState } from '../../app';
|
||||
import { AppState } from '../../app.reducer';
|
||||
|
||||
export interface DesktopCommandContext {
|
||||
state: AppState;
|
||||
|
||||
@@ -64,28 +64,6 @@ a {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
.note-list .list-item-container:hover {
|
||||
background-color: rgba(0,160,255,0.1) !important;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
.editor-toolbar .button:not(.disabled):hover,
|
||||
.header .button:not(.disabled):hover {
|
||||
background-color: rgba(0,160,255,0.1);
|
||||
border: 1px solid rgba(0,160,255,0.5);
|
||||
box-sizing: 'border-box';
|
||||
}
|
||||
|
||||
.editor-toolbar .button:not(.disabled):active,
|
||||
.header .button:not(.disabled):active {
|
||||
background-color: rgba(0,160,255,0.2);
|
||||
border: 1px solid rgba(0,160,255,0.7);
|
||||
box-sizing: 'border-box';
|
||||
}
|
||||
*/
|
||||
|
||||
.editor-toolbar .button,
|
||||
.header .button {
|
||||
border: 1px solid rgba(0,160,255,0);
|
||||
@@ -163,11 +141,6 @@ a {
|
||||
to {transform: rotate(360deg);}
|
||||
}
|
||||
|
||||
/* .joplin-tinymce .tox-editor-header {
|
||||
padding-left: 88px;
|
||||
padding-right: 150px;
|
||||
} */
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -141,8 +141,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097648
|
||||
versionName "2.4.0"
|
||||
versionCode 2097649
|
||||
versionName "2.4.1"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -116,15 +116,29 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
inputStyle.borderBottomWidth = 1;
|
||||
inputStyle.borderBottomColor = theme.dividerColor;
|
||||
|
||||
const renderPasswordInput = (masterKeyId: string) => {
|
||||
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
|
||||
return (
|
||||
<Text style={{ ...this.styles().normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
|
||||
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
|
||||
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View key={mk.id}>
|
||||
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
|
||||
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
|
||||
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
|
||||
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
|
||||
{renderPasswordInput(mk.id)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -203,6 +217,43 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
private renderMasterPassword() {
|
||||
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const onMasterPasswordSave = async () => {
|
||||
shared.onMasterPasswordSave(this);
|
||||
|
||||
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
|
||||
alert('Password is invalid. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
|
||||
inputStyle.borderBottomWidth = 1;
|
||||
inputStyle.borderBottomColor = theme.dividerColor;
|
||||
|
||||
if (this.state.passwordChecks['master']) {
|
||||
return (
|
||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ ...this.styles().normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
|
||||
<Text style={{ ...this.styles().normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}>
|
||||
<Text style={this.styles().normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
|
||||
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
|
||||
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={this.state.masterPasswordInput} onChangeText={(text: string) => shared.onMasterPasswordChange(this, text)} style={inputStyle}></TextInput>
|
||||
<Button onPress={onMasterPasswordSave} title={_('Save')} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const masterKeys = this.props.masterKeys;
|
||||
@@ -289,6 +340,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
|
||||
<Text style={this.styles().titleText}>{_('Status')}</Text>
|
||||
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
|
||||
{decryptedItemsInfo}
|
||||
{this.renderMasterPassword()}
|
||||
{toggleButton}
|
||||
{passwordPromptComp}
|
||||
{mkComps}
|
||||
@@ -315,6 +367,7 @@ const EncryptionConfigScreen = connect((state: State) => {
|
||||
encryptionEnabled: syncInfo.e2ee,
|
||||
activeMasterKeyId: syncInfo.activeMasterKeyId,
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
masterPassword: state.settings['encryption.masterPassword'],
|
||||
};
|
||||
})(EncryptionConfigScreenComponent);
|
||||
|
||||
|
||||
@@ -46,6 +46,11 @@
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>api.joplincloud.local</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
|
||||
@@ -14,7 +14,7 @@ import ResourceService from '@joplin/lib/services/ResourceService';
|
||||
import KvStore from '@joplin/lib/services/KvStore';
|
||||
import NoteScreen from './components/screens/Note';
|
||||
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Setting, { Env } from '@joplin/lib/models/Setting';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
||||
import reducer from '@joplin/lib/reducer';
|
||||
@@ -103,7 +103,7 @@ import { clearSharedFilesCache } from './utils/ShareUtils';
|
||||
import setIgnoreTlsErrors from './utils/TlsUtils';
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
import setupNotifications from './utils/setupNotifications';
|
||||
import { loadMasterKeysFromSettings } from '@joplin/lib/services/e2ee/utils';
|
||||
import { loadMasterKeysFromSettings, migrateMasterPassword } from '@joplin/lib/services/e2ee/utils';
|
||||
import SyncTargetNone from '../lib/SyncTargetNone';
|
||||
|
||||
let storeDispatch = function(_action: any) {};
|
||||
@@ -474,15 +474,24 @@ async function initialize(dispatch: Function) {
|
||||
if (Setting.value('env') == 'prod') {
|
||||
await db.open({ name: 'joplin.sqlite' });
|
||||
} else {
|
||||
await db.open({ name: 'joplin-101.sqlite' });
|
||||
await db.open({ name: 'joplin-104.sqlite' });
|
||||
|
||||
// await db.clearForTesting();
|
||||
}
|
||||
|
||||
reg.logger().info('Database is ready.');
|
||||
|
||||
reg.logger().info('Loading settings...');
|
||||
|
||||
await loadKeychainServiceAndSettings(KeychainServiceDriverMobile);
|
||||
await migrateMasterPassword();
|
||||
|
||||
if (Setting.value('env') === Env.Dev) {
|
||||
// Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
|
||||
// Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
|
||||
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
|
||||
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
|
||||
}
|
||||
|
||||
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
|
||||
|
||||
@@ -530,7 +539,6 @@ async function initialize(dispatch: Function) {
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
EncryptionService.fsDriver_ = fsDriver;
|
||||
EncryptionService.instance().setLogger(mainLogger);
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
BaseItem.encryptionService_ = EncryptionService.instance();
|
||||
BaseItem.shareService_ = ShareService.instance();
|
||||
@@ -634,7 +642,7 @@ async function initialize(dispatch: Function) {
|
||||
|
||||
class AppComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
@@ -677,7 +685,7 @@ class AppComponent extends React.Component {
|
||||
// https://github.com/laurent22/joplin/issues/3807
|
||||
// https://discourse.joplinapp.org/t/webdav-config-encryption-config-randomly-lost-on-android/11364
|
||||
// https://discourse.joplinapp.org/t/android-keeps-on-resetting-my-sync-and-theme/11443
|
||||
async componentDidMount() {
|
||||
public async componentDidMount() {
|
||||
if (this.props.appState == 'starting') {
|
||||
this.props.dispatch({
|
||||
type: 'APP_STATE_SET',
|
||||
@@ -725,15 +733,18 @@ class AppComponent extends React.Component {
|
||||
setupQuickActions(this.props.dispatch, this.props.selectedFolderId);
|
||||
|
||||
await setupNotifications(this.props.dispatch);
|
||||
|
||||
// Setting.setValue('encryption.masterPassword', 'WRONG');
|
||||
// setTimeout(() => NavService.go('EncryptionConfig'), 2000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this.onAppStateChange_);
|
||||
Linking.removeEventListener('url', this.handleOpenURL_);
|
||||
if (this.unsubscribeNetInfoHandler_) this.unsubscribeNetInfoHandler_();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: any) {
|
||||
public componentDidUpdate(prevProps: any) {
|
||||
if (this.props.showSideMenu !== prevProps.showSideMenu) {
|
||||
Animated.timing(this.state.sideMenuContentOpacity, {
|
||||
toValue: this.props.showSideMenu ? 0.5 : 0,
|
||||
@@ -742,7 +753,7 @@ class AppComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
async backButtonHandler() {
|
||||
private async backButtonHandler() {
|
||||
if (this.props.noteSelectionEnabled) {
|
||||
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
|
||||
return true;
|
||||
@@ -763,7 +774,7 @@ class AppComponent extends React.Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
async handleShareData() {
|
||||
private async handleShareData() {
|
||||
const sharedData = await ShareExtension.data();
|
||||
if (sharedData) {
|
||||
reg.logger().info('Received shared data');
|
||||
@@ -775,14 +786,14 @@ class AppComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(newProps: any) {
|
||||
public UNSAFE_componentWillReceiveProps(newProps: any) {
|
||||
if (newProps.syncStarted != this.lastSyncStarted_) {
|
||||
if (!newProps.syncStarted) FoldersScreenUtils.refreshFolders();
|
||||
this.lastSyncStarted_ = newProps.syncStarted;
|
||||
}
|
||||
}
|
||||
|
||||
sideMenu_change(isOpen: boolean) {
|
||||
private sideMenu_change(isOpen: boolean) {
|
||||
// Make sure showSideMenu property of state is updated
|
||||
// when the menu is open/closed.
|
||||
this.props.dispatch({
|
||||
@@ -790,7 +801,7 @@ class AppComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
if (this.props.appState != 'ready') return null;
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
|
||||
2
packages/fork-htmlparser2/package-lock.json
generated
2
packages/fork-htmlparser2/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/fork-htmlparser2",
|
||||
"version": "4.1.33",
|
||||
"version": "4.1.34",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@joplin/fork-htmlparser2",
|
||||
"description": "Fast & forgiving HTML/XML/RSS parser",
|
||||
"version": "4.1.33",
|
||||
"version": "4.1.34",
|
||||
"author": "Felix Boehm <me@feedic.com>",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
2
packages/fork-sax/package-lock.json
generated
2
packages/fork-sax/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/fork-sax",
|
||||
"version": "1.2.37",
|
||||
"version": "1.2.38",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@joplin/fork-sax",
|
||||
"description": "An evented streaming XML parser in JavaScript",
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
|
||||
"version": "1.2.37",
|
||||
"version": "1.2.38",
|
||||
"main": "lib/sax.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -43,7 +43,7 @@ import SearchEngine from './services/searchengine/SearchEngine';
|
||||
import RevisionService from './services/RevisionService';
|
||||
import ResourceService from './services/ResourceService';
|
||||
import DecryptionWorker from './services/DecryptionWorker';
|
||||
const { loadKeychainServiceAndSettings } = require('./services/SettingUtils');
|
||||
import { loadKeychainServiceAndSettings } from './services/SettingUtils';
|
||||
import MigrationService from './services/MigrationService';
|
||||
import ShareService from './services/share/ShareService';
|
||||
import handleSyncStartupOperation from './services/synchronizer/utils/handleSyncStartupOperation';
|
||||
@@ -51,7 +51,7 @@ import SyncTargetJoplinCloud from './SyncTargetJoplinCloud';
|
||||
const { toSystemSlashes } = require('./path-utils');
|
||||
const { setAutoFreeze } = require('immer');
|
||||
import { getEncryptionEnabled } from './services/synchronizer/syncInfoUtils';
|
||||
import { loadMasterKeysFromSettings } from './services/e2ee/utils';
|
||||
import { loadMasterKeysFromSettings, migrateMasterPassword } from './services/e2ee/utils';
|
||||
import SyncTargetNone from './SyncTargetNone';
|
||||
|
||||
const appLogger: LoggerWrapper = Logger.create('App');
|
||||
@@ -465,6 +465,7 @@ export default class BaseApplication {
|
||||
sideEffects['timeFormat'] = sideEffects['dateFormat'];
|
||||
sideEffects['locale'] = sideEffects['dateFormat'];
|
||||
sideEffects['encryption.passwordCache'] = sideEffects['syncInfoCache'];
|
||||
sideEffects['encryption.masterPassword'] = sideEffects['syncInfoCache'];
|
||||
|
||||
if (action) {
|
||||
const effect = sideEffects[action.key];
|
||||
@@ -768,6 +769,7 @@ export default class BaseApplication {
|
||||
BaseModel.setDb(this.database_);
|
||||
|
||||
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
|
||||
await migrateMasterPassword();
|
||||
await handleSyncStartupOperation();
|
||||
|
||||
appLogger.info(`Client ID: ${Setting.value('clientId')}`);
|
||||
@@ -825,7 +827,6 @@ export default class BaseApplication {
|
||||
|
||||
KvStore.instance().setDb(reg.db());
|
||||
|
||||
EncryptionService.instance().setLogger(globalLogger);
|
||||
BaseItem.encryptionService_ = EncryptionService.instance();
|
||||
BaseItem.shareService_ = ShareService.instance();
|
||||
DecryptionWorker.instance().setLogger(globalLogger);
|
||||
|
||||
@@ -259,6 +259,14 @@ export default class JoplinDatabase extends Database {
|
||||
folders: {},
|
||||
resources: {},
|
||||
tags: {},
|
||||
item_changes: {
|
||||
type: 'The type of change - either 1 (created), 2 (updated) or 3 (deleted)',
|
||||
created_time: 'When the event was generated',
|
||||
item_type: 'The item type (see table above for the list of item types)',
|
||||
item_id: 'The item ID',
|
||||
before_change_item: 'Unused',
|
||||
source: 'Unused',
|
||||
},
|
||||
};
|
||||
|
||||
const baseItems = ['notes', 'folders', 'tags', 'resources'];
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
const ObjectUtils = {};
|
||||
|
||||
ObjectUtils.sortByValue = function(object) {
|
||||
export function sortByValue(object: any) {
|
||||
const temp = [];
|
||||
for (const k in object) {
|
||||
if (!object.hasOwnProperty(k)) continue;
|
||||
@@ -19,16 +17,16 @@ ObjectUtils.sortByValue = function(object) {
|
||||
return v1 < v2 ? -1 : +1;
|
||||
});
|
||||
|
||||
const output = {};
|
||||
const output: any = {};
|
||||
for (let i = 0; i < temp.length; i++) {
|
||||
const item = temp[i];
|
||||
output[item.key] = item.value;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
}
|
||||
|
||||
ObjectUtils.fieldsEqual = function(o1, o2) {
|
||||
export function fieldsEqual(o1: any, o2: any) {
|
||||
if ((!o1 || !o2) && o1 !== o2) return false;
|
||||
|
||||
for (const k in o1) {
|
||||
@@ -42,10 +40,10 @@ ObjectUtils.fieldsEqual = function(o1, o2) {
|
||||
if (c1.length !== c2.length) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
ObjectUtils.convertValuesToFunctions = function(o) {
|
||||
const output = {};
|
||||
export function convertValuesToFunctions(o: any) {
|
||||
const output: any = {};
|
||||
for (const n in o) {
|
||||
if (!o.hasOwnProperty(n)) continue;
|
||||
output[n] = () => {
|
||||
@@ -53,11 +51,26 @@ ObjectUtils.convertValuesToFunctions = function(o) {
|
||||
};
|
||||
}
|
||||
return output;
|
||||
};
|
||||
}
|
||||
|
||||
ObjectUtils.isEmpty = function(o) {
|
||||
export function isEmpty(o: any) {
|
||||
if (!o) return true;
|
||||
return Object.keys(o).length === 0 && o.constructor === Object;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = ObjectUtils;
|
||||
// export function isStringifiable(o:any):boolean {
|
||||
// if (o === null || o === undefined) return true;
|
||||
|
||||
// if (Array.isArray(o)) {
|
||||
// for (const e of o) {
|
||||
// if (!isStringifiable(e)) return false;
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// if (typeof o === 'object') {
|
||||
|
||||
// }
|
||||
|
||||
// return true;
|
||||
// }
|
||||
13
packages/lib/commands/index.ts
Normal file
13
packages/lib/commands/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
import * as historyBackward from './historyBackward';
|
||||
import * as historyForward from './historyForward';
|
||||
import * as synchronize from './synchronize';
|
||||
|
||||
const index:any[] = [
|
||||
historyBackward,
|
||||
historyForward,
|
||||
synchronize,
|
||||
];
|
||||
|
||||
export default index;
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
@@ -8,6 +8,7 @@ import shim from '../../shim';
|
||||
import { MasterKeyEntity } from '../../services/e2ee/types';
|
||||
import time from '../../time';
|
||||
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
|
||||
import { findMasterKeyPassword } from '../../services/e2ee/utils';
|
||||
|
||||
class Shared {
|
||||
|
||||
@@ -16,12 +17,16 @@ class Shared {
|
||||
public initialize(comp: any, props: any) {
|
||||
comp.state = {
|
||||
passwordChecks: {},
|
||||
// Master keys that can be decrypted with the master password
|
||||
// (normally all of them, but for legacy support we need this).
|
||||
masterPasswordKeys: {},
|
||||
stats: {
|
||||
encrypted: null,
|
||||
total: null,
|
||||
},
|
||||
passwords: Object.assign({}, props.passwords),
|
||||
showDisabledMasterKeys: false,
|
||||
masterPasswordInput: '',
|
||||
};
|
||||
comp.isMounted_ = false;
|
||||
|
||||
@@ -108,15 +113,37 @@ class Shared {
|
||||
}
|
||||
}
|
||||
|
||||
public async masterPasswordIsValid(comp: any, masterPassword: string = null) {
|
||||
const activeMasterKey = comp.props.masterKeys.find((mk: MasterKeyEntity) => mk.id === comp.props.activeMasterKeyId);
|
||||
masterPassword = masterPassword === null ? comp.props.masterPassword : masterPassword;
|
||||
if (activeMasterKey && masterPassword) {
|
||||
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async checkPasswords(comp: any) {
|
||||
const passwordChecks = Object.assign({}, comp.state.passwordChecks);
|
||||
const masterPasswordKeys = Object.assign({}, comp.state.masterPasswordKeys);
|
||||
for (let i = 0; i < comp.props.masterKeys.length; i++) {
|
||||
const mk = comp.props.masterKeys[i];
|
||||
const password = comp.state.passwords[mk.id];
|
||||
const password = await findMasterKeyPassword(EncryptionService.instance(), mk);
|
||||
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
|
||||
passwordChecks[mk.id] = ok;
|
||||
masterPasswordKeys[mk.id] = password === comp.props.masterPassword;
|
||||
}
|
||||
comp.setState({ passwordChecks: passwordChecks });
|
||||
|
||||
passwordChecks['master'] = await this.masterPasswordIsValid(comp);
|
||||
|
||||
comp.setState({ passwordChecks, masterPasswordKeys });
|
||||
}
|
||||
|
||||
public masterPasswordStatus(comp: any) {
|
||||
// Don't translate for now because that's temporary - later it should
|
||||
// always be set and the label should be replaced by a "Change master
|
||||
// password" button.
|
||||
return comp.props.masterPassword ? 'Master password is set' : 'Master password is not set';
|
||||
}
|
||||
|
||||
public decryptedStatText(comp: any) {
|
||||
@@ -138,6 +165,14 @@ class Shared {
|
||||
comp.checkPasswords();
|
||||
}
|
||||
|
||||
public onMasterPasswordChange(comp: any, value: string) {
|
||||
comp.setState({ masterPasswordInput: value });
|
||||
}
|
||||
|
||||
public onMasterPasswordSave(comp: any) {
|
||||
Setting.setValue('encryption.masterPassword', comp.state.masterPasswordInput);
|
||||
}
|
||||
|
||||
public onPasswordChange(comp: any, mk: MasterKeyEntity, password: string) {
|
||||
const passwords = Object.assign({}, comp.state.passwords);
|
||||
passwords[mk.id] = password;
|
||||
|
||||
@@ -77,7 +77,7 @@ export default class Database {
|
||||
throw new Error(`Invalid field format: ${field}`);
|
||||
}
|
||||
|
||||
escapeFields(fields: string[] | string): string[] | string {
|
||||
public escapeFields(fields: string[] | string): string[] | string {
|
||||
if (fields == '*') return '*';
|
||||
|
||||
const output = [];
|
||||
@@ -87,6 +87,16 @@ export default class Database {
|
||||
return output;
|
||||
}
|
||||
|
||||
public escapeFieldsToString(fields: string[] | string): string {
|
||||
if (fields === '*') return '*';
|
||||
|
||||
const output = [];
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
output.push(this.escapeField(fields[i]));
|
||||
}
|
||||
return output.join(',');
|
||||
}
|
||||
|
||||
async tryCall(callName: string, inputSql: StringOrSqlQuery, inputParams: SqlParams) {
|
||||
let sql: string = null;
|
||||
let params: SqlParams = null;
|
||||
|
||||
@@ -21,6 +21,38 @@ export default class FsDriverBase {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public async readFile(_path: string, _encoding: string = 'utf8'): Promise<any> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public async copy(_source: string, _dest: string) {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public async mkdir(_path: string) {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public async unlink(_path: string) {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public async move(_source: string, _dest: string) {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public async readFileChunk(_handle: any, _length: number, _encoding: string = 'base64'): Promise<string> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public async open(_path: string, _mode: any): Promise<any> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public async close(_handle: any): Promise<any> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public async readDirStats(_path: string, _options: ReadDirStatsOptions = null): Promise<Stat[]> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
18
packages/lib/hooks/useAsyncEffect.ts
Normal file
18
packages/lib/hooks/useAsyncEffect.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import shim from '../shim';
|
||||
const { useEffect } = shim.react();
|
||||
|
||||
export interface AsyncEffectEvent {
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
export type EffectFunction = (event: AsyncEffectEvent)=> Promise<void>;
|
||||
|
||||
export default function(effect: EffectFunction, dependencies: any[]) {
|
||||
useEffect(() => {
|
||||
const event: AsyncEffectEvent = { cancelled: false };
|
||||
void effect(event);
|
||||
return () => {
|
||||
event.cancelled = true;
|
||||
};
|
||||
}, dependencies);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -48,7 +48,7 @@ stats['bg_BG'] = {"percentDone":56};
|
||||
stats['ca'] = {"percentDone":95};
|
||||
stats['hr_HR'] = {"percentDone":96};
|
||||
stats['cs_CZ'] = {"percentDone":95};
|
||||
stats['da_DK'] = {"percentDone":96};
|
||||
stats['da_DK'] = {"percentDone":99};
|
||||
stats['de_DE'] = {"percentDone":95};
|
||||
stats['et_EE'] = {"percentDone":54};
|
||||
stats['en_GB'] = {"percentDone":100};
|
||||
@@ -64,7 +64,7 @@ stats['hu_HU'] = {"percentDone":84};
|
||||
stats['nl_BE'] = {"percentDone":87};
|
||||
stats['nl_NL'] = {"percentDone":90};
|
||||
stats['nb_NO'] = {"percentDone":96};
|
||||
stats['fa'] = {"percentDone":68};
|
||||
stats['fa'] = {"percentDone":67};
|
||||
stats['pl_PL'] = {"percentDone":90};
|
||||
stats['pt_BR'] = {"percentDone":96};
|
||||
stats['pt_PT'] = {"percentDone":90};
|
||||
@@ -73,13 +73,13 @@ stats['sl_SI'] = {"percentDone":91};
|
||||
stats['sv'] = {"percentDone":96};
|
||||
stats['th_TH'] = {"percentDone":42};
|
||||
stats['vi'] = {"percentDone":96};
|
||||
stats['tr_TR'] = {"percentDone":95};
|
||||
stats['tr_TR'] = {"percentDone":99};
|
||||
stats['uk_UA'] = {"percentDone":89};
|
||||
stats['el_GR'] = {"percentDone":92};
|
||||
stats['ru_RU'] = {"percentDone":90};
|
||||
stats['ru_RU'] = {"percentDone":89};
|
||||
stats['sr_RS'] = {"percentDone":81};
|
||||
stats['zh_CN'] = {"percentDone":96};
|
||||
stats['zh_TW'] = {"percentDone":95};
|
||||
stats['zh_CN'] = {"percentDone":99};
|
||||
stats['zh_TW'] = {"percentDone":94};
|
||||
stats['ja_JP'] = {"percentDone":95};
|
||||
stats['ko'] = {"percentDone":95};
|
||||
module.exports = { locales: locales, stats: stats };
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,13 +1,13 @@
|
||||
const { revisionService, setupDatabaseAndSynchronizer, db, switchClient } = require('../testing/test-utils.js');
|
||||
const SearchEngine = require('../services/searchengine/SearchEngine').default;
|
||||
const ResourceService = require('../services/ResourceService').default;
|
||||
const ItemChangeUtils = require('../services/ItemChangeUtils').default;
|
||||
const Note = require('../models/Note').default;
|
||||
const ItemChange = require('../models/ItemChange').default;
|
||||
import { revisionService, setupDatabaseAndSynchronizer, db, switchClient, msleep } from '../testing/test-utils';
|
||||
import SearchEngine from '../services/searchengine/SearchEngine';
|
||||
import ResourceService from '../services/ResourceService';
|
||||
import ItemChangeUtils from '../services/ItemChangeUtils';
|
||||
import Note from '../models/Note';
|
||||
import ItemChange from '../models/ItemChange';
|
||||
|
||||
let searchEngine = null;
|
||||
let searchEngine: SearchEngine = null;
|
||||
|
||||
describe('models_ItemChange', function() {
|
||||
describe('models/ItemChange', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
@@ -27,17 +27,28 @@ describe('models_ItemChange', function() {
|
||||
const resourceService = new ResourceService();
|
||||
|
||||
await searchEngine.syncTables();
|
||||
|
||||
// If we run this now, it should not delete any change because
|
||||
// the resource service has not yet processed the change
|
||||
await ItemChangeUtils.deleteProcessedChanges();
|
||||
await ItemChangeUtils.deleteProcessedChanges(0);
|
||||
expect(await ItemChange.lastChangeId()).toBe(1);
|
||||
|
||||
await resourceService.indexNoteResources();
|
||||
await ItemChangeUtils.deleteProcessedChanges();
|
||||
await ItemChangeUtils.deleteProcessedChanges(0);
|
||||
expect(await ItemChange.lastChangeId()).toBe(1);
|
||||
|
||||
await revisionService().collectRevisions();
|
||||
|
||||
// If we don't set a TTL it will default to 90 days so it won't delete
|
||||
// either.
|
||||
await ItemChangeUtils.deleteProcessedChanges();
|
||||
expect(await ItemChange.lastChangeId()).toBe(1);
|
||||
|
||||
// All changes should be at least 4 ms old now
|
||||
await msleep(4);
|
||||
|
||||
// Now it should delete all changes older than 3 ms
|
||||
await ItemChangeUtils.deleteProcessedChanges(3);
|
||||
expect(await ItemChange.lastChangeId()).toBe(0);
|
||||
}));
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import BaseModel, { ModelType } from '../BaseModel';
|
||||
import shim from '../shim';
|
||||
import eventManager from '../eventManager';
|
||||
import { ItemChangeEntity } from '../services/database/types';
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
export interface ChangeSinceIdOptions {
|
||||
limit?: number;
|
||||
fields?: string[];
|
||||
}
|
||||
|
||||
export default class ItemChange extends BaseModel {
|
||||
|
||||
private static addChangeMutex_: any = new Mutex();
|
||||
@@ -24,7 +30,7 @@ export default class ItemChange extends BaseModel {
|
||||
return BaseModel.TYPE_ITEM_CHANGE;
|
||||
}
|
||||
|
||||
static async add(itemType: ModelType, itemId: string, type: number, changeSource: any = null, beforeChangeItemJson: string = null) {
|
||||
public static async add(itemType: ModelType, itemId: string, type: number, changeSource: any = null, beforeChangeItemJson: string = null) {
|
||||
if (changeSource === null) changeSource = ItemChange.SOURCE_UNSPECIFIED;
|
||||
if (!beforeChangeItemJson) beforeChangeItemJson = '';
|
||||
|
||||
@@ -57,14 +63,14 @@ export default class ItemChange extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
static async lastChangeId() {
|
||||
public static async lastChangeId() {
|
||||
const row = await this.db().selectOne('SELECT max(id) as max_id FROM item_changes');
|
||||
return row && row.max_id ? row.max_id : 0;
|
||||
}
|
||||
|
||||
// Because item changes are recorded in the background, this function
|
||||
// can be used for synchronous code, in particular when unit testing.
|
||||
static async waitForAllSaved() {
|
||||
public static async waitForAllSaved() {
|
||||
return new Promise((resolve) => {
|
||||
const iid = shim.setInterval(() => {
|
||||
if (!ItemChange.saveCalls_.length) {
|
||||
@@ -75,8 +81,32 @@ export default class ItemChange extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
static async deleteOldChanges(lowestChangeId: number) {
|
||||
public static async deleteOldChanges(lowestChangeId: number, itemMinTtl: number) {
|
||||
if (!lowestChangeId) return;
|
||||
return this.db().exec('DELETE FROM item_changes WHERE id <= ?', [lowestChangeId]);
|
||||
|
||||
const cutOffDate = Date.now() - itemMinTtl;
|
||||
|
||||
return this.db().exec(`
|
||||
DELETE FROM item_changes
|
||||
WHERE id <= ?
|
||||
AND created_time <= ?
|
||||
`, [lowestChangeId, cutOffDate]);
|
||||
}
|
||||
|
||||
public static async changesSinceId(changeId: number, options: ChangeSinceIdOptions = null): Promise<ItemChangeEntity[]> {
|
||||
options = {
|
||||
limit: 100,
|
||||
fields: ['id', 'item_type', 'item_id', 'type', 'created_time'],
|
||||
...options,
|
||||
};
|
||||
|
||||
return this.db().selectAll(`
|
||||
SELECT ${this.db().escapeFieldsToString(options.fields)}
|
||||
FROM item_changes
|
||||
WHERE id > ?
|
||||
ORDER BY id
|
||||
LIMIT ?
|
||||
`, [changeId, options.limit]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ export default class MasterKey extends BaseItem {
|
||||
}
|
||||
}
|
||||
return output;
|
||||
// return this.modelSelectOne('SELECT * FROM master_keys WHERE created_time >= (SELECT max(created_time) FROM master_keys)');
|
||||
}
|
||||
|
||||
static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], method: number) {
|
||||
|
||||
@@ -919,6 +919,7 @@ class Setting extends BaseModel {
|
||||
'encryption.enabled': { value: false, type: SettingItemType.Bool, public: false },
|
||||
'encryption.activeMasterKeyId': { value: '', type: SettingItemType.String, public: false },
|
||||
'encryption.passwordCache': { value: {}, type: SettingItemType.Object, public: false, secure: true },
|
||||
'encryption.masterPassword': { value: '', type: SettingItemType.String, public: false, secure: true },
|
||||
'encryption.shouldReencrypt': {
|
||||
value: -1, // will be set on app startup
|
||||
type: SettingItemType.Int,
|
||||
|
||||
17
packages/lib/package-lock.json
generated
17
packages/lib/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@joplin/lib",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.1.3",
|
||||
@@ -64,6 +64,7 @@
|
||||
"xml2js": "^0.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/css": "^0.0.33",
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^14.14.6",
|
||||
@@ -1003,6 +1004,12 @@
|
||||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/css": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/css/-/css-0.0.33.tgz",
|
||||
"integrity": "sha512-qjeDgh86R0LIeEM588q65yatc8Yyo/VvSIYFqq8JOIHDolhGNX0rz7k/OuxqDpnpqlefoHj8X4Ai/6hT9IWtKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/fs-extra": {
|
||||
"version": "9.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.11.tgz",
|
||||
@@ -9688,6 +9695,12 @@
|
||||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"@types/css": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/css/-/css-0.0.33.tgz",
|
||||
"integrity": "sha512-qjeDgh86R0LIeEM588q65yatc8Yyo/VvSIYFqq8JOIHDolhGNX0rz7k/OuxqDpnpqlefoHj8X4Ai/6hT9IWtKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/fs-extra": {
|
||||
"version": "9.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.11.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"description": "Joplin Core library",
|
||||
"author": "Laurent Cozic",
|
||||
"homepage": "",
|
||||
@@ -16,6 +16,7 @@
|
||||
"test-ci": "npm run test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/css": "^0.0.33",
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^14.14.6",
|
||||
@@ -25,11 +26,11 @@
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/fork-htmlparser2": "^4.1.33",
|
||||
"@joplin/fork-sax": "^1.2.37",
|
||||
"@joplin/renderer": "~2.4",
|
||||
"@joplin/turndown": "^4.0.55",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.37",
|
||||
"@joplin/fork-htmlparser2": "^4.1.34",
|
||||
"@joplin/fork-sax": "^1.2.38",
|
||||
"@joplin/renderer": "^2.4.1",
|
||||
"@joplin/turndown": "^4.0.56",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.38",
|
||||
"async-mutex": "^0.1.3",
|
||||
"aws-sdk": "^2.588.0",
|
||||
"base-64": "^0.1.0",
|
||||
|
||||
@@ -6,6 +6,7 @@ import ResourceService from './ResourceService';
|
||||
import Logger from '../Logger';
|
||||
import shim from '../shim';
|
||||
import KvStore from './KvStore';
|
||||
import EncryptionService from './e2ee/EncryptionService';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
|
||||
@@ -28,7 +29,7 @@ export default class DecryptionWorker {
|
||||
private kvStore_: KvStore = null;
|
||||
private maxDecryptionAttempts_ = 2;
|
||||
private startCalls_: boolean[] = [];
|
||||
private encryptionService_: any = null;
|
||||
private encryptionService_: EncryptionService = null;
|
||||
|
||||
constructor() {
|
||||
this.state_ = 'idle';
|
||||
@@ -134,6 +135,11 @@ export default class DecryptionWorker {
|
||||
this.logger().info(msg);
|
||||
const ids = await MasterKey.allIds();
|
||||
|
||||
// Note that the current implementation means that a warning will be
|
||||
// displayed even if the user has no encrypted note. Just having
|
||||
// encrypted master key is sufficient. Not great but good enough for
|
||||
// now.
|
||||
|
||||
if (ids.length) {
|
||||
if (options.masterKeyNotLoadedHandler === 'throw') {
|
||||
// By trying to load the master key here, we throw the "masterKeyNotLoaded" error
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import Setting from '../models/Setting';
|
||||
import ItemChange from '../models/ItemChange';
|
||||
|
||||
const dayMs = 86400000;
|
||||
|
||||
export default class ItemChangeUtils {
|
||||
static async deleteProcessedChanges() {
|
||||
static async deleteProcessedChanges(itemMinTtl: number = dayMs * 90) {
|
||||
const lastProcessedChangeIds = [
|
||||
Setting.value('resourceService.lastProcessedChangeId'),
|
||||
Setting.value('searchEngine.lastProcessedChangeId'),
|
||||
@@ -10,6 +12,6 @@ export default class ItemChangeUtils {
|
||||
];
|
||||
|
||||
const lowestChangeId = Math.min(...lastProcessedChangeIds);
|
||||
await ItemChange.deleteOldChanges(lowestChangeId);
|
||||
await ItemChange.deleteOldChanges(lowestChangeId, itemMinTtl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import JoplinError from '../../JoplinError';
|
||||
import { getActiveMasterKeyId, setActiveMasterKeyId } from '../synchronizer/syncInfoUtils';
|
||||
const { padLeft } = require('../../string-utils.js');
|
||||
|
||||
const logger = Logger.create('EncryptionService');
|
||||
|
||||
function hexPad(s: string, length: number) {
|
||||
return padLeft(s, length, '0');
|
||||
}
|
||||
@@ -52,7 +54,6 @@ export default class EncryptionService {
|
||||
private decryptedMasterKeys_: Record<string, DecryptedMasterKey> = {};
|
||||
public defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A; // public because used in tests
|
||||
private defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
|
||||
private logger_ = new Logger();
|
||||
|
||||
private headerTemplates_ = {
|
||||
// Template version 1
|
||||
@@ -80,7 +81,6 @@ export default class EncryptionService {
|
||||
this.decryptedMasterKeys_ = {};
|
||||
this.defaultEncryptionMethod_ = EncryptionService.METHOD_SJCL_1A;
|
||||
this.defaultMasterKeyEncryptionMethod_ = EncryptionService.METHOD_SJCL_4;
|
||||
this.logger_ = new Logger();
|
||||
|
||||
this.headerTemplates_ = {
|
||||
// Template version 1
|
||||
@@ -97,14 +97,6 @@ export default class EncryptionService {
|
||||
return this.instance_;
|
||||
}
|
||||
|
||||
setLogger(l: Logger) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
loadedMasterKeysCount() {
|
||||
return Object.keys(this.decryptedMasterKeys_).length;
|
||||
}
|
||||
@@ -139,10 +131,14 @@ export default class EncryptionService {
|
||||
|
||||
public async loadMasterKey(model: MasterKeyEntity, password: string, makeActive = false) {
|
||||
if (!model.id) throw new Error('Master key does not have an ID - save it first');
|
||||
|
||||
logger.info(`Loading master key: ${model.id}. Make active:`, makeActive);
|
||||
|
||||
this.decryptedMasterKeys_[model.id] = {
|
||||
plainText: await this.decryptMasterKey_(model, password),
|
||||
updatedTime: model.updated_time,
|
||||
};
|
||||
|
||||
if (makeActive) this.setActiveMasterKeyId(model.id);
|
||||
}
|
||||
|
||||
@@ -245,7 +241,7 @@ export default class EncryptionService {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
async checkMasterKeyPassword(model: MasterKeyEntity, password: string) {
|
||||
public async checkMasterKeyPassword(model: MasterKeyEntity, password: string) {
|
||||
try {
|
||||
await this.decryptMasterKey_(model, password);
|
||||
} catch (error) {
|
||||
@@ -255,7 +251,7 @@ export default class EncryptionService {
|
||||
return true;
|
||||
}
|
||||
|
||||
wrapSjclError(sjclError: any) {
|
||||
private wrapSjclError(sjclError: any) {
|
||||
const error = new Error(sjclError.message);
|
||||
error.stack = sjclError.stack;
|
||||
return error;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService } from '../../testing/test-utils';
|
||||
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow } from '../../testing/test-utils';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
import { showMissingMasterKeyMessage } from './utils';
|
||||
import { localSyncInfo, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
|
||||
import { migrateMasterPassword, showMissingMasterKeyMessage } from './utils';
|
||||
import { localSyncInfo, setActiveMasterKeyId, setMasterKeyEnabled } from '../synchronizer/syncInfoUtils';
|
||||
import Setting from '../../models/Setting';
|
||||
|
||||
describe('e2ee/utils', function() {
|
||||
|
||||
@@ -34,9 +35,40 @@ describe('e2ee/utils', function() {
|
||||
setMasterKeyEnabled(mk2.id, true);
|
||||
expect(showMissingMasterKeyMessage(localSyncInfo(), [mk1.id, mk2.id])).toBe(true);
|
||||
|
||||
await expectNotThrow(async () => showMissingMasterKeyMessage(localSyncInfo(), ['not_downloaded_yet']));
|
||||
|
||||
const syncInfo = localSyncInfo();
|
||||
syncInfo.masterKeys = [];
|
||||
expect(showMissingMasterKeyMessage(syncInfo, [mk1.id, mk2.id])).toBe(false);
|
||||
});
|
||||
|
||||
it('should do the master password migration', async () => {
|
||||
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey('111111'));
|
||||
const mk2 = await MasterKey.save(await encryptionService().generateMasterKey('222222'));
|
||||
|
||||
Setting.setValue('encryption.passwordCache', {
|
||||
[mk1.id]: '111111',
|
||||
[mk2.id]: '222222',
|
||||
});
|
||||
|
||||
await migrateMasterPassword();
|
||||
|
||||
{
|
||||
expect(Setting.value('encryption.masterPassword')).toBe('');
|
||||
const newCache = Setting.value('encryption.passwordCache');
|
||||
expect(newCache[mk1.id]).toBe('111111');
|
||||
expect(newCache[mk2.id]).toBe('222222');
|
||||
}
|
||||
|
||||
setActiveMasterKeyId(mk1.id);
|
||||
await migrateMasterPassword();
|
||||
|
||||
{
|
||||
expect(Setting.value('encryption.masterPassword')).toBe('111111');
|
||||
const newCache = Setting.value('encryption.passwordCache');
|
||||
expect(newCache[mk1.id]).toBe(undefined);
|
||||
expect(newCache[mk2.id]).toBe('222222');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -4,11 +4,11 @@ import MasterKey from '../../models/MasterKey';
|
||||
import Setting from '../../models/Setting';
|
||||
import { MasterKeyEntity } from './types';
|
||||
import EncryptionService from './EncryptionService';
|
||||
import { getActiveMasterKeyId, masterKeyEnabled, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
|
||||
import { getActiveMasterKey, getActiveMasterKeyId, masterKeyEnabled, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
|
||||
|
||||
const logger = Logger.create('e2ee/utils');
|
||||
|
||||
export async function setupAndEnableEncryption(service: EncryptionService, masterKey: MasterKeyEntity = null, password: string = null) {
|
||||
export async function setupAndEnableEncryption(service: EncryptionService, masterKey: MasterKeyEntity = null, masterPassword: string = null) {
|
||||
if (!masterKey) {
|
||||
// May happen for example if there are master keys in info.json but none
|
||||
// of them is set as active. But in fact, unless there is a bug in the
|
||||
@@ -18,10 +18,8 @@ export async function setupAndEnableEncryption(service: EncryptionService, maste
|
||||
|
||||
setEncryptionEnabled(true, masterKey ? masterKey.id : null);
|
||||
|
||||
if (masterKey && password) {
|
||||
const passwordCache = Setting.value('encryption.passwordCache');
|
||||
passwordCache[masterKey.id] = password;
|
||||
Setting.setValue('encryption.passwordCache', passwordCache);
|
||||
if (masterPassword) {
|
||||
Setting.setValue('encryption.masterPassword', masterPassword);
|
||||
}
|
||||
|
||||
// Mark only the non-encrypted ones for sync since, if there are encrypted ones,
|
||||
@@ -47,6 +45,8 @@ export async function setupAndDisableEncryption(service: EncryptionService) {
|
||||
}
|
||||
|
||||
export async function toggleAndSetupEncryption(service: EncryptionService, enabled: boolean, masterKey: MasterKeyEntity, password: string) {
|
||||
logger.info('toggleAndSetupEncryption: enabled:', enabled, ' Master key', masterKey);
|
||||
|
||||
if (!enabled) {
|
||||
await setupAndDisableEncryption(service);
|
||||
} else {
|
||||
@@ -68,17 +68,65 @@ export async function generateMasterKeyAndEnableEncryption(service: EncryptionSe
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
// Migration function to initialise the master password. Normally it is set when
|
||||
// enabling E2EE, but previously it wasn't. So here we check if the password is
|
||||
// set. If it is not, we set it from the active master key. It needs to be
|
||||
// called after the settings have been initialized.
|
||||
export async function migrateMasterPassword() {
|
||||
if (Setting.value('encryption.masterPassword')) return; // Already migrated
|
||||
|
||||
logger.info('Master password is not set - trying to get it from the active master key...');
|
||||
|
||||
const mk = getActiveMasterKey();
|
||||
if (!mk) return;
|
||||
|
||||
const masterPassword = Setting.value('encryption.passwordCache')[mk.id];
|
||||
if (masterPassword) {
|
||||
Setting.setValue('encryption.masterPassword', masterPassword);
|
||||
logger.info('Master password is now set.');
|
||||
|
||||
// Also clear the key passwords that match the master password to avoid
|
||||
// any confusion.
|
||||
const cache = Setting.value('encryption.passwordCache');
|
||||
const newCache = { ...cache };
|
||||
for (const [mkId, password] of Object.entries(cache)) {
|
||||
if (password === masterPassword) {
|
||||
delete newCache[mkId];
|
||||
}
|
||||
}
|
||||
Setting.setValue('encryption.passwordCache', newCache);
|
||||
await Setting.saveAll();
|
||||
}
|
||||
}
|
||||
|
||||
// All master keys normally should be decryped with the master password, however
|
||||
// previously any master key could be encrypted with any password, so to support
|
||||
// this legacy case, we first check if the MK decrypts with the master password.
|
||||
// If not, try with the master key specific password, if any is defined.
|
||||
export async function findMasterKeyPassword(service: EncryptionService, masterKey: MasterKeyEntity): Promise<string> {
|
||||
const masterPassword = Setting.value('encryption.masterPassword');
|
||||
if (masterPassword && await service.checkMasterKeyPassword(masterKey, masterPassword)) {
|
||||
logger.info('findMasterKeyPassword: Using master password');
|
||||
return masterPassword;
|
||||
}
|
||||
|
||||
logger.info('findMasterKeyPassword: No master password is defined - trying to get master key specific password');
|
||||
|
||||
const passwords = Setting.value('encryption.passwordCache');
|
||||
return passwords[masterKey.id];
|
||||
}
|
||||
|
||||
export async function loadMasterKeysFromSettings(service: EncryptionService) {
|
||||
const masterKeys = await MasterKey.all();
|
||||
const passwords = Setting.value('encryption.passwordCache');
|
||||
const activeMasterKeyId = getActiveMasterKeyId();
|
||||
|
||||
logger.info(`Trying to load ${masterKeys.length} master keys...`);
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
const password = passwords[mk.id];
|
||||
if (service.isMasterKeyLoaded(mk)) continue;
|
||||
|
||||
const password = await findMasterKeyPassword(service, mk);
|
||||
if (!password) continue;
|
||||
|
||||
try {
|
||||
@@ -98,8 +146,22 @@ export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterK
|
||||
|
||||
for (let i = notLoadedMasterKeys.length - 1; i >= 0; i--) {
|
||||
const mk = syncInfo.masterKeys.find(mk => mk.id === notLoadedMasterKeys[i]);
|
||||
|
||||
// A "notLoadedMasterKey" is a key that either doesn't exist or for
|
||||
// which a password hasn't been set yet. For the purpose of this
|
||||
// function, we only want to notify the user about unset passwords.
|
||||
// Master keys that haven't been downloaded yet should normally be
|
||||
// downloaded at some point.
|
||||
// https://github.com/laurent22/joplin/issues/5391
|
||||
if (!mk) continue;
|
||||
if (!masterKeyEnabled(mk)) notLoadedMasterKeys.pop();
|
||||
}
|
||||
|
||||
return !!notLoadedMasterKeys.length;
|
||||
}
|
||||
|
||||
export function getDefaultMasterKey(): MasterKeyEntity {
|
||||
const mk = getActiveMasterKey();
|
||||
if (mk) return mk;
|
||||
return MasterKey.latest();
|
||||
}
|
||||
|
||||
@@ -5,27 +5,28 @@ import { ImportExportResult } from './types';
|
||||
import Setting from '../../models/Setting';
|
||||
|
||||
export default class InteropService_Importer_Base {
|
||||
|
||||
private metadata_: any = null;
|
||||
protected sourcePath_: string = '';
|
||||
protected options_: any = {};
|
||||
|
||||
setMetadata(md: any) {
|
||||
public setMetadata(md: any) {
|
||||
this.metadata_ = md;
|
||||
}
|
||||
|
||||
metadata() {
|
||||
public metadata() {
|
||||
return this.metadata_;
|
||||
}
|
||||
|
||||
async init(sourcePath: string, options: any) {
|
||||
public async init(sourcePath: string, options: any) {
|
||||
this.sourcePath_ = sourcePath;
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
async exec(result: ImportExportResult): Promise<ImportExportResult> {}
|
||||
public async exec(result: ImportExportResult): Promise<ImportExportResult> {}
|
||||
|
||||
async temporaryDirectory_(createIt: boolean) {
|
||||
protected async temporaryDirectory_(createIt: boolean) {
|
||||
const md5 = require('md5');
|
||||
const tempDir = `${Setting.value('tempDir')}/${md5(Math.random() + Date.now())}`;
|
||||
if (createIt) await require('fs-extra').mkdirp(tempDir);
|
||||
|
||||
@@ -5,15 +5,27 @@ export default class InteropService_Importer_Custom extends InteropService_Impor
|
||||
|
||||
private module_: Module = null;
|
||||
|
||||
constructor(handler: Module) {
|
||||
public constructor(handler: Module) {
|
||||
super();
|
||||
this.module_ = handler;
|
||||
}
|
||||
|
||||
async exec(result: ImportExportResult): Promise<ImportExportResult> {
|
||||
public async exec(result: ImportExportResult): Promise<ImportExportResult> {
|
||||
// When passing the options to the plugin, we strip off any function
|
||||
// because they won't serialized over ipc.
|
||||
|
||||
const processedOptions: any = {};
|
||||
|
||||
if (this.options_) {
|
||||
for (const [k, v] of Object.entries(this.options_)) {
|
||||
if (typeof v === 'function') continue;
|
||||
processedOptions[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
return this.module_.onExec({
|
||||
sourcePath: this.sourcePath_,
|
||||
options: this.options_,
|
||||
options: processedOptions,
|
||||
warnings: result.warnings,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,13 @@ export default function manifestFromObject(o: any): PluginManifest {
|
||||
return o[name];
|
||||
};
|
||||
|
||||
const getBoolean = (name: string, required: boolean = true, defaultValue: boolean = false): boolean => {
|
||||
if (required && !o[name]) throw new Error(`Missing required field: ${name}`);
|
||||
if (!o[name]) return defaultValue;
|
||||
if (typeof o[name] !== 'boolean') throw new Error(`Field must be a boolean: ${name}`);
|
||||
return o[name];
|
||||
};
|
||||
|
||||
const permissions: PluginPermission[] = [];
|
||||
|
||||
const manifest: PluginManifest = {
|
||||
@@ -39,6 +46,8 @@ export default function manifestFromObject(o: any): PluginManifest {
|
||||
repository_url: getString('repository_url', false),
|
||||
keywords: getStrings('keywords', false),
|
||||
permissions: permissions,
|
||||
|
||||
_recommended: getBoolean('_recommended', false, false),
|
||||
};
|
||||
|
||||
validatePluginId(manifest.id);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user