1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-01-05 00:12:33 +02:00

Compare commits

...

62 Commits

Author SHA1 Message Date
Laurent Cozic
99b5847244 update 2021-09-06 16:50:37 +01:00
Laurent Cozic
4ad7e1b245 movile 2021-09-06 14:37:26 +01:00
Laurent Cozic
b6f889baca update 2021-09-06 14:31:40 +01:00
Laurent Cozic
54fb4b0946 theme to css 2021-09-05 19:17:47 +01:00
Laurent Cozic
80762572cf Chore: Moved app state to separate file 2021-09-04 18:11:29 +01:00
Marph
8e5d209d3c All: Translation: Update de_DE.po (#5419) 2021-09-04 13:07:09 -04:00
reportxx
f71dad6d09 All: Translation: Update sv.po (#5418) 2021-09-04 12:05:59 -04:00
Laurent Cozic
f5891dfae8 Tools: Fixed build 2021-09-04 15:15:25 +01:00
Laurent Cozic
736bbbd8ed Plugins: Fixed import API
Ref: https://discourse.joplinapp.org/t/prompt-when-the-plugin-is-running-typeerror-this-module-oninit-is-not-a-function/20009/14
2021-09-04 15:07:38 +01:00
Laurent Cozic
973121addd Fixed missing command 2021-09-04 14:36:52 +01:00
Laurent Cozic
95ad4c3177 Tools: Ignore commands/index files for linting 2021-09-04 14:29:43 +01:00
Laurent Cozic
71c470f59d Desktop: Fixes #5417: Handle invalid search index in Goto Anything 2021-09-04 14:26:29 +01:00
Laurent Cozic
c529b972e3 Chore: Automatically create command index for desktop app 2021-09-04 13:43:25 +01:00
Laurent Cozic
e6bff3f2e0 Chore: Moved desktop app reducer to separate file so that it can be unit tested 2021-09-04 12:37:22 +01:00
Laurent Cozic
1bc674a1f9 Tools: Update ignored files when running desktop app in dev mode 2021-09-03 16:43:06 +01:00
Laurent Cozic
f371bb8e59 Revert "Merge branch 'desktop-protocol' of https://github.com/roman-r-m/joplin into dev"
This reverts commit 7c85889c1f, reversing
changes made to ab134807ea.

Reason: Messed up and accidentally merged a pull request.
2021-09-03 15:03:17 +01:00
Laurent Cozic
7c85889c1f Merge branch 'desktop-protocol' of https://github.com/roman-r-m/joplin into dev 2021-09-03 15:00:31 +01:00
Piotr Banasik
ab134807ea Server: Enable multi platform builds (amd64, armv7 and arm64) (#5338) 2021-09-03 14:54:10 +01:00
Laurent Cozic
a5b3bb6058 Doc: Add Joplin Cloud links 2021-09-03 14:50:21 +01:00
Laurent Cozic
8ab1cd984c Server v2.4.3 2021-09-02 18:49:29 +01:00
Laurent Cozic
e387d9a91b Merge branch 'dev' into release-2.4 2021-09-02 18:48:43 +01:00
Laurent Cozic
0793b1be59 Releasing sub-packages 2021-09-02 18:40:37 +01:00
Laurent Cozic
19225abbcf Android 2.4.1 2021-09-02 18:40:37 +01:00
Laurent Cozic
85fa3288ab Desktop release v2.4.4 2021-09-02 18:40:36 +01:00
Laurent Cozic
17f82c426a Chore: Added server test 2021-09-02 11:44:13 +01:00
Laurent Cozic
82331c9b93 Server: Display note title as page title when sharing note 2021-09-02 11:37:53 +01:00
Roman
886b6d1126 Add stuff to maybe get it to work on MacOS 2021-09-01 22:46:44 +01:00
Roman
1a703c4ecd Rename ProtocolUtils -> callbackUrlUtils 2021-09-01 22:28:33 +01:00
Roman
1126899769 Code review changes 2021-09-01 22:27:24 +01:00
James Wu
a571d38862 installer script regex match bracket fix in Debian version check (#5405) 2021-09-01 13:27:16 -06:00
Laurent Cozic
4ab2fb73b7 Doc: Added social links in footer 2021-09-01 15:21:56 +01:00
Laurent Cozic
78c7b79299 Fixed margin 2021-09-01 12:47:35 +01:00
Laurent Cozic
d97ba57dda Desktop: Sort plugin results according to recommended property, and display Recommended tag 2021-09-01 12:17:20 +01:00
Laurent Cozic
9c44133bd0 Plugin Repo: Allow overridding manifest properties and added recommended plugins 2021-08-31 22:37:56 +01:00
Laurent Cozic
dc008ecf64 Doc: typo 2021-08-31 17:24:40 +01:00
Laurent Cozic
93a4ad09bb Server: Fixed calculation of max sizes for Postgres 2021-08-31 16:15:20 +01:00
Laurent Cozic
9c1dc7898a Doc: Add more links to FAQ 2021-08-31 15:39:02 +01:00
Laurent Cozic
6520a481ca Server: Added Help page for Joplin Cloud 2021-08-31 13:46:46 +01:00
Laurent Cozic
5805a41249 Server: Added icon next to profile button 2021-08-31 12:16:57 +01:00
Laurent Cozic
b88b747ba6 Api: Resolves #5199: Add support for "events" end point to retrieve info about latest note changes 2021-08-30 18:53:24 +01:00
Roman
b269c2fdb9 Simplify protocol setup 2021-08-28 14:26:54 +01:00
Roman
f42fd0ecce Fix enum usage 2021-08-20 21:43:37 +01:00
Roman
62c5f433d7 Rename enum values 2021-08-20 21:24:16 +01:00
Roman Musin
e73a4b7286 Merge branch 'dev' into desktop-protocol 2021-08-20 12:02:11 +01:00
Roman
6c18c6ddc7 Use url-parse 2021-08-15 13:48:32 +01:00
Roman
20d1f74ee4 Use enum 2021-08-14 23:30:19 +01:00
Roman
2386abea3e Rename parseUrl -> parseCallbackUrl 2021-08-14 23:18:58 +01:00
Roman
90621a8417 Fix linter errors 2021-08-14 23:07:22 +01:00
Roman
b02baa6891 Update ignore files 2021-08-14 22:34:47 +01:00
Roman
ee46978389 Review comments - throw an error if callback url is not valid 2021-08-14 21:59:54 +01:00
Roman
2b6b4dd916 Rename initialUrl -> initialCallbackUrl 2021-08-14 21:55:47 +01:00
Roman
f0361bf80d Review comments - escape vars in url 2021-08-14 21:53:52 +01:00
Roman
ecf718005d Code review comments 2021-08-14 21:01:03 +01:00
Roman
305d0ffc49 Rename copy folder URL -> copy notebook URL 2021-08-14 20:57:01 +01:00
Roman
f454c4e33b Add a function to check for valid callback url 2021-08-14 20:20:16 +01:00
Roman
047883bd27 Read initial url from a field 2021-08-14 20:12:14 +01:00
Roman
f118f5250f Handle openFolder and openTag too; change the URL format; extract ULR functions to a separate file 2021-08-14 13:33:45 +01:00
Roman
61161039c8 Open the note from URL even if Joplin isn't running 2021-08-13 23:21:14 +01:00
Roman
00504898f2 Cleanup 2021-08-13 22:50:49 +01:00
Roman
0a6390ed96 Actually open the note 2021-08-13 22:45:13 +01:00
Roman
f909fe6670 Trying to get it to work on Linux 2021-08-13 22:06:01 +01:00
Roman
b17d8bc533 Register to handle joplin:// links 2021-08-12 21:51:03 +01:00
125 changed files with 2396 additions and 880 deletions

View File

@@ -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,6 +1807,9 @@ 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

View File

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

82
.gitignore vendored
View File

@@ -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,6 +1792,9 @@ 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

View File

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

View File

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

View File

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

View 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 &copy; 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -237,7 +237,7 @@ export default class ElectronAppWrapper {
const iid = setInterval(() => {
if (this.electronApp().isReady()) {
clearInterval(iid);
resolve();
resolve(null);
}
}, 10);
});

View 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' }]);
});
});

View 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;
}

View File

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

View 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`

View File

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

View File

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

View File

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

View File

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

View File

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

View 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`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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`

View File

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

View File

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

View 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`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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`

View 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>;
}

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.3",
"version": "2.4.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/app-desktop",
"version": "2.4.3",
"version": "2.4.4",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.3",
"version": "2.4.4",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { AppState } from '../../app';
import { AppState } from '../../app.reducer';
export interface DesktopCommandContext {
state: AppState;

View File

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

View File

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

View File

@@ -488,7 +488,7 @@ SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
FBReactNativeSpec: d2f54de51f69366bd1f5c1fb9270698dce678f8d
FBReactNativeSpec: 6da2c8ff1ebe6b6cf4510fcca58c24c4d02b16fc
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
JoplinCommonShareExtension: 270b4f8eb4e22828eeda433a04ed689fc1fd09b5
JoplinRNShareExtension: 7137e9787374e1b0797ecbef9103d1588d90e403

View File

@@ -480,6 +480,7 @@ async function initialize(dispatch: Function) {
}
reg.logger().info('Database is ready.');
reg.logger().info('Loading settings...');
await loadKeychainServiceAndSettings(KeychainServiceDriverMobile);
@@ -641,7 +642,7 @@ async function initialize(dispatch: Function) {
class AppComponent extends React.Component {
constructor() {
public constructor() {
super();
this.state = {
@@ -684,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',
@@ -737,13 +738,13 @@ class AppComponent extends React.Component {
// 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,
@@ -752,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;
@@ -773,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');
@@ -785,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({
@@ -800,7 +801,7 @@ class AppComponent extends React.Component {
});
}
render() {
public render() {
if (this.props.appState != 'ready') return null;
const theme = themeStyle(this.props.themeId);

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/fork-htmlparser2",
"version": "4.1.33",
"version": "4.1.34",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/fork-sax",
"version": "1.2.37",
"version": "1.2.38",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

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

View File

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

View File

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

View 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`

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,4 +20,6 @@ export interface PluginManifest {
_publish_hash?: string;
_publish_commit?: string;
_npm_package_name?: string;
_obsolete?: boolean;
_recommended?: boolean;
}

View File

@@ -9,6 +9,7 @@ import route_master_keys from './routes/master_keys';
import route_search from './routes/search';
import route_ping from './routes/ping';
import route_auth from './routes/auth';
import route_events from './routes/events';
const { ltrimSlashes } = require('../../path-utils');
const md5 = require('md5');
@@ -43,6 +44,9 @@ interface RequestQuery {
// Auth token
auth_token?: string;
// Event cursor
cursor?: string;
}
export interface Request {
@@ -104,6 +108,7 @@ export default class Api {
search: route_search,
services: this.action_services.bind(this),
auth: route_auth,
events: route_events,
};
this.dispatch = this.dispatch.bind(this);

View File

@@ -0,0 +1,94 @@
import { ModelType } from '../../../BaseModel';
import ItemChange from '../../../models/ItemChange';
import Note from '../../../models/Note';
import { expectThrow, setupDatabaseAndSynchronizer, switchClient } from '../../../testing/test-utils';
import { ItemChangeEntity } from '../../database/types';
import Api, { RequestMethod } from '../Api';
let api: Api = null;
describe('routes/events', function() {
beforeEach(async (done) => {
api = new Api();
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should retrieve the latest events', async () => {
let cursor = '0';
{
const response = await api.route(RequestMethod.GET, 'events', { cursor });
expect(response.cursor).toBe('0');
}
const note1 = await Note.save({ title: 'toto' });
await Note.save({ id: note1.id, title: 'tutu' });
const note2 = await Note.save({ title: 'tata' });
await ItemChange.waitForAllSaved();
{
const response = await api.route(RequestMethod.GET, 'events', { cursor });
expect(response.cursor).toBe('3');
expect(response.items.length).toBe(2);
expect(response.has_more).toBe(false);
expect(response.items.map((it: ItemChangeEntity) => it.item_id).sort()).toEqual([note1.id, note2.id].sort());
cursor = response.cursor;
}
{
const response = await api.route(RequestMethod.GET, 'events', { cursor });
expect(response.cursor).toBe(cursor);
expect(response.items.length).toBe(0);
expect(response.has_more).toBe(false);
}
await Note.save({ id: note2.id, title: 'titi' });
await ItemChange.waitForAllSaved();
{
const response = await api.route(RequestMethod.GET, 'events', { cursor });
expect(response.cursor).toBe('4');
expect(response.items.length).toBe(1);
expect(response.items[0].item_id).toBe(note2.id);
}
});
it('should limit the number of response items', async () => {
const promises = [];
for (let i = 0; i < 101; i++) {
promises.push(Note.save({ title: 'toto' }));
}
await Promise.all(promises);
await ItemChange.waitForAllSaved();
const response1 = await api.route(RequestMethod.GET, 'events', { cursor: '0' });
expect(response1.items.length).toBe(100);
expect(response1.has_more).toBe(true);
const response2 = await api.route(RequestMethod.GET, 'events', { cursor: response1.cursor });
expect(response2.items.length).toBe(1);
expect(response2.has_more).toBe(false);
});
it('should retrieve a single item', async () => {
const beforeTime = Date.now();
const note = await Note.save({ title: 'toto' });
await ItemChange.waitForAllSaved();
const response = await api.route(RequestMethod.GET, 'events/1');
expect(response.item_type).toBe(ModelType.Note);
expect(response.type).toBe(1);
expect(response.item_id).toBe(note.id);
expect(response.created_time).toBeGreaterThanOrEqual(beforeTime);
await expectThrow(async () => api.route(RequestMethod.GET, 'events/1234'));
});
});

View File

@@ -0,0 +1,39 @@
import { ModelType } from '../../../BaseModel';
import { Request, RequestMethod } from '../Api';
import { ErrorBadRequest, ErrorNotFound } from '../utils/errors';
import ItemChange, { ChangeSinceIdOptions } from '../../../models/ItemChange';
import requestFields from '../utils/requestFields';
export default async function(request: Request, id: string = null, _link: string = null) {
if (request.method === RequestMethod.GET) {
const options: ChangeSinceIdOptions = {
limit: 100,
fields: requestFields(request, ModelType.ItemChange, ['id', 'item_type', 'item_id', 'type', 'created_time']),
};
if (!id) {
if (!('cursor' in request.query)) {
return {
items: [],
has_more: false,
cursor: (await ItemChange.lastChangeId()).toString(),
};
} else {
const cursor = Number(request.query.cursor);
if (isNaN(cursor)) throw new ErrorBadRequest(`Invalid cursor: ${request.query.cursor}`);
const changes = await ItemChange.changesSinceId(cursor, options);
return {
items: changes,
has_more: changes.length >= options.limit,
cursor: (changes.length ? changes[changes.length - 1].id : cursor).toString(),
};
}
} else {
const change = await ItemChange.load(id, { fields: options.fields });
if (!change) throw new ErrorNotFound();
return change;
}
}
}

View File

@@ -11,13 +11,18 @@ function defaultFieldsByModelType(modelType: number): string[] {
return output;
}
export default function(request: Request, modelType: number) {
export default function(request: Request, modelType: number, defaultFields: string[] = null) {
const getDefaults = () => {
if (defaultFields) return defaultFields;
return defaultFieldsByModelType(modelType);
};
const query = request.query;
if (!query || !query.fields) return defaultFieldsByModelType(modelType);
if (!query || !query.fields) return getDefaults();
if (Array.isArray(query.fields)) return query.fields.slice();
const fields = query.fields
.split(',')
.map((f: string) => f.trim())
.filter((f: string) => !!f);
return fields.length ? fields : defaultFieldsByModelType(modelType);
return fields.length ? fields : getDefaults();
}

View File

@@ -0,0 +1,28 @@
import cssToTheme from './cssToTheme';
describe('cssToTheme', function() {
it('should convert a CSS string to a theme', async () => {
const input = `
:root {
--joplin-appearence: light;
--joplin-color: #333333;
--joplin-background-color: #778899;
/* Should skip this comment and empty lines */
--joplin-background-color-transparent: rgba(255,255,255,0.9);
}`;
const expected = {
appearence: 'light',
color: '#333333',
backgroundColor: '#778899',
backgroundColorTransparent: 'rgba(255,255,255,0.9)',
};
const actual = cssToTheme(input, 'test.css');
expect(actual).toEqual(expected);
});
});

View File

@@ -0,0 +1,46 @@
import { Theme } from '../../themes/type';
// Need to include it that way due to a bug in the lib:
// https://github.com/reworkcss/css/pull/146#issuecomment-740412799
const cssParse = require('css/lib/parse');
function formatCssToThemeVariable(cssVariable: string): string {
const elements = cssVariable.substr(2).split('-');
if (elements[0] !== 'joplin') throw new Error(`CSS variable name must start with "--joplin": ${cssVariable}`);
elements.splice(0, 1);
return elements.map((e, i) => {
const c = i === 0 ? e[0] : e[0].toUpperCase();
return c + e.substr(1);
}).join('');
}
// function unquoteValue(v:string):string {
// if (v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"')) return v.substr(1, v.length - 2);
// return v;
// }
export default function cssToTheme(css: string, sourceFilePath: string): Theme {
const o = cssParse(css, {
silent: false,
source: sourceFilePath,
});
if (!o?.stylesheet?.rules?.length) throw new Error(`Invalid CSS color file: ${sourceFilePath}`);
// Need "as any" because outdated TS definition file
const rootRule = o.stylesheet.rules[0];
if (!rootRule.selectors.includes(':root')) throw new Error('`:root` rule not found');
const declarations: any[] = rootRule.declarations;
const output: any = {};
for (const declaration of declarations) {
if (declaration.type !== 'declaration') continue; // Skip comment lines
output[formatCssToThemeVariable(declaration.property)] = declaration.value;
}
return output;
}

View File

@@ -0,0 +1,27 @@
import { Theme } from '../../themes/type';
import { filename } from '../../path-utils';
import shim from '../../shim';
import cssToTheme from './cssToTheme';
export default async function(cssBaseDir: string): Promise<Record<string, Theme>> {
const themeDirs = (await shim.fsDriver().readDirStats(cssBaseDir)).filter((f: any) => f.isDirectory());
const output: Record<string, Theme> = {};
for (const themeDir of themeDirs) {
const themeName = filename(themeDir.path);
const cssFile = `${cssBaseDir}/${themeDir.path}/colors.css`;
const cssContent = await shim.fsDriver().readFile(cssFile, 'utf8');
let themeId = themeName;
const manifestFile = `${cssBaseDir}/${themeDir.path}/manifest.json`;
if (await shim.fsDriver().exists(manifestFile)) {
const manifest = JSON.parse(await shim.fsDriver().readFile(manifestFile, 'utf8'));
if (manifest.id) themeId = manifest.id;
}
output[themeId] = cssToTheme(cssContent, cssFile);
}
return output;
}

View File

@@ -0,0 +1,105 @@
import { Theme, ThemeAppearance } from '../../themes/type';
import themeToCss from './themeToCss';
const input: Theme = {
appearance: ThemeAppearance.Light,
// Color scheme "1" is the basic one, like used to display the note
// content. It's basically dark gray text on white background
backgroundColor: '#ffffff',
backgroundColorTransparent: 'rgba(255,255,255,0.9)',
oddBackgroundColor: '#eeeeee',
color: '#32373F', // For regular text
colorError: 'red',
colorWarn: 'rgb(228,86,0)',
colorWarnUrl: '#155BDA',
colorFaded: '#7C8B9E', // For less important text
colorBright: '#000000', // For important text
dividerColor: '#dddddd',
selectedColor: '#e5e5e5',
urlColor: '#155BDA',
// Color scheme "2" is used for the sidebar. It's white text over
// dark blue background.
backgroundColor2: '#313640',
color2: '#ffffff',
selectedColor2: '#131313',
colorError2: '#ff6c6c',
colorWarn2: '#ffcb81',
// Color scheme "3" is used for the config screens for example/
// It's dark text over gray background.
backgroundColor3: '#F4F5F6',
backgroundColorHover3: '#CBDAF1',
color3: '#738598',
// Color scheme "4" is used for secondary-style buttons. It makes a white
// button with blue text.
backgroundColor4: '#ffffff',
color4: '#2D6BDC',
raisedBackgroundColor: '#e5e5e5',
raisedColor: '#222222',
searchMarkerBackgroundColor: '#F7D26E',
searchMarkerColor: 'black',
warningBackgroundColor: '#FFD08D',
tableBackgroundColor: 'rgb(247, 247, 247)',
codeBackgroundColor: 'rgb(243, 243, 243)',
codeBorderColor: 'rgb(220, 220, 220)',
codeColor: 'rgb(0,0,0)',
blockQuoteOpacity: 0.7,
codeMirrorTheme: 'default',
codeThemeCss: 'atom-one-light.css',
};
const expected = `
:root {
--joplin-appearance: light;
--joplin-background-color: #ffffff;
--joplin-background-color-transparent: rgba(255,255,255,0.9);
--joplin-odd-background-color: #eeeeee;
--joplin-color: #32373F;
--joplin-color-error: red;
--joplin-color-warn: rgb(228,86,0);
--joplin-color-warn-url: #155BDA;
--joplin-color-faded: #7C8B9E;
--joplin-color-bright: #000000;
--joplin-divider-color: #dddddd;
--joplin-selected-color: #e5e5e5;
--joplin-url-color: #155BDA;
--joplin-background-color2: #313640;
--joplin-color2: #ffffff;
--joplin-selected-color2: #131313;
--joplin-color-error2: #ff6c6c;
--joplin-color-warn2: #ffcb81;
--joplin-background-color3: #F4F5F6;
--joplin-background-color-hover3: #CBDAF1;
--joplin-color3: #738598;
--joplin-background-color4: #ffffff;
--joplin-color4: #2D6BDC;
--joplin-raised-background-color: #e5e5e5;
--joplin-raised-color: #222222;
--joplin-search-marker-background-color: #F7D26E;
--joplin-search-marker-color: black;
--joplin-warning-background-color: #FFD08D;
--joplin-table-background-color: rgb(247, 247, 247);
--joplin-code-background-color: rgb(243, 243, 243);
--joplin-code-border-color: rgb(220, 220, 220);
--joplin-code-color: rgb(0,0,0);
--joplin-block-quote-opacity: 0.7;
--joplin-code-mirror-theme: default;
--joplin-code-theme-css: atom-one-light.css;
}`;
describe('themeToCss', function() {
it('should a theme to a CSS string', async () => {
const actual = themeToCss(input);
expect(actual.trim()).toBe(expected.trim());
});
});

View File

@@ -0,0 +1,24 @@
import { Theme } from '../../themes/type';
const { camelCaseToDash, formatCssSize } = require('../../string-utils');
// function quoteCssValue(name: string, value: string): string {
// const needsQuote = ['appearance', 'codeMirrorTheme', 'codeThemeCss'].includes(name);
// if (needsQuote) return `'${value}'`;
// return value;
// }
export default function(theme: Theme) {
const lines = [];
lines.push(':root {');
for (const name in theme) {
const value = (theme as any)[name];
const newName = `--joplin-${camelCaseToDash(name)}`;
const formattedValue = typeof value === 'number' && newName.indexOf('opacity') < 0 ? formatCssSize(value) : value;
lines.push(`\t${newName}: ${formattedValue};`);
}
lines.push('}');
return lines.join('\n');
}

View File

@@ -23,7 +23,7 @@ const themes: any = {
[Setting.THEME_OLED_DARK]: theme_oledDark,
};
function themeById(themeId: string) {
export function themeById(themeId: string) {
if (!themes[themeId]) throw new Error(`Invalid theme ID: ${themeId}`);
const output = Object.assign({}, themes[themeId]);
@@ -365,7 +365,7 @@ function addExtraStyles(style: any) {
const themeCache_: any = {};
function themeStyle(themeId: number) {
export function themeStyle(themeId: number) {
if (!themeId) throw new Error('Theme must be specified');
const zoomRatio = 1;
@@ -405,7 +405,7 @@ const cachedStyles_: any = {
// cacheKey must be a globally unique key, and must change whenever
// the dependencies of the style change. If the style depends only
// on the theme, a static string can be provided as a cache key.
function buildStyle(cacheKey: any, themeId: number, callback: Function) {
export function buildStyle(cacheKey: any, themeId: number, callback: Function) {
cacheKey = Array.isArray(cacheKey) ? cacheKey.join('_') : cacheKey;
// We clear the cache whenever switching themes
@@ -425,5 +425,3 @@ function buildStyle(cacheKey: any, themeId: number, callback: Function) {
return cachedStyles_.styles[cacheKey].style;
}
export { themeStyle, buildStyle, themeById };

View File

@@ -10,18 +10,8 @@ import updateReadme from './lib/updateReadme';
import { NpmPackage } from './lib/types';
import gitCompareUrl from './lib/gitCompareUrl';
import commandUpdateRelease from './commands/updateRelease';
function stripOffPackageOrg(name: string): string {
const n = name.split('/');
if (n[0][0] === '@') n.splice(0, 1);
return n.join('/');
}
function isJoplinPluginPackage(pack: any): boolean {
if (!pack.keywords || !pack.keywords.includes('joplin-plugin')) return false;
if (stripOffPackageOrg(pack.name).indexOf('joplin-plugin') !== 0) return false;
return true;
}
import { isJoplinPluginPackage, readJsonFile } from './lib/utils';
import { applyManifestOverrides, getObsoleteManifests, readManifestOverrides } from './lib/overrideUtils';
function pluginInfoFromSearchResults(results: any[]): NpmPackage[] {
const output: NpmPackage[] = [];
@@ -44,19 +34,11 @@ async function checkPluginRepository(dirPath: string, dryRun: boolean) {
if (!(await fs.pathExists(`${dirPath}/.git`))) throw new Error(`Directory is not a Git repository: ${dirPath}`);
const previousDir = chdir(dirPath);
await gitRepoCleanTry();
if (!dryRun) await gitPullTry();
chdir(previousDir);
}
async function readJsonFile(manifestPath: string, defaultValue: any = null): Promise<any> {
if (!(await fs.pathExists(manifestPath))) {
if (defaultValue === null) throw new Error(`No such file: ${manifestPath}`);
return defaultValue;
if (!dryRun) {
await gitRepoCleanTry();
await gitPullTry();
}
const content = await fs.readFile(manifestPath, 'utf8');
return JSON.parse(content);
chdir(previousDir);
}
async function extractPluginFilesFromPackage(existingManifests: any, workDir: string, packageName: string, destDir: string): Promise<any> {
@@ -147,14 +129,14 @@ function chdir(path: string): string {
return previous;
}
async function processNpmPackage(npmPackage: NpmPackage, repoDir: string) {
async function processNpmPackage(npmPackage: NpmPackage, repoDir: string, dryRun: boolean) {
const tempDir = `${repoDir}/temp`;
const obsoleteManifestsPath = path.resolve(repoDir, 'obsoletes.json');
await fs.mkdirp(tempDir);
const originalPluginManifests = await readManifests(repoDir);
const obsoleteManifests = await readJsonFile(obsoleteManifestsPath, {});
const manifestOverrides = await readManifestOverrides(repoDir);
const obsoleteManifests = getObsoleteManifests(manifestOverrides);
const existingManifests = {
...originalPluginManifests,
...obsoleteManifests,
@@ -199,6 +181,8 @@ async function processNpmPackage(npmPackage: NpmPackage, repoDir: string) {
...manifests,
};
manifests = applyManifestOverrides(manifests, manifestOverrides);
await writeManifests(repoDir, manifests);
await updateReadme(`${repoDir}/README.md`, manifests);
}
@@ -206,11 +190,13 @@ async function processNpmPackage(npmPackage: NpmPackage, repoDir: string) {
chdir(repoDir);
await fs.remove(tempDir);
if (!(await gitRepoClean())) {
await execCommand2('git add -A', { showOutput: false });
await execCommand2(['git', 'commit', '-m', commitMessage(actionType, manifest, previousManifest, npmPackage, error)], { showOutput: false });
} else {
console.info('Nothing to commit');
if (!dryRun) {
if (!(await gitRepoClean())) {
await execCommand2('git add -A', { showOutput: false });
await execCommand2(['git', 'commit', '-m', commitMessage(actionType, manifest, previousManifest, npmPackage, error)], { showOutput: false });
} else {
console.info('Nothing to commit');
}
}
}
@@ -223,36 +209,40 @@ async function commandBuild(args: CommandBuildArgs) {
await checkPluginRepository(repoDir, dryRun);
// When starting, always update and commit README, in case something has
// been updated via a pull request (for example obsoletes.json being
// modified). We do that separately so that the README update doesn't get
// mixed up with plugin updates, as in this example:
// been updated via a pull request. We do that separately so that the README
// update doesn't get mixed up with plugin updates, as in this example:
// https://github.com/joplin/plugins/commit/8a65bbbf64bf267674f854a172466ffd4f07c672
const manifests = await readManifests(repoDir);
await updateReadme(`${repoDir}/README.md`, manifests);
const previousDir = chdir(repoDir);
if (!(await gitRepoClean())) {
console.info('Updating README...');
await execCommand2('git add -A', { showOutput: true });
await execCommand2('git commit -m "Update README"', { showOutput: true });
if (!dryRun) {
if (!(await gitRepoClean())) {
console.info('Updating README...');
await execCommand2('git add -A', { showOutput: true });
await execCommand2('git commit -m "Update README"', { showOutput: true });
}
}
chdir(previousDir);
const searchResults = (await execCommand2('npm search joplin-plugin --searchlimit 5000 --json', { showOutput: false })).trim();
const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults));
for (const npmPackage of npmPackages) {
await processNpmPackage(npmPackage, repoDir);
await processNpmPackage(npmPackage, repoDir, dryRun);
}
if (!dryRun) {
await commandUpdateRelease(args);
if (!(await gitRepoClean())) {
await execCommand2('git add -A', { showOutput: true });
await execCommand2('git commit -m "Update stats"', { showOutput: true });
}
}
if (!dryRun) await execCommand2('git push');
await execCommand2('git push');
}
}
async function commandVersion() {

View File

@@ -0,0 +1,95 @@
import { applyManifestOverrides, getObsoleteManifests, ManifestOverrides } from './overrideUtils';
describe('overrideUtils', () => {
test('should get the obsolete manifests', () => {
const manifestOverrides: any = {
'ambrt.backlinksToNote': {
'manifest_version': 1,
'id': 'ambrt.backlinksToNote',
'app_min_version': '1.5',
'version': '1.0.2',
'name': 'Backlinks to note',
'description': 'Creates backlinks to opened note',
'author': 'a',
'homepage_url': 'https://discourse.joplinapp.org/t/insert-referencing-notes-backlinks-plugin/13632',
'_publish_hash': 'sha256:5676da6b9ad71fc5a9779d3bde13f17de5352344711e135f0db8c62c6dbb5696',
'_publish_commit': 'master:19e515bd67e51cc37bd270a32d2898ca009a0de2',
'_npm_package_name': 'joplin-plugin-backlinks',
'_obsolete': true,
},
'MyPlugin': {
'manifest_version': 1,
'id': 'MyPlugin',
'app_min_version': '1.6',
'version': '1.0.0',
'name': 'Testing New Plugin',
'description': 'bla',
'author': 'bla',
'homepage_url': 'bla',
'repository_url': 'bla',
'_publish_hash': 'sha256:065285d06ea3c084e7f8f8c23583de8d70c4d586274a242c4c750f6faad8c7cb',
'_publish_commit': '',
'_npm_package_name': 'joplin-plugin-testing-new-plugin',
'_obsolete': true,
},
'io.github.jackgruber.copytags': {
'_recommended': true,
},
};
const obsoletes = getObsoleteManifests(manifestOverrides);
expect(Object.keys(obsoletes).sort()).toEqual(['MyPlugin', 'ambrt.backlinksToNote']);
expect(obsoletes['ambrt.backlinksToNote'].description).toBe('Creates backlinks to opened note');
});
test('should apply the overrides', () => {
const manifests: any = {
'io.github.jackgruber.copytags': {
'manifest_version': 1,
'id': 'io.github.jackgruber.copytags',
'app_min_version': '1.6.2',
'version': '1.0.1',
'name': 'Tagging',
'description': 'Plugin to extend the Joplin tagging menu with a coppy all tags and a tagging dialog with more control. (Formerly Copy Tags).',
'author': 'JackGruber',
'homepage_url': 'https://github.com/JackGruber/joplin-plugin-tagging/blob/master/README.md',
'repository_url': 'https://github.com/JackGruber/joplin-plugin-tagging',
'keywords': [
'duplicate',
'copy',
'tags',
'tagging',
'tag',
],
'_publish_hash': 'sha256:88daaf234a9b47e5644a8de6f830a801d12edbe41ea5364d994773e89eeafeef',
'_publish_commit': 'master:64c0510e3236df7788a8d10ec28dcfbb4c2bdbb7',
'_npm_package_name': 'joplin-plugin-copytags',
},
'joplin.plugin.ambrt.backlinksToNote': {
'manifest_version': 1,
'id': 'joplin.plugin.ambrt.backlinksToNote',
'app_min_version': '1.7',
'version': '2.0.11',
'name': 'Automatic Backlinks to note',
'description': 'Creates backlinks to opened note, also in automatic way',
'author': 'ambrt,cyingfan',
'homepage_url': 'https://discourse.joplinapp.org/t/insert-referencing-notes-backlinks-plugin/13632',
'_publish_hash': 'sha256:df57930d1ab62d4297dad0bb1764888935fcbf6ca8c04e3a843e86a260735c51',
'_publish_commit': 'master:98102718a9c0fa9416d654451b18602798c4d3bb',
'_npm_package_name': 'joplin-plugin-backlinks',
},
};
const overrides: ManifestOverrides = {
'io.github.jackgruber.copytags': {
_recommended: true,
},
};
const updatedManifests = applyManifestOverrides(manifests, overrides);
expect(updatedManifests['io.github.jackgruber.copytags']._recommended).toBe(true);
expect(updatedManifests['joplin.plugin.ambrt.backlinksToNote']._recommended).toBe(undefined);
});
});

View File

@@ -0,0 +1,51 @@
import * as path from 'path';
import { readJsonFile } from './utils';
export interface ManifestOverride {
_obsolete?: boolean;
_recommended?: boolean;
}
export type ManifestOverrides = Record<string, ManifestOverride>;
export function applyManifestOverrides(manifests: any, overrides: ManifestOverrides) {
for (const [pluginId, override] of Object.entries(overrides)) {
const manifest: any = manifests[pluginId];
if (!manifest) {
// If the manifest does not exist in the destination, it means the
// plugin has been taken out, so the obsolete property should be set
// to `true`. If not, it's an error.
if (!override._obsolete) {
console.error(`Could not find manifest for plugin ${pluginId} when applying override`);
}
continue;
}
for (const [propName, propValue] of Object.entries(override)) {
manifest[propName] = propValue;
}
}
return manifests;
}
export function getObsoleteManifests(overrides: ManifestOverrides) {
const output: any = {};
for (const [pluginId, override] of Object.entries(overrides)) {
if (override._obsolete) {
output[pluginId] = override;
}
}
return output;
}
function pluginManifestOverridesPath(repoDir: string): string {
return path.resolve(repoDir, 'manifestOverrides.json');
}
export async function readManifestOverrides(repoDir: string): Promise<ManifestOverrides> {
return readJsonFile(pluginManifestOverridesPath(repoDir), {});
}

View File

@@ -0,0 +1,23 @@
import * as fs from 'fs-extra';
export async function readJsonFile(manifestPath: string, defaultValue: any = null): Promise<any> {
if (!(await fs.pathExists(manifestPath))) {
if (defaultValue === null) throw new Error(`No such file: ${manifestPath}`);
return defaultValue;
}
const content = await fs.readFile(manifestPath, 'utf8');
return JSON.parse(content);
}
function stripOffPackageOrg(name: string): string {
const n = name.split('/');
if (n[0][0] === '@') n.splice(0, 1);
return n.join('/');
}
export function isJoplinPluginPackage(pack: any): boolean {
if (!pack.keywords || !pack.keywords.includes('joplin-plugin')) return false;
if (stripOffPackageOrg(pack.name).indexOf('joplin-plugin') !== 0) return false;
return true;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/plugin-repo-cli",
"version": "2.3.1",
"version": "2.4.1",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/plugin-repo-cli",
"version": "2.4.0",
"version": "2.4.1",
"description": "",
"main": "index.js",
"bin": {
@@ -13,13 +13,14 @@
"tsc": "tsc --project tsconfig.json",
"watch": "tsc --watch --project tsconfig.json",
"test": "jest",
"test-ci": "npm run test"
"test-ci": "npm run test",
"start": "node index.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"@joplin/lib": "~2.4",
"@joplin/tools": "~2.4",
"@joplin/lib": "^2.4.1",
"@joplin/tools": "^2.4.1",
"fs-extra": "^9.0.1",
"gh-release-assets": "^2.0.0",
"node-fetch": "^2.6.1",

View File

@@ -0,0 +1,92 @@
export default function(markdownIt: any) {
markdownIt.core.ruler.push('anchorHeader', (state: any): boolean => {
const tokens = state.tokens;
const Token = state.Token;
const doneNames = [];
const headingTextToAnchorName = (text: string, doneNames: string[]) => {
const allowed = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let lastWasDash = true;
let output = '';
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (allowed.indexOf(c) < 0) {
if (lastWasDash) continue;
lastWasDash = true;
output += '-';
} else {
lastWasDash = false;
output += c;
}
}
output = output.toLowerCase();
while (output.length && output[output.length - 1] === '-') {
output = output.substr(0, output.length - 1);
}
let temp = output;
let index = 1;
while (doneNames.indexOf(temp) >= 0) {
temp = `${output}-${index}`;
index++;
}
output = temp;
return output;
};
const createAnchorTokens = (anchorName: string) => {
const output = [];
{
const token = new Token('heading_anchor_open', 'a', 1);
token.attrs = [
['name', anchorName],
['href', `#${anchorName}`],
['class', 'heading-anchor'],
];
output.push(token);
}
{
const token = new Token('text', '', 0);
token.content = '🔗';
output.push(token);
}
{
const token = new Token('heading_anchor_close', 'a', -1);
output.push(token);
}
return output;
};
let insideHeading = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'heading_open') {
insideHeading = true;
continue;
}
if (token.type === 'heading_close') {
insideHeading = false;
continue;
}
if (insideHeading && token.type === 'inline') {
const anchorName = headingTextToAnchorName(token.content, doneNames);
doneNames.push(anchorName);
const anchorTokens = createAnchorTokens(anchorName);
// token.children = anchorTokens.concat(token.children);
token.children = token.children.concat(anchorTokens);
}
}
return true;
});
}

View File

@@ -4,6 +4,7 @@ import HtmlToHtml from './HtmlToHtml';
import utils from './utils';
import setupLinkify from './MdToHtml/setupLinkify';
import validateLinks from './MdToHtml/validateLinks';
import headerAnchor from './headerAnchor';
const assetsToHeaders = require('./assetsToHeaders');
export {
@@ -13,6 +14,7 @@ export {
HtmlToHtml,
setupLinkify,
validateLinks,
headerAnchor,
assetsToHeaders,
utils,
};

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.4.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.4.1",
"license": "MIT",
"dependencies": {
"font-awesome-filetypes": "^2.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.4.1",
"description": "The Joplin note renderer, used the mobile and desktop application",
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
"main": "index.js",
@@ -24,7 +24,7 @@
"typescript": "^4.0.5"
},
"dependencies": {
"@joplin/fork-htmlparser2": "^4.1.33",
"@joplin/fork-htmlparser2": "^4.1.34",
"font-awesome-filetypes": "^2.1.0",
"fs-extra": "^8.1.0",
"highlight.js": "^11.2.0",

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/server",
"version": "2.4.2",
"version": "2.4.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/server",
"version": "2.4.2",
"version": "2.4.3",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@koa/cors": "^3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.4.2",
"version": "2.4.3",
"private": true,
"scripts": {
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",

View File

@@ -63,4 +63,24 @@ ul.pagination-list li {
.readable-block {
max-width: 740px;
}
a.heading-anchor {
display: inline-block;
opacity: 0;
width: 1.3em;
font-size: 0.7em;
margin-left: 0.4em;
line-height: 1em;
text-decoration: none;
transition: opacity 0.3s;
}
a.heading-anchor:hover,
h1:hover a.heading-anchor,
h2:hover a.heading-anchor,
h3:hover a.heading-anchor,
h4:hover a.heading-anchor,
h5:hover a.heading-anchor,
h6:hover a.heading-anchor {
opacity: 1;
}

View File

@@ -149,7 +149,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
config_ = {
appVersion: packageJson.version,
appName,
isJoplinCloud: apiBaseUrl.includes('.joplincloud.com'),
isJoplinCloud: apiBaseUrl.includes('.joplincloud.com') || apiBaseUrl.includes('.joplincloud.local'),
env: envType,
rootDir: rootDir,
viewDir: viewDir,

View File

@@ -210,13 +210,13 @@ describe('UserModel', function() {
await models().user().save({
id: user1.id,
account_type: AccountType.Basic,
total_item_size: accountByType(AccountType.Basic).max_total_item_size * 0.85,
total_item_size: Math.round(accountByType(AccountType.Basic).max_total_item_size * 0.85),
});
await models().user().save({
id: user2.id,
account_type: AccountType.Pro,
total_item_size: accountByType(AccountType.Pro).max_total_item_size * 0.2,
total_item_size: Math.round(accountByType(AccountType.Pro).max_total_item_size * 0.2),
});
const emailBeforeCount = (await models().email().all()).length;
@@ -242,7 +242,7 @@ describe('UserModel', function() {
await models().user().save({
id: user2.id,
total_item_size: accountByType(AccountType.Pro).max_total_item_size * 1.1,
total_item_size: Math.round(accountByType(AccountType.Pro).max_total_item_size * 1.1),
});
// User upload should be enabled at this point

View File

@@ -383,8 +383,8 @@ export default class UserModel extends BaseModel<User> {
.db(this.tableName)
.select(['id', 'total_item_size', 'max_total_item_size', 'account_type', 'email', 'full_name'])
.where(function() {
void this.whereRaw('total_item_size > ? AND account_type = ?', [alertLimit1 * basicAccount.max_total_item_size, AccountType.Basic])
.orWhereRaw('total_item_size > ? AND account_type = ?', [alertLimit1 * proAccount.max_total_item_size, AccountType.Pro]);
void this.whereRaw('total_item_size > ? AND account_type = ?', [Math.round(alertLimit1 * basicAccount.max_total_item_size), AccountType.Basic])
.orWhereRaw('total_item_size > ? AND account_type = ?', [Math.round(alertLimit1 * proAccount.max_total_item_size), AccountType.Pro]);
})
// Users who are disabled or who cannot upload already received the
// notification.

View File

@@ -0,0 +1,21 @@
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { ErrorMethodNotAllowed } from '../../utils/errors';
import defaultView from '../../utils/defaultView';
const router: Router = new Router(RouteType.Web);
router.public = true;
router.get('help', async (_path: SubPath, ctx: AppContext) => {
if (ctx.method === 'GET') {
const view = defaultView('help', 'Help');
return view;
}
throw new ErrorMethodNotAllowed();
});
export default router;

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