You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-08 23:07:32 +02:00
Compare commits
188 Commits
note-list-
...
remove-pdf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dc78cad61 | ||
|
|
f7d14c7afb | ||
|
|
4ff78b9b57 | ||
|
|
c442abd65f | ||
|
|
0e2e867140 | ||
|
|
b83165f9e5 | ||
|
|
7706f9058b | ||
|
|
03222ba1b2 | ||
|
|
d4f49db342 | ||
|
|
40e1b0559e | ||
|
|
738f1decbb | ||
|
|
e5a364d052 | ||
|
|
357a3e2e7b | ||
|
|
af91fd99cc | ||
|
|
3855f60a0d | ||
|
|
079b379e7a | ||
|
|
476906849b | ||
|
|
e23e036677 | ||
|
|
8fd54d41cb | ||
|
|
b824ff5457 | ||
|
|
3669a1b5d6 | ||
|
|
b93f9aaf01 | ||
|
|
8679290206 | ||
|
|
4acec5c6c7 | ||
|
|
f1b03453a4 | ||
|
|
7972dd5556 | ||
|
|
4842500f0a | ||
|
|
84c7f28ec5 | ||
|
|
f3eea43d24 | ||
|
|
8babaddbcb | ||
|
|
13cdaabb17 | ||
|
|
a94aa21088 | ||
|
|
6116bed4e3 | ||
|
|
fabd0b4dda | ||
|
|
6b72f86e7b | ||
|
|
02cf546124 | ||
|
|
eecb012d64 | ||
|
|
04e9b40769 | ||
|
|
efdbaeb397 | ||
|
|
46425b920c | ||
|
|
f5be43c2ac | ||
|
|
080541a2fe | ||
|
|
7dc638edf4 | ||
|
|
3b686194d8 | ||
|
|
5c2640f88f | ||
|
|
eca0f92dff | ||
|
|
260fa6c038 | ||
|
|
8ec6bc9138 | ||
|
|
93fa92369b | ||
|
|
bc6c5ab7a7 | ||
|
|
1826625e4f | ||
|
|
20b8fb2719 | ||
|
|
f813e71b29 | ||
|
|
02422a6e31 | ||
|
|
69a34e87f3 | ||
|
|
cbeaa16b61 | ||
|
|
05917ac142 | ||
|
|
0c8de68b80 | ||
|
|
44d93d52d3 | ||
|
|
073bec9e8c | ||
|
|
e6a8c2bea5 | ||
|
|
81c316cd2c | ||
|
|
659c851960 | ||
|
|
572701d9a0 | ||
|
|
66ef37bd4e | ||
|
|
9ddf75604d | ||
|
|
3ed7e1d7e8 | ||
|
|
b2b412105a | ||
|
|
60a3c4f65e | ||
|
|
9645414c17 | ||
|
|
af0136ef39 | ||
|
|
b76586c4fd | ||
|
|
376e4ebde0 | ||
|
|
1439b8787f | ||
|
|
b8854a99be | ||
|
|
6cf02173dc | ||
|
|
4d8a53d8c9 | ||
|
|
7f43718e1d | ||
|
|
690ce637b1 | ||
|
|
4d023e679e | ||
|
|
6e220a978f | ||
|
|
39757cd90e | ||
|
|
5ccbbea757 | ||
|
|
309222c082 | ||
|
|
50f5fe2c91 | ||
|
|
eacae83182 | ||
|
|
403d770b1d | ||
|
|
a481bf1b53 | ||
|
|
0d32570c9e | ||
|
|
f017e99b02 | ||
|
|
a89d64d435 | ||
|
|
3a27086534 | ||
|
|
413c1e41b5 | ||
|
|
8b879464b8 | ||
|
|
97c9bbc1fe | ||
|
|
e5bebef7b2 | ||
|
|
73752c4b3f | ||
|
|
dcf7c9838d | ||
|
|
f325e7694b | ||
|
|
75d204c9ca | ||
|
|
cf4008951d | ||
|
|
d67818d096 | ||
|
|
6aaea8ad4f | ||
|
|
de41278096 | ||
|
|
f01ab70907 | ||
|
|
bbdb221a67 | ||
|
|
7d053f8c79 | ||
|
|
fcad0bf3ca | ||
|
|
58f929f6b5 | ||
|
|
943198c56e | ||
|
|
2112ad4004 | ||
|
|
5995dc81f3 | ||
|
|
104e752634 | ||
|
|
fc335cd15d | ||
|
|
45923ba0d3 | ||
|
|
8fefa99d81 | ||
|
|
85d652cd67 | ||
|
|
88e41e9c7d | ||
|
|
26750488d0 | ||
|
|
a0f582b2b9 | ||
|
|
917b53bec2 | ||
|
|
e44a93422a | ||
|
|
e115ef4259 | ||
|
|
bcec699124 | ||
|
|
d23c728a1a | ||
|
|
0a2d507dec | ||
|
|
0c08617606 | ||
|
|
29fba45c33 | ||
|
|
1071a455b6 | ||
|
|
57e4b36fd7 | ||
|
|
f08fa92294 | ||
|
|
3a8d87d292 | ||
|
|
53302c9e90 | ||
|
|
28a24d8c03 | ||
|
|
3e52411bc4 | ||
|
|
1548ea18e1 | ||
|
|
f8cd1ba8e5 | ||
|
|
d18a4be31f | ||
|
|
c56f270ed6 | ||
|
|
2bca3d1032 | ||
|
|
9f81d69c5e | ||
|
|
815419260d | ||
|
|
6729a3d51f | ||
|
|
6d8ce280dd | ||
|
|
9e5b455065 | ||
|
|
09cbac3019 | ||
|
|
5354ad3934 | ||
|
|
7754048b80 | ||
|
|
ffeeff260f | ||
|
|
71ea74d273 | ||
|
|
3a744c79ae | ||
|
|
d9ba27a1ec | ||
|
|
0a3540049c | ||
|
|
ab50ca9bbd | ||
|
|
0bee793ab8 | ||
|
|
89fc5e19d9 | ||
|
|
6a3bf51084 | ||
|
|
df1e298c84 | ||
|
|
b9c706324b | ||
|
|
ab7e2de1d9 | ||
|
|
7ef8753b94 | ||
|
|
d48a5efa03 | ||
|
|
0804b62ffb | ||
|
|
37995b9ec7 | ||
|
|
7a3e6fde7f | ||
|
|
bd4291462e | ||
|
|
489d6778db | ||
|
|
538a1413d9 | ||
|
|
3c9b755045 | ||
|
|
fd7b345efa | ||
|
|
c96468149a | ||
|
|
d6d4897e1c | ||
|
|
e07e248fea | ||
|
|
b561460307 | ||
|
|
f6d1a27f51 | ||
|
|
80a1500634 | ||
|
|
bcb578c933 | ||
|
|
75ad454971 | ||
|
|
e90b7f2d81 | ||
|
|
ffca11ca8a | ||
|
|
bd98951d32 | ||
|
|
9106fb82f3 | ||
|
|
5c6e17bc89 | ||
|
|
538e9e9b4e | ||
|
|
d871b3c7d6 | ||
|
|
99c6c9b411 | ||
|
|
3eca4ada5a | ||
|
|
19431abc73 |
@@ -66,6 +66,7 @@ packages/lib/welcomeAssets.js
|
||||
packages/plugins/**/api
|
||||
packages/plugins/**/dist
|
||||
packages/server/dist/
|
||||
packages/utils/dist/
|
||||
packages/tools/node_modules
|
||||
packages/tools/PortableAppsLauncher
|
||||
packages/turndown-plugin-gfm/
|
||||
@@ -395,6 +396,7 @@ packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
@@ -404,6 +406,7 @@ packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
@@ -709,6 +712,7 @@ packages/lib/services/searchengine/SearchFilter.test.js
|
||||
packages/lib/services/searchengine/filterParser.js
|
||||
packages/lib/services/searchengine/filterParser.test.js
|
||||
packages/lib/services/searchengine/gotoAnythingStyleQuery.js
|
||||
packages/lib/services/searchengine/gotoAnythingStyleQuery.test.js
|
||||
packages/lib/services/searchengine/queryBuilder.js
|
||||
packages/lib/services/share/ShareService.js
|
||||
packages/lib/services/share/ShareService.test.js
|
||||
@@ -846,6 +850,7 @@ packages/tools/convertThemesToCss.js
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/git-changelog.test.js
|
||||
packages/tools/licenseChecker.js
|
||||
packages/tools/release-android.js
|
||||
packages/tools/release-cli.js
|
||||
@@ -861,6 +866,7 @@ packages/tools/update-readme-download.js
|
||||
packages/tools/update-readme-sponsors.js
|
||||
packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/website/build.js
|
||||
packages/tools/website/buildTranslations.js
|
||||
@@ -868,6 +874,8 @@ packages/tools/website/updateDownloadPage.js
|
||||
packages/tools/website/updateNews.js
|
||||
packages/tools/website/utils/applyTranslations.js
|
||||
packages/tools/website/utils/applyTranslations.test.js
|
||||
packages/tools/website/utils/convertLinksToLocale.js
|
||||
packages/tools/website/utils/convertLinksToLocale.test.js
|
||||
packages/tools/website/utils/frontMatter.js
|
||||
packages/tools/website/utils/news.js
|
||||
packages/tools/website/utils/openGraph.js
|
||||
|
||||
@@ -91,6 +91,10 @@ module.exports = {
|
||||
// Disable because of this: https://github.com/facebook/react/issues/16265
|
||||
// "react-hooks/exhaustive-deps": "warn",
|
||||
|
||||
'jest/require-top-level-describe': ['error', { 'maxNumberOfTopLevelDescribes': 1 }],
|
||||
'jest/no-identical-title': ['error'],
|
||||
'jest/prefer-lowercase-title': ['error', { 'ignoreTopLevelDescribe': true }],
|
||||
|
||||
'promise/prefer-await-to-then': 'error',
|
||||
'no-unneeded-ternary': 'error',
|
||||
|
||||
@@ -155,6 +159,7 @@ module.exports = {
|
||||
// 'react-hooks',
|
||||
'import',
|
||||
'promise',
|
||||
'jest',
|
||||
],
|
||||
'overrides': [
|
||||
{
|
||||
|
||||
2
.github/workflows/build-android.yml
vendored
2
.github/workflows/build-android.yml
vendored
@@ -6,6 +6,7 @@ on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
pre_job:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
@@ -16,6 +17,7 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
|
||||
BuildAndroidDebug:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
1
.github/workflows/cla.yml
vendored
1
.github/workflows/cla.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
CLAAssistant:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
|
||||
1
.github/workflows/close-stale-issues.yml
vendored
1
.github/workflows/close-stale-issues.yml
vendored
@@ -6,6 +6,7 @@ permissions:
|
||||
issues: write
|
||||
jobs:
|
||||
ProcessStaleIssues:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
|
||||
10
.github/workflows/github-actions-main.yml
vendored
10
.github/workflows/github-actions-main.yml
vendored
@@ -2,6 +2,7 @@ name: Joplin Continuous Integration
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
pre_job:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
@@ -14,7 +15,7 @@ jobs:
|
||||
Main:
|
||||
needs: pre_job
|
||||
# We always process server or desktop release tags, because they also publish the release
|
||||
if: needs.pre_job.outputs.should_skip != 'true' || startsWith(github.ref, 'refs/tags/server-v') || startsWith(github.ref, 'refs/tags/v')
|
||||
if: github.repository == 'laurent22/joplin' && (needs.pre_job.outputs.should_skip != 'true' || startsWith(github.ref, 'refs/tags/server-v') || startsWith(github.ref, 'refs/tags/v'))
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -70,7 +71,9 @@ jobs:
|
||||
- uses: olegtarasov/get-tag@v2.1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||
# https://github.com/facebook/react-native/issues/36440
|
||||
node-version: '18.15.0'
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
@@ -92,6 +95,7 @@ jobs:
|
||||
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CSC_KEY_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
@@ -129,7 +133,7 @@ jobs:
|
||||
|
||||
ServerDockerImage:
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
if: github.repository == 'laurent22/joplin' && needs.pre_job.outputs.should_skip != 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -383,6 +383,7 @@ packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
@@ -392,6 +393,7 @@ packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
@@ -697,6 +699,7 @@ packages/lib/services/searchengine/SearchFilter.test.js
|
||||
packages/lib/services/searchengine/filterParser.js
|
||||
packages/lib/services/searchengine/filterParser.test.js
|
||||
packages/lib/services/searchengine/gotoAnythingStyleQuery.js
|
||||
packages/lib/services/searchengine/gotoAnythingStyleQuery.test.js
|
||||
packages/lib/services/searchengine/queryBuilder.js
|
||||
packages/lib/services/share/ShareService.js
|
||||
packages/lib/services/share/ShareService.test.js
|
||||
@@ -834,6 +837,7 @@ packages/tools/convertThemesToCss.js
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/git-changelog.test.js
|
||||
packages/tools/licenseChecker.js
|
||||
packages/tools/release-android.js
|
||||
packages/tools/release-cli.js
|
||||
@@ -849,6 +853,7 @@ packages/tools/update-readme-download.js
|
||||
packages/tools/update-readme-sponsors.js
|
||||
packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/website/build.js
|
||||
packages/tools/website/buildTranslations.js
|
||||
@@ -856,6 +861,8 @@ packages/tools/website/updateDownloadPage.js
|
||||
packages/tools/website/updateNews.js
|
||||
packages/tools/website/utils/applyTranslations.js
|
||||
packages/tools/website/utils/applyTranslations.test.js
|
||||
packages/tools/website/utils/convertLinksToLocale.js
|
||||
packages/tools/website/utils/convertLinksToLocale.test.js
|
||||
packages/tools/website/utils/frontMatter.js
|
||||
packages/tools/website/utils/news.js
|
||||
packages/tools/website/utils/openGraph.js
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"@joplin/turndown-plugin-gfm",
|
||||
"@joplin/tools",
|
||||
"@joplin/react-native-saf-x",
|
||||
"@joplin/react-native-alarm-notification"
|
||||
"@joplin/react-native-alarm-notification",
|
||||
"@joplin/utils"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
30
.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch
Normal file
30
.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch
Normal file
@@ -0,0 +1,30 @@
|
||||
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
|
||||
index a8abd71833879201e3438b2fa51d712a311c4551..ffe9c2c6dfa5c703ba76b65d94d5dd6784102c19 100644
|
||||
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
|
||||
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
|
||||
@@ -591,7 +591,7 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
|
||||
// ignored.printStackTrace();
|
||||
}
|
||||
|
||||
- RNFetchBlobFileResp rnFetchBlobFileResp = (RNFetchBlobFileResp) responseBody;
|
||||
+ RNFetchBlobFileResp rnFetchBlobFileResp = new RNFetchBlobFileResp(responseBody);
|
||||
|
||||
if(rnFetchBlobFileResp != null && !rnFetchBlobFileResp.isDownloadComplete()){
|
||||
callback.invoke("Download interrupted.", null);
|
||||
diff --git a/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java b/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java
|
||||
index 2470eef612308c15a89dfea5a1f16937469be29f..965f8becc195965907699182c764ec9e51811450 100644
|
||||
--- a/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java
|
||||
+++ b/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java
|
||||
@@ -35,6 +35,12 @@ public class RNFetchBlobFileResp extends ResponseBody {
|
||||
FileOutputStream ofStream;
|
||||
boolean isEndMarkerReceived;
|
||||
|
||||
+ // ref: https://github.com/joltup/rn-fetch-blob/issues/490#issuecomment-990899440
|
||||
+ public RNFetchBlobFileResp(ResponseBody body) {
|
||||
+ super();
|
||||
+ this.originalBody = body;
|
||||
+ }
|
||||
+
|
||||
public RNFetchBlobFileResp(ReactApplicationContext ctx, String taskId, ResponseBody body, String path, boolean overwrite) throws IOException {
|
||||
super();
|
||||
this.rctContext = ctx;
|
||||
@@ -657,6 +657,16 @@ footer .bottom-links-row p {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.language-switcher > button {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: #0557ba;
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
WHAT'S NEW PAGE
|
||||
*****************************************************************/
|
||||
@@ -780,6 +790,7 @@ footer .bottom-links-row p {
|
||||
|
||||
#menu-mobile .social-links .social-link-mastodon,
|
||||
#menu-mobile .social-links .social-link-reddit,
|
||||
#menu-mobile .social-links .social-link-linkedin,
|
||||
#menu-mobile .social-links .social-link-patreon {
|
||||
display: none;
|
||||
}
|
||||
@@ -947,6 +958,41 @@ footer .bottom-links-row p {
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
MORE NARROW VIEW
|
||||
eg for Galaxy S9
|
||||
*****************************************************************/
|
||||
|
||||
@media (max-width: 580px) {
|
||||
|
||||
#nav-section .plans-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
MORE NARROW VIEW
|
||||
eg for Galaxy S9
|
||||
*****************************************************************/
|
||||
|
||||
@media (max-width: 400px) {
|
||||
|
||||
#nav-section .navbar-mobile-content a.sponsor-button .sponsor-button-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#nav-section .navbar-mobile-content a.sponsor-button {
|
||||
padding: 2px 6px;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
#nav-section a {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
VERY NARROW VIEW
|
||||
eg for Galaxy Fold
|
||||
@@ -968,6 +1014,15 @@ footer .bottom-links-row p {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
div.navbar-mobile-content a.sponsor-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#nav-section .button-link {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
|
||||
7
Assets/WebsiteAssets/js/bootstrap5.0.2.bundle.min.js
vendored
Normal file
7
Assets/WebsiteAssets/js/bootstrap5.0.2.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5016
Assets/WebsiteAssets/js/bootstrap5.0.2.js
vendored
5016
Assets/WebsiteAssets/js/bootstrap5.0.2.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -421,7 +421,7 @@
|
||||
</div>
|
||||
|
||||
<script
|
||||
src="{{jsBaseUrl}}/bootstrap5.0.2.min.js"
|
||||
src="{{jsBaseUrl}}/bootstrap5.0.2.bundle.min.js"
|
||||
rel="preload"
|
||||
as="script"
|
||||
></script>
|
||||
|
||||
@@ -85,6 +85,11 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
|
||||
{{> footer}}
|
||||
</div>
|
||||
|
||||
<script
|
||||
src="{{jsBaseUrl}}/bootstrap5.0.2.bundle.min.js"
|
||||
rel="preload"
|
||||
as="script"
|
||||
></script>
|
||||
<script src="{{{assetUrls.js.script}}}"></script>
|
||||
|
||||
{{> analytics}}
|
||||
|
||||
@@ -17,6 +17,21 @@
|
||||
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
|
||||
<a href="{{forumUrl}}" class="fw500">Forum</a>
|
||||
<a href="{{baseUrl}}/cn/" class="fw500">中文</a>
|
||||
|
||||
<!--
|
||||
<div class="dropdown language-switcher">
|
||||
<button class="fw500" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Language
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
||||
<li><a class="dropdown-item" href="#">Action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Another action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
-->
|
||||
|
||||
|
||||
{{#showJoplinCloudLinks}}
|
||||
{{> joplinCloudButton}}
|
||||
{{/showJoplinCloudLinks}}
|
||||
@@ -24,7 +39,8 @@
|
||||
</div>
|
||||
<div class="col-9 text-right d-block d-md-none navbar-mobile-content">
|
||||
{{> twitterLink}}
|
||||
<a href="{{baseUrl}}/cn/" class="fw500">中文</a>
|
||||
<a href="{{baseUrl}}/cn/" class="fw500 chinese-page-link">中文</a>
|
||||
{{> joplinCloudButton}}
|
||||
{{> supportButton}}
|
||||
|
||||
<span class="pointer"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<a class="social-link-mastodon" href="https://mastodon.social/@joplinapp" title="Joplin Mastodon feed"><i class="fab fa-mastodon"></i></a>
|
||||
<a class="social-link-patreon" href="https://www.patreon.com/joplin" title="Joplin Patreon"><i class="fab fa-patreon"></i></a>
|
||||
<a class="social-link-discord" href="https://discord.gg/VSj7AFHvpq" title="Joplin Discord chat"><i class="fab fa-discord"></i></a>
|
||||
<a class="social-link-linkedin" href="https://www.linkedin.com/company/joplin" title="Joplin LinkedIn Feed"><i class="fab fa-linkedin"></i></a>
|
||||
<a class="social-link-reddit" href="https://www.reddit.com/r/joplinapp/" title="Joplin Subreddit"><i class="fab fa-reddit"></i></a>
|
||||
<a class="social-link-github" href="https://github.com/laurent22/joplin/" title="Joplin GitHub repository"><i class="fab fa-github"></i></a>
|
||||
</div>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500"><i class="fab fa-twitter"></i></a>
|
||||
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500 twitter-link"><i class="fab fa-twitter"></i></a>
|
||||
@@ -30,6 +30,7 @@ COPY packages/fork-uslug ./packages/fork-uslug
|
||||
COPY packages/htmlpack ./packages/htmlpack
|
||||
COPY packages/renderer ./packages/renderer
|
||||
COPY packages/tools ./packages/tools
|
||||
COPY packages/utils ./packages/utils
|
||||
COPY packages/lib ./packages/lib
|
||||
COPY packages/server ./packages/server
|
||||
|
||||
|
||||
17
README.md
17
README.md
@@ -64,7 +64,7 @@ A community maintained list of these distributions can be found here: [Unofficia
|
||||
# Sponsors
|
||||
|
||||
<!-- SPONSORS-ORG -->
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://usrigging.com/"><img title="U.S. Ringing Supply" width="256" src="https://joplinapp.org/images/sponsors/RingingSupply.svg"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-github&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://residence-greece.com/"><img title="Greece Golden Visa" width="256" src="https://joplinapp.org/images/sponsors/ResidenceGreece.jpg"/></a> <a href="https://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.png"/></a>
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://residence-greece.com/"><img title="Greece Golden Visa" width="256" src="https://joplinapp.org/images/sponsors/ResidenceGreece.jpg"/></a> <a href="https://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.png"/></a>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
@@ -72,14 +72,11 @@ A community maintained list of these distributions can be found here: [Unofficia
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
| | | | |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/3061769?s=96&v=4"/></br>[c-nagy](https://github.com/c-nagy) | <img width="50" src="https://avatars2.githubusercontent.com/u/70780798?s=96&v=4"/></br>[cabottech](https://github.com/cabottech) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/4862947?s=96&v=4"/></br>[chrootlogin](https://github.com/chrootlogin) | <img width="50" src="https://avatars2.githubusercontent.com/u/82579431?s=96&v=4"/></br>[clmntsl](https://github.com/clmntsl) | <img width="50" src="https://avatars2.githubusercontent.com/u/808091?s=96&v=4"/></br>[cuongtransc](https://github.com/cuongtransc) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[fbloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/49439044?s=96&v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars2.githubusercontent.com/u/38898566?s=96&v=4"/></br>[h4sh5](https://github.com/h4sh5) | <img width="50" src="https://avatars2.githubusercontent.com/u/3266447?s=96&v=4"/></br>[iamwillbar](https://github.com/iamwillbar) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) | <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) | <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars2.githubusercontent.com/u/77214738?s=96&v=4"/></br>[Polymathic-Company](https://github.com/Polymathic-Company) | <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/54626606?s=96&v=4"/></br>[skyrunner15](https://github.com/skyrunner15) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) | | | |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) | <img width="50" src="https://avatars2.githubusercontent.com/u/2793530?s=96&v=4"/></br>[CyberXZT](https://github.com/CyberXZT) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/49439044?s=96&v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) | <img width="50" src="https://avatars2.githubusercontent.com/u/3266447?s=96&v=4"/></br>[iamwillbar](https://github.com/iamwillbar) | <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | |
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
|
||||
<!-- TOC -->
|
||||
@@ -129,6 +126,7 @@ A community maintained list of these distributions can be found here: [Unofficia
|
||||
- [Writing a technical spec](https://github.com/laurent22/joplin/blob/dev/readme/technical_spec.md)
|
||||
- [Desktop application styling](https://github.com/laurent22/joplin/blob/dev/readme/spec/desktop_styling.md)
|
||||
- [Note History spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/history.md)
|
||||
- [Synchronisation spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync.md)
|
||||
- [Sync Lock spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_lock.md)
|
||||
- [Synchronous Scroll spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_scroll.md)
|
||||
- [Plugin Architecture spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/plugins.md)
|
||||
@@ -505,6 +503,7 @@ Name | Description
|
||||
[Mastodon feed](https://mastodon.social/@joplinapp) | Follow us on Mastodon
|
||||
[Patreon page](https://www.patreon.com/joplin) |The latest news are often posted there
|
||||
[Discord server](https://discord.gg/VSj7AFHvpq) | Our chat server
|
||||
[LinkedIn](https://www.linkedin.com/company/joplin) | Our LinkedIn page
|
||||
[Sub-reddit](https://www.reddit.com/r/joplinapp/) | Also a good place to get help
|
||||
|
||||
# Contributing
|
||||
|
||||
7
bootstrap.bundle.min.js
vendored
Normal file
7
bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
54
gulpfile.js
54
gulpfile.js
@@ -1,26 +1,52 @@
|
||||
const gulp = require('gulp');
|
||||
const utils = require('./packages/tools/gulp/utils');
|
||||
const execa = require('execa');
|
||||
const { stdout } = require('process');
|
||||
|
||||
const execCommand = async (executableName, args, options = null) => {
|
||||
options = {
|
||||
showInput: true,
|
||||
showStdout: true,
|
||||
showStderr: true,
|
||||
quiet: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (options.quiet) {
|
||||
options.showInput = false;
|
||||
options.showStdout = false;
|
||||
options.showStderr = false;
|
||||
}
|
||||
|
||||
if (options.showInput) {
|
||||
stdout.write(`> ${executableName} ${args.join(' ')}\n`);
|
||||
}
|
||||
|
||||
const promise = execa(executableName, args);
|
||||
if (options.showStdout && promise.stdout) promise.stdout.pipe(process.stdout);
|
||||
if (options.showStderr && promise.stderr) promise.stderr.pipe(process.stderr);
|
||||
const result = await promise;
|
||||
return result.stdout.trim();
|
||||
};
|
||||
|
||||
|
||||
const tasks = {
|
||||
updateIgnoredTypeScriptBuild: require('./packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
buildCommandIndex: require('./packages/tools/gulp/tasks/buildCommandIndex'),
|
||||
completePublishAll: {
|
||||
fn: async () => {
|
||||
await utils.execCommandVerbose('git', ['add', '-A']);
|
||||
await utils.execCommandVerbose('git', ['commit', '-m', 'Releasing sub-packages']);
|
||||
await execCommand('git', ['add', '-A']);
|
||||
await execCommand('git', ['commit', '-m', 'Releasing sub-packages']);
|
||||
|
||||
// Lerna does some unnecessary auth check that doesn't work with
|
||||
// automation tokens, thus the --no-verify-access. Automation token
|
||||
// is still used for access when publishing even with this flag
|
||||
// (publishing would fail otherwise).
|
||||
// https://github.com/lerna/lerna/issues/2788
|
||||
await utils.execCommandVerbose('lerna', ['publish', 'from-package', '-y', '--no-verify-access']);
|
||||
await execCommand('lerna', ['publish', 'from-package', '-y', '--no-verify-access']);
|
||||
|
||||
await utils.execCommandVerbose('yarn', ['install']);
|
||||
await utils.execCommandVerbose('git', ['add', '-A']);
|
||||
await utils.execCommandVerbose('git', ['commit', '-m', 'Lock file']);
|
||||
await execCommand('yarn', ['install']);
|
||||
await execCommand('git', ['add', '-A']);
|
||||
await execCommand('git', ['commit', '-m', 'Lock file']);
|
||||
|
||||
await utils.execCommandVerbose('git', ['push']);
|
||||
await execCommand('git', ['push']);
|
||||
},
|
||||
},
|
||||
build: {
|
||||
@@ -33,12 +59,14 @@ const tasks = {
|
||||
// faster, especially when having to rebuild after adding a
|
||||
// dependency.
|
||||
if (process.env.BUILD_SEQUENCIAL === '1') {
|
||||
await utils.execCommandVerbose('yarn', ['run', 'buildSequential']);
|
||||
await execCommand('yarn', ['run', 'buildSequential']);
|
||||
} else {
|
||||
await utils.execCommandVerbose('yarn', ['run', 'buildParallel']);
|
||||
await execCommand('yarn', ['run', 'buildParallel']);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
for (const taskName in tasks) {
|
||||
gulp.task(taskName, tasks[taskName].fn);
|
||||
}
|
||||
|
||||
@@ -329,6 +329,7 @@
|
||||
"packages/renderer/MdToHtml/rules/sanitize_html.js": true,
|
||||
"packages/server/db-*.sqlite": true,
|
||||
"packages/server/dist/": true,
|
||||
"packages/utils/dist/": true,
|
||||
"packages/server/temp": true,
|
||||
"packages/server/test.pid": true,
|
||||
"phpunit.xml": true,
|
||||
|
||||
16
package.json
16
package.json
@@ -15,7 +15,7 @@
|
||||
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 --topological run build && yarn run tsc",
|
||||
"buildSequential": "yarn workspaces foreach --verbose --interlaced --topological run build && yarn run tsc",
|
||||
"buildApiDoc": "yarn workspace joplin start apidoc ../../readme/api/references/rest_api.md",
|
||||
"buildCommandIndex": "gulp buildCommandIndex",
|
||||
"buildCommandIndex": "node packages/tools/gulp/tasks/buildCommandIndexRun.js",
|
||||
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../joplin-website/docs/api/references/plugin_api packages/lib/services/plugins/api/",
|
||||
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
|
||||
"updateNews": "node ./packages/tools/website/updateNews",
|
||||
@@ -53,7 +53,7 @@
|
||||
"test-ci": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 2 run test-ci",
|
||||
"test": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 2 run test",
|
||||
"tsc": "yarn workspaces foreach --parallel --verbose --interlaced run tsc",
|
||||
"updateIgnored": "gulp updateIgnoredTypeScriptBuild",
|
||||
"updateIgnored": "node packages/tools/gulp/tasks/updateIgnoredTypeScriptBuildRun.js",
|
||||
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
|
||||
"watch": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 999 run watch",
|
||||
"watchWebsite": "nodemon --verbose --watch Assets/WebsiteAssets --watch packages/tools/website --watch packages/tools/website/utils --ext md,ts,js,mustache,css,tsx,gif,png,svg --exec \"node packages/tools/website/build.js && http-server --port 8077 ../joplin-website/docs -a localhost\""
|
||||
@@ -64,6 +64,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/utils": "~2.11",
|
||||
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.2",
|
||||
"@typescript-eslint/parser": "5.48.2",
|
||||
@@ -71,14 +72,16 @@
|
||||
"eslint": "8.31.0",
|
||||
"eslint-interactive": "10.3.0",
|
||||
"eslint-plugin-import": "2.27.4",
|
||||
"eslint-plugin-jest": "27.2.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-react": "7.32.0",
|
||||
"fs-extra": "11.1.0",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.1.1",
|
||||
"glob": "8.1.0",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "3.1.0",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "13.1.2",
|
||||
"lint-staged": "13.2.0",
|
||||
"madge": "6.0.0",
|
||||
"npm-package-json-lint": "6.4.0",
|
||||
"typedoc": "0.17.8",
|
||||
@@ -88,10 +91,11 @@
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"http-server": "14.1.1",
|
||||
"node-gyp": "9.3.1",
|
||||
"nodemon": "2.0.20"
|
||||
"nodemon": "2.0.22"
|
||||
},
|
||||
"packageManager": "yarn@3.3.1",
|
||||
"resolutions": {
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch"
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||
"rn-fetch-blob@0.12.0": "patch:rn-fetch-blob@npm%3A0.12.0#./.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const Resource = require('@joplin/lib/models/Resource').default;
|
||||
const Setting = require('@joplin/lib/models/Setting').default;
|
||||
const reducer = require('@joplin/lib/reducer').default;
|
||||
const { defaultState } = require('@joplin/lib/reducer');
|
||||
const { splitCommandString } = require('@joplin/lib/string-utils.js');
|
||||
const { splitCommandString } = require('@joplin/utils');
|
||||
const { reg } = require('@joplin/lib/registry.js');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
|
||||
@@ -9,7 +9,8 @@ const Tag = require('@joplin/lib/models/Tag').default;
|
||||
const Setting = require('@joplin/lib/models/Setting').default;
|
||||
const { reg } = require('@joplin/lib/registry.js');
|
||||
const { fileExtension } = require('@joplin/lib/path-utils');
|
||||
const { splitCommandString, splitCommandBatch } = require('@joplin/lib/string-utils');
|
||||
const { splitCommandString } = require('@joplin/utils');
|
||||
const { splitCommandBatch } = require('@joplin/lib/string-utils');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const fs = require('fs-extra');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const fs = require('fs-extra');
|
||||
const BaseCommand = require('./base-command').default;
|
||||
const { splitCommandString } = require('@joplin/lib/string-utils.js');
|
||||
const { splitCommandString } = require('@joplin/utils');
|
||||
const uuid = require('@joplin/lib/uuid').default;
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
|
||||
@@ -30,22 +30,24 @@
|
||||
2019,
|
||||
2020,
|
||||
2021,
|
||||
2022
|
||||
2022,
|
||||
2023
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "2.10.3",
|
||||
"version": "2.11.0",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "~2.10",
|
||||
"@joplin/renderer": "~2.10",
|
||||
"@joplin/lib": "~2.11",
|
||||
"@joplin/renderer": "~2.11",
|
||||
"@joplin/utils": "~2.11",
|
||||
"aws-sdk": "2.1290.0",
|
||||
"chalk": "4.1.2",
|
||||
"compare-version": "0.1.2",
|
||||
"fs-extra": "11.1.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"html-entities": "1.4.0",
|
||||
"image-type": "3.1.0",
|
||||
"keytar": "7.9.0",
|
||||
@@ -57,7 +59,7 @@
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.31.3",
|
||||
"sprintf-js": "1.1.2",
|
||||
"sqlite3": "5.1.4",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
"strip-ansi": "6.0.1",
|
||||
"tcp-port-used": "1.0.2",
|
||||
@@ -68,7 +70,7 @@
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~2.10",
|
||||
"@joplin/tools": "~2.11",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/jest": "29.2.6",
|
||||
"@types/node": "18.11.18",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
|
||||
import KeychainService from '@joplin/lib/services/keychain/KeychainService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
@@ -47,9 +47,11 @@ describe('services_plugins_RepositoryApi', () => {
|
||||
|
||||
it('should tell if a plugin can be updated', (async () => {
|
||||
const api = await newRepoApi();
|
||||
expect(await api.pluginCanBeUpdated('org.joplinapp.plugins.ToggleSidebars', '1.0.0')).toBe(true);
|
||||
expect(await api.pluginCanBeUpdated('org.joplinapp.plugins.ToggleSidebars', '1.0.2')).toBe(false);
|
||||
expect(await api.pluginCanBeUpdated('does.not.exist', '1.0.0')).toBe(false);
|
||||
|
||||
expect(await api.pluginCanBeUpdated('org.joplinapp.plugins.ToggleSidebars', '1.0.0', '3.0.0')).toBe(true);
|
||||
expect(await api.pluginCanBeUpdated('org.joplinapp.plugins.ToggleSidebars', '1.0.0', '1.0.0')).toBe(false);
|
||||
expect(await api.pluginCanBeUpdated('org.joplinapp.plugins.ToggleSidebars', '1.0.2', '3.0.0')).toBe(false);
|
||||
expect(await api.pluginCanBeUpdated('does.not.exist', '1.0.0', '3.0.0')).toBe(false);
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import * as fs from 'fs-extra';
|
||||
import { homedir } from 'os';
|
||||
import { execCommand2 } from '@joplin/tools/tool-utils';
|
||||
import { execCommand } from '@joplin/utils';
|
||||
import { chdir } from 'process';
|
||||
|
||||
const minUserNum = 1;
|
||||
@@ -66,7 +66,7 @@ const processUser = async (userNum: number) => {
|
||||
|
||||
await chdir(cliDir);
|
||||
|
||||
await execCommand2(['yarn', 'run', 'start-no-build', '--', '--profile', profileDir, 'batch', commandFile]);
|
||||
await execCommand(['yarn', 'run', 'start-no-build', '--', '--profile', profileDir, 'batch', commandFile]);
|
||||
} catch (error) {
|
||||
console.error(`Could not process user ${userNum}:`, error);
|
||||
} finally {
|
||||
@@ -90,7 +90,7 @@ const main = async () => {
|
||||
|
||||
// Build the app once before starting, because we'll use start-no-build to
|
||||
// run the scripts (faster)
|
||||
await execCommand2(['yarn', 'run', 'build']);
|
||||
await execCommand(['yarn', 'run', 'build']);
|
||||
|
||||
const focusUserNum = 0;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "2.10.0",
|
||||
"version": "2.11.2",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
||||
@@ -126,4 +126,4 @@
|
||||
"react-app"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import appReducer, { createAppDefaultState } from './app.reducer';
|
||||
|
||||
describe('app.reducer', () => {
|
||||
|
||||
it('DIALOG_OPEN', async () => {
|
||||
it('should handle DIALOG_OPEN', async () => {
|
||||
const state: AppState = createAppDefaultState({}, {});
|
||||
|
||||
let newState = appReducer(state, {
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function(props: Props) {
|
||||
let cancelled = false;
|
||||
|
||||
async function fetchPluginIds() {
|
||||
const pluginIds = await repoApi().canBeUpdatedPlugins(pluginItems.map(p => p.manifest));
|
||||
const pluginIds = await repoApi().canBeUpdatedPlugins(pluginItems.map(p => p.manifest), pluginService.appVersion);
|
||||
if (cancelled) return;
|
||||
const conv: Record<string, boolean> = {};
|
||||
pluginIds.forEach(id => conv[id] = true);
|
||||
@@ -155,7 +155,7 @@ export default function(props: Props) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [manifestsLoaded, pluginItems]);
|
||||
}, [manifestsLoaded, pluginItems, pluginService.appVersion]);
|
||||
|
||||
const onDelete = useCallback(async (event: ItemEvent) => {
|
||||
const item = event.item;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const logger = Logger.create('ItemList');
|
||||
|
||||
interface Props {
|
||||
style: any;
|
||||
@@ -47,6 +50,15 @@ class ItemList extends React.Component<Props, State> {
|
||||
let bottomItemIndex = topItemIndex + (visibleItemCount - 1);
|
||||
if (bottomItemIndex >= props.items.length) bottomItemIndex = props.items.length - 1;
|
||||
|
||||
// EDGE CASE:
|
||||
// ref: https://github.com/laurent22/joplin/issues/4124
|
||||
// when the note list is hidden, visibleItemCount is negative, and scroll top is positive when a note is selected
|
||||
if (visibleItemCount < 0 && this.scrollTop_ > 0) {
|
||||
logger.warn('Resetting scrollTop to 0. visibleItemCount is negative, scrollTop is positive.');
|
||||
// we will reset the scroll top so that there is no blank space at the top of note list
|
||||
this.scrollTop_ = 0;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
topItemIndex: topItemIndex,
|
||||
bottomItemIndex: bottomItemIndex,
|
||||
@@ -69,6 +81,21 @@ class ItemList extends React.Component<Props, State> {
|
||||
this.updateStateItemIndexes(newProps);
|
||||
}
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
// EDGE CASE
|
||||
// scroll top is not updated when item list visibility is toggled
|
||||
// if the user was at the bottom of the item list before hiding, blank spaces are added at the bottom of the item list
|
||||
if (this.offsetScroll() !== this.listRef.current?.scrollTop) {
|
||||
logger.warn(`scrollTop mismatch. Updating scrollTop with current listRef scrollTop(${this.listRef.current.scrollTop})`);
|
||||
// update scroll postion once if there is a mismatch in scroll position after showing item list
|
||||
this.onScroll({
|
||||
target: {
|
||||
scrollTop: this.listRef.current.scrollTop,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onScroll(event: any) {
|
||||
this.scrollTop_ = event.target.scrollTop;
|
||||
this.updateStateItemIndexes();
|
||||
|
||||
@@ -21,7 +21,7 @@ import checkForUpdates from '../checkForUpdates';
|
||||
const { connect } = require('react-redux');
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
const packageInfo = require('../packageInfo.js');
|
||||
const { clipboard } = require('electron');
|
||||
const Menu = bridge().Menu;
|
||||
@@ -128,6 +128,7 @@ interface Props {
|
||||
customCss: string;
|
||||
locale: string;
|
||||
profileConfig: ProfileConfig;
|
||||
pluginSettings: PluginSettings;
|
||||
}
|
||||
|
||||
const commandNames: string[] = menuCommandNames();
|
||||
@@ -487,8 +488,7 @@ function useMenu(props: Props) {
|
||||
}
|
||||
|
||||
function _showAbout() {
|
||||
const v = versionInfo(packageInfo, PluginService.instance().plugins);
|
||||
|
||||
const v = versionInfo(packageInfo, PluginService.instance().enabledPlugins(props.pluginSettings));
|
||||
|
||||
const copyToClipboard = bridge().showMessageBox(v.message, {
|
||||
icon: `${bridge().electronApp().buildDir()}/icons/128x128.png`,
|
||||
@@ -931,6 +931,7 @@ function useMenu(props: Props) {
|
||||
props['spellChecker.languages'],
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
props['spellChecker.enabled'],
|
||||
props.pluginSettings,
|
||||
props.customCss,
|
||||
props.locale,
|
||||
props.profileConfig,
|
||||
@@ -986,6 +987,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
['folders.sortOrder.field']: state.settings['folders.sortOrder.field'],
|
||||
['notes.sortOrder.reverse']: state.settings['notes.sortOrder.reverse'],
|
||||
['folders.sortOrder.reverse']: state.settings['folders.sortOrder.reverse'],
|
||||
pluginSettings: state.settings['plugins.states'],
|
||||
showNoteCounts: state.settings.showNoteCounts,
|
||||
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||
showCompletedTodos: state.settings.showCompletedTodos,
|
||||
|
||||
@@ -641,8 +641,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
resourceInfos: props.resourceInfos,
|
||||
contentMaxWidth: props.contentMaxWidth,
|
||||
mapsToLine: true,
|
||||
// Always using useCustomPdfViewer for now, we can add a new setting for it in future if we need to.
|
||||
useCustomPdfViewer: props.useCustomPdfViewer,
|
||||
// turning off custom pdf viewer; ref: https://github.com/laurent22/joplin/issues/8028
|
||||
useCustomPdfViewer: false,
|
||||
noteId: props.noteId,
|
||||
vendorDir: bridge().vendorDir(),
|
||||
}));
|
||||
|
||||
@@ -77,16 +77,6 @@ function stripMarkup(markupLanguage: number, markup: string, options: any = null
|
||||
return markupToHtml_.stripMarkup(markupLanguage, markup, options);
|
||||
}
|
||||
|
||||
function createSyntheticClipboardEventWithoutHTML(): ClipboardEvent {
|
||||
const clipboardData = new DataTransfer();
|
||||
for (const format of clipboard.availableFormats()) {
|
||||
if (format !== 'text/html') {
|
||||
clipboardData.setData(format, clipboard.read(format));
|
||||
}
|
||||
}
|
||||
return new ClipboardEvent('paste', { clipboardData });
|
||||
}
|
||||
|
||||
interface TinyMceCommand {
|
||||
name: string;
|
||||
value?: any;
|
||||
@@ -1076,24 +1066,24 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(event: any) {
|
||||
async function onKeyDown(event: any) {
|
||||
// It seems "paste as text" is handled automatically on Windows and Linux,
|
||||
// so we need to run the below code only on macOS. If we were to run this
|
||||
// on Windows/Linux, we would have this double-paste issue:
|
||||
// https://github.com/laurent22/joplin/issues/4243
|
||||
|
||||
// Handle "paste as text". Note that when pressing CtrlOrCmd+Shift+V it's going
|
||||
// to trigger the "keydown" event but not the "paste" event, so it's ok to process
|
||||
// it here and we don't need to do anything special in onPaste
|
||||
if (!shim.isWindows() && !shim.isLinux()) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
|
||||
pasteAsPlainText();
|
||||
}
|
||||
// While "paste as text" functionality is handled by Windows and Linux, if we
|
||||
// want to allow the user to customize the shortcut we need to prevent when it
|
||||
// has the default value so it doesn't paste the content twice
|
||||
// (one by the system and the other by our code)
|
||||
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
|
||||
event.preventDefault();
|
||||
pasteAsPlainText(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function onPasteAsText() {
|
||||
await onPaste(createSyntheticClipboardEventWithoutHTML());
|
||||
function onPasteAsText() {
|
||||
pasteAsPlainText(null);
|
||||
}
|
||||
|
||||
editor.on(TinyMceEditorEvents.KeyUp, onKeyUp);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react';
|
||||
import { ResourceInfos } from './types';
|
||||
import markupLanguageUtils from '../../../utils/markupLanguageUtils';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
@@ -23,6 +24,7 @@ export interface MarkupToHtmlOptions {
|
||||
useCustomPdfViewer?: boolean;
|
||||
noteId?: string;
|
||||
vendorDir?: string;
|
||||
platformName?: string;
|
||||
}
|
||||
|
||||
export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
@@ -40,6 +42,7 @@ export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
options = {
|
||||
replaceResourceInternalToExternalLinks: false,
|
||||
resourceInfos: {},
|
||||
platformName: shim.platformName(),
|
||||
...options,
|
||||
};
|
||||
|
||||
|
||||
@@ -241,6 +241,7 @@ const NoteListComponent = (props: Props) => {
|
||||
event.dataTransfer.setDragImage(new Image(), 1, 1);
|
||||
event.dataTransfer.clearData();
|
||||
event.dataTransfer.setData('text/x-jop-note-ids', JSON.stringify(noteIds));
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
const renderItem = useCallback((item: any, index: number) => {
|
||||
@@ -294,7 +295,7 @@ const NoteListComponent = (props: Props) => {
|
||||
useEffect(() => {
|
||||
if (previousSelectedNoteIds !== props.selectedNoteIds && props.selectedNoteIds.length === 1) {
|
||||
const id = props.selectedNoteIds[0];
|
||||
const doRefocus = props.notes.length < previousNotes.length;
|
||||
const doRefocus = props.notes.length < previousNotes.length && !props.focusedField;
|
||||
|
||||
for (let i = 0; i < props.notes.length; i++) {
|
||||
if (props.notes[i].id === id) {
|
||||
@@ -311,8 +312,7 @@ const NoteListComponent = (props: Props) => {
|
||||
if (previousVisible !== props.visible) {
|
||||
updateSizeState();
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [previousSelectedNoteIds, previousNotes, previousVisible, props.selectedNoteIds, props.notes]);
|
||||
}, [previousSelectedNoteIds, previousNotes, previousVisible, props.selectedNoteIds, props.notes, props.focusedField, props.visible]);
|
||||
|
||||
const scrollNoteIndex_ = (keyCode: any, ctrlKey: any, metaKey: any, noteIndex: any) => {
|
||||
|
||||
@@ -559,6 +559,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customCss,
|
||||
focusedField: state.focusedField,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -23,4 +23,5 @@ export interface Props {
|
||||
highlightedWords: string[];
|
||||
provisionalNoteIds: string[];
|
||||
visible: boolean;
|
||||
focusedField: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppState } from '../../app.reducer';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useMemo, useState } from 'react';
|
||||
import SearchBar from '../SearchBar/SearchBar';
|
||||
import Button, { ButtonLevel, ButtonSize, buttonSizePx } from '../Button/Button';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -11,6 +11,13 @@ import { _ } from '@joplin/lib/locale';
|
||||
const { connect } = require('react-redux');
|
||||
const styled = require('styled-components').default;
|
||||
|
||||
enum BaseBreakpoint {
|
||||
Sm = 75,
|
||||
Md = 80,
|
||||
Lg = 120,
|
||||
Xl = 474,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
showNewNoteButtons: boolean;
|
||||
sortOrderButtonsVisible: boolean;
|
||||
@@ -18,6 +25,15 @@ interface Props {
|
||||
sortOrderReverse: boolean;
|
||||
notesParentType: string;
|
||||
height: number;
|
||||
width: number;
|
||||
onContentHeightChange: (sameRow: boolean)=> void;
|
||||
}
|
||||
|
||||
interface Breakpoints {
|
||||
Sm: number;
|
||||
Md: number;
|
||||
Lg: number;
|
||||
Xl: number;
|
||||
}
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
@@ -34,7 +50,9 @@ const StyledButton = styled(Button)`
|
||||
width: auto;
|
||||
height: 26px;
|
||||
min-height: 26px;
|
||||
flex: 1 0 auto;
|
||||
min-width: 37px;
|
||||
max-width: none;
|
||||
white-space: nowrap;
|
||||
|
||||
.fa, .fas {
|
||||
font-size: 11px;
|
||||
@@ -54,7 +72,13 @@ const StyledPairButtonR = styled(Button)`
|
||||
width: auto;
|
||||
`;
|
||||
|
||||
const RowContainer = styled.div`
|
||||
const TopRow = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const BottomRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1 1 auto;
|
||||
@@ -68,7 +92,102 @@ const SortOrderButtonsContainer = styled.div`
|
||||
`;
|
||||
|
||||
function NoteListControls(props: Props) {
|
||||
const [dynamicBreakpoints, setDynamicBreakpoints] = useState<Breakpoints>({ Sm: BaseBreakpoint.Sm, Md: BaseBreakpoint.Md, Lg: BaseBreakpoint.Lg, Xl: BaseBreakpoint.Xl });
|
||||
|
||||
const searchBarRef = useRef(null);
|
||||
const newNoteRef = useRef(null);
|
||||
const newTodoRef = useRef(null);
|
||||
const noteControlsRef = useRef(null);
|
||||
const searchAndSortRef = useRef(null);
|
||||
|
||||
const getTextWidth = (text: string): number => {
|
||||
const canvas = document.createElement('canvas');
|
||||
if (!canvas) throw new Error('Failed to create canvas element');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to get context');
|
||||
const fontWeight = getComputedStyle(newNoteRef.current).getPropertyValue('font-weight');
|
||||
const fontSize = getComputedStyle(newNoteRef.current).getPropertyValue('font-size');
|
||||
const fontFamily = getComputedStyle(newNoteRef.current).getPropertyValue('font-family');
|
||||
ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;
|
||||
|
||||
return ctx.measureText(text).width;
|
||||
};
|
||||
|
||||
// Initialize language-specific breakpoints
|
||||
useEffect(() => {
|
||||
// Use the longest string to calculate the amount of extra width needed
|
||||
const smAdditional = getTextWidth(_('note')) > getTextWidth(_('to-do')) ? getTextWidth(_('note')) : getTextWidth(_('to-do'));
|
||||
const mdAdditional = getTextWidth(_('New note')) > getTextWidth(_('New to-do')) ? getTextWidth(_('New note')) : getTextWidth(_('New to-do'));
|
||||
|
||||
const Sm = BaseBreakpoint.Sm + smAdditional * 2;
|
||||
const Md = BaseBreakpoint.Md + mdAdditional * 2;
|
||||
const Lg = BaseBreakpoint.Lg + Md;
|
||||
const Xl = BaseBreakpoint.Xl;
|
||||
|
||||
setDynamicBreakpoints({ Sm, Md, Lg, Xl });
|
||||
}, []);
|
||||
|
||||
const breakpoint = useMemo(() => {
|
||||
// Find largest breakpoint that width is less than
|
||||
const index = Object.values(dynamicBreakpoints).findIndex(x => props.width < x);
|
||||
|
||||
return index === -1 ? dynamicBreakpoints.Xl : Object.values(dynamicBreakpoints)[index];
|
||||
}, [props.width, dynamicBreakpoints]);
|
||||
|
||||
const noteButtonText = useMemo(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Sm) {
|
||||
return '';
|
||||
} else if (breakpoint === dynamicBreakpoints.Md) {
|
||||
return _('note');
|
||||
} else {
|
||||
return _('New note');
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints]);
|
||||
|
||||
const todoButtonText = useMemo(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Sm) {
|
||||
return '';
|
||||
} else if (breakpoint === dynamicBreakpoints.Md) {
|
||||
return _('to-do');
|
||||
} else {
|
||||
return _('New to-do');
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints]);
|
||||
|
||||
const noteIcon = useMemo(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Sm) {
|
||||
return 'icon-note';
|
||||
} else {
|
||||
return 'fas fa-plus';
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints]);
|
||||
|
||||
const todoIcon = useMemo(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Sm) {
|
||||
return 'far fa-check-square';
|
||||
} else {
|
||||
return 'fas fa-plus';
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints]);
|
||||
|
||||
const showTooltip = useMemo(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Sm) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints.Sm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Xl) {
|
||||
noteControlsRef.current.style.flexDirection = 'row';
|
||||
searchAndSortRef.current.style.flex = '2 1 auto';
|
||||
props.onContentHeightChange(true);
|
||||
} else {
|
||||
noteControlsRef.current.style.flexDirection = 'column';
|
||||
props.onContentHeightChange(false);
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints, props.onContentHeightChange]);
|
||||
|
||||
useEffect(() => {
|
||||
CommandService.instance().registerRuntime('focusSearch', focusSearchRuntime(searchBarRef));
|
||||
@@ -127,33 +246,33 @@ function NoteListControls(props: Props) {
|
||||
if (!props.showNewNoteButtons) return null;
|
||||
|
||||
return (
|
||||
<RowContainer>
|
||||
<StyledButton
|
||||
<TopRow className="new-note-todo-buttons">
|
||||
<StyledButton ref={newNoteRef}
|
||||
className="new-note-button"
|
||||
tooltip={CommandService.instance().label('newNote')}
|
||||
iconName="fas fa-plus"
|
||||
title={_('%s', 'New note')}
|
||||
tooltip={ showTooltip ? CommandService.instance().label('newNote') : '' }
|
||||
iconName={noteIcon}
|
||||
title={_('%s', noteButtonText)}
|
||||
level={ButtonLevel.Primary}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onNewNoteButtonClick}
|
||||
/>
|
||||
<StyledButton
|
||||
<StyledButton ref={newTodoRef}
|
||||
className="new-todo-button"
|
||||
tooltip={CommandService.instance().label('newTodo')}
|
||||
iconName="fas fa-plus"
|
||||
title={_('%s', 'New to-do')}
|
||||
tooltip={ showTooltip ? CommandService.instance().label('newTodo') : '' }
|
||||
iconName={todoIcon}
|
||||
title={_('%s', todoButtonText)}
|
||||
level={ButtonLevel.Secondary}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onNewTodoButtonClick}
|
||||
/>
|
||||
</RowContainer>
|
||||
</TopRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<StyledRoot ref={noteControlsRef}>
|
||||
{renderNewNoteButtons()}
|
||||
<RowContainer>
|
||||
<BottomRow ref={searchAndSortRef} className="search-and-sort">
|
||||
<SearchBar inputRef={searchBarRef}/>
|
||||
{showsSortOrderButtons() &&
|
||||
<SortOrderButtonsContainer>
|
||||
@@ -175,7 +294,7 @@ function NoteListControls(props: Props) {
|
||||
/>
|
||||
</SortOrderButtonsContainer>
|
||||
}
|
||||
</RowContainer>
|
||||
</BottomRow>
|
||||
</StyledRoot>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import NoteList from '../NoteList/NoteList';
|
||||
import NoteListControls from '../NoteListControls/NoteListControls';
|
||||
import { Size } from '../ResizableLayout/utils/types';
|
||||
@@ -22,7 +22,15 @@ const StyledRoot = styled.div`
|
||||
|
||||
export default function NoteListWrapper(props: Props) {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const controlHeight = theme.topRowHeight;
|
||||
const [controlHeight, setControlHeight] = useState(theme.topRowHeight);
|
||||
|
||||
const onContentHeightChange = (sameRow: boolean) => {
|
||||
if (sameRow) {
|
||||
setControlHeight(theme.topRowHeight);
|
||||
} else {
|
||||
setControlHeight(theme.topRowHeight * 2);
|
||||
}
|
||||
};
|
||||
|
||||
const noteListSize = useMemo(() => {
|
||||
return {
|
||||
@@ -33,7 +41,7 @@ export default function NoteListWrapper(props: Props) {
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<NoteListControls height={controlHeight}/>
|
||||
<NoteListControls height={controlHeight} width={noteListSize.width} onContentHeightChange={onContentHeightChange}/>
|
||||
<NoteList resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
|
||||
</StyledRoot>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,8 @@ import { ContextMenuItemType, ContextMenuOptions } from './NoteEditor/utils/cont
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import styled from 'styled-components';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = new Entities().encode;
|
||||
|
||||
const Window = styled.div`
|
||||
height: 100%;
|
||||
@@ -19,12 +21,6 @@ const Window = styled.div`
|
||||
color: ${(props: any) => props.theme.color};
|
||||
`;
|
||||
|
||||
const IFrame = styled.iframe`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Function;
|
||||
@@ -93,13 +89,12 @@ export default function PdfViewer(props: Props) {
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const escapedResourcePath = htmlentities(Resource.fullPath(props.resource));
|
||||
const escapedMime = htmlentities('application/pdf');
|
||||
|
||||
return (
|
||||
<Window theme={theme}>
|
||||
<IFrame src="./vendor/lib/@joplin/pdf-viewer/index.html" x-url={Resource.fullPath(props.resource)}
|
||||
x-appearance={theme.appearance} ref={iframeRef}
|
||||
x-title={props.resource.title}
|
||||
x-anchorpage={props.pageNo}
|
||||
x-type="full"></IFrame>
|
||||
<object data={escapedResourcePath} className="media-player media-pdf" type={escapedMime}></object>;
|
||||
</Window>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ describe('movements', () => {
|
||||
expect(canMove(MoveDirection.Right, findItemByKey(layout, 'col2'), findItemByKey(layout, 'root'))).toBe(false);
|
||||
});
|
||||
|
||||
test('Container with only one child should take the width of its parent', () => {
|
||||
test('container with only one child should take the width of its parent', () => {
|
||||
let layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 100,
|
||||
@@ -170,7 +170,7 @@ describe('movements', () => {
|
||||
expect(layout.children[0].children[0].width).toBe(undefined);
|
||||
});
|
||||
|
||||
test('Temp container should take the width of the child it replaces', () => {
|
||||
test('temp container should take the width of the child it replaces', () => {
|
||||
let layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 100,
|
||||
@@ -198,7 +198,7 @@ describe('movements', () => {
|
||||
expect(layout.children[0].children[1].width).toBe(undefined);
|
||||
});
|
||||
|
||||
test('Last child should have flexible width if all siblings have fixed width', () => {
|
||||
test('last child should have flexible width if all siblings have fixed width', () => {
|
||||
let layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 100,
|
||||
|
||||
@@ -260,10 +260,6 @@ describe('useLayoutItemSizes', () => {
|
||||
expect(sizes.col4.width).toBe(50);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('calculateMaxSizeAvailableForItem', () => {
|
||||
|
||||
test('should give maximum available space this item can take up during resizing', () => {
|
||||
const layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { StyledRoot, StyledAddButton, StyledShareIcon, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
|
||||
import { ButtonLevel } from '../Button/Button';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -38,6 +40,24 @@ const { clipboard } = require('electron');
|
||||
|
||||
const logger = Logger.create('Sidebar');
|
||||
|
||||
const StyledFoldersHolder = styled.div`
|
||||
// linux bug: https://github.com/laurent22/joplin/issues/7506#issuecomment-1447101057
|
||||
& a.list-item {
|
||||
${shim.isLinux() && {
|
||||
opacity: 1,
|
||||
}}
|
||||
}
|
||||
`;
|
||||
const TagsHolder = styled.div`
|
||||
// linux bug: https://github.com/laurent22/joplin/issues/8000
|
||||
// solution ref: https://github.com/laurent22/joplin/issues/7506#issuecomment-1447101057
|
||||
& a.list-item {
|
||||
${shim.isLinux() && {
|
||||
opacity: 1,
|
||||
}}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Function;
|
||||
@@ -705,13 +725,13 @@ const SidebarComponent = (props: Props) => {
|
||||
const folderItems = [renderAllNotesItem(theme, allNotesSelected)].concat(result.items);
|
||||
folderItemsOrder_.current = result.order;
|
||||
items.push(
|
||||
<div
|
||||
<StyledFoldersHolder
|
||||
className={`folders ${props.folderHeaderIsExpanded ? 'expanded' : ''}`}
|
||||
key="folder_items"
|
||||
style={foldersStyle}
|
||||
>
|
||||
{folderItems}
|
||||
</div>
|
||||
</StyledFoldersHolder>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -727,9 +747,9 @@ const SidebarComponent = (props: Props) => {
|
||||
tagItemsOrder_.current = result.order;
|
||||
|
||||
items.push(
|
||||
<div className="tags" key="tag_items" style={{ display: props.tagHeaderIsExpanded ? 'block' : 'none' }}>
|
||||
<TagsHolder className="tags" key="tag_items" style={{ display: props.tagHeaderIsExpanded ? 'block' : 'none' }}>
|
||||
{tagItems}
|
||||
</div>
|
||||
</TagsHolder>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,19 @@ if (typeof module !== 'undefined') {
|
||||
|
||||
const markJsUtils = {};
|
||||
|
||||
const isInsideContainer = (node, tagName) => {
|
||||
if (!node) return false;
|
||||
|
||||
tagName = tagName.toLowerCase();
|
||||
|
||||
while (node) {
|
||||
if (node.tagName && node.tagName.toLowerCase() === tagName) return true;
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
|
||||
if (typeof keyword === 'string') {
|
||||
keyword = {
|
||||
@@ -71,12 +84,13 @@ markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
|
||||
if (isBasicSearch) accuracy = 'partially';
|
||||
if (keyword.type === 'regex') {
|
||||
accuracy = 'complementary';
|
||||
// Remove the trailing wildcard and "accuracy = complementary" will take care of
|
||||
// highlighting the relevant keywords.
|
||||
// Remove the trailing wildcard and "accuracy = complementary" will take
|
||||
// care of highlighting the relevant keywords.
|
||||
|
||||
// Known bug: it will also highlight word that contain the term as a suffix for example for "ent*", it will highlight "present"
|
||||
// which is incorrect (it should only highlight what starts with "ent") but for now will do. Mark.js doesn't have an option
|
||||
// to tweak this behaviour.
|
||||
// Known bug: it will also highlight word that contain the term as a
|
||||
// suffix for example for "ent*", it will highlight "present" which is
|
||||
// incorrect (it should only highlight what starts with "ent") but for
|
||||
// now will do. Mark.js doesn't have an option to tweak this behaviour.
|
||||
value = keyword.value.substr(0, keyword.value.length - 1);
|
||||
}
|
||||
|
||||
@@ -86,6 +100,18 @@ markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
|
||||
{},
|
||||
{
|
||||
accuracy: accuracy,
|
||||
filter: (node, _term, _totalCounter, _counter) => {
|
||||
// We exclude SVG because it creates a "<mark>" tag inside
|
||||
// the document, which is not a valid SVG tag. As a result
|
||||
// the content within that tag disappears.
|
||||
//
|
||||
// mark.js has an "exclude" parameter, but it doesn't work
|
||||
// so we use "filter" instead.
|
||||
//
|
||||
// https://github.com/joplin/plugin-abc-sheet-music
|
||||
if (isInsideContainer(node, 'SVG')) return false;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
extraOptions
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.10.8",
|
||||
"version": "2.11.1",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -27,6 +27,7 @@
|
||||
},
|
||||
"build": {
|
||||
"appId": "net.cozic.joplin-desktop",
|
||||
"compression": "maximum",
|
||||
"productName": "Joplin",
|
||||
"npmRebuild": false,
|
||||
"afterSign": "./tools/notarizeMacApp.js",
|
||||
@@ -107,7 +108,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~2.10",
|
||||
"@joplin/tools": "~2.11",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.2.6",
|
||||
"@types/node": "18.11.18",
|
||||
@@ -136,9 +137,8 @@
|
||||
"@electron/remote": "2.0.9",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@joeattardi/emoji-button": "4.6.4",
|
||||
"@joplin/lib": "~2.10",
|
||||
"@joplin/pdf-viewer": "~2.10",
|
||||
"@joplin/renderer": "~2.10",
|
||||
"@joplin/lib": "~2.11",
|
||||
"@joplin/renderer": "~2.11",
|
||||
"async-mutex": "0.4.0",
|
||||
"codemirror": "5.65.9",
|
||||
"color": "3.2.1",
|
||||
@@ -147,8 +147,9 @@
|
||||
"debounce": "1.2.1",
|
||||
"electron-window-state": "5.0.3",
|
||||
"formatcoords": "1.1.3",
|
||||
"fs-extra": "11.1.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"highlight.js": "11.7.0",
|
||||
"html-entities": "1.4.0",
|
||||
"immer": "7.0.15",
|
||||
"keytar": "7.9.0",
|
||||
"mark.js": "8.11.1",
|
||||
@@ -163,15 +164,15 @@
|
||||
"react-datetime": "3.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-redux": "8.0.5",
|
||||
"react-select": "5.7.0",
|
||||
"react-select": "5.7.2",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
"redux": "4.2.1",
|
||||
"reselect": "4.1.7",
|
||||
"roboto-fontface": "0.10.0",
|
||||
"smalltalk": "2.5.1",
|
||||
"sqlite3": "5.1.4",
|
||||
"styled-components": "5.3.6",
|
||||
"sqlite3": "5.1.6",
|
||||
"styled-components": "5.3.9",
|
||||
"styled-system": "5.1.5",
|
||||
"taboverride": "4.0.3",
|
||||
"tinymce": "5.10.6"
|
||||
|
||||
@@ -6,15 +6,15 @@ const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
const folderId1 = 'aa012345678901234567890123456789';
|
||||
const folderId2 = 'bb012345678901234567890123456789';
|
||||
|
||||
beforeAll(async () => {
|
||||
shimInit();
|
||||
Setting.autoSaveEnabled = false;
|
||||
PerFolderSortOrderService.initialize();
|
||||
Setting.setValue('notes.perFolderSortOrderEnabled', true);
|
||||
});
|
||||
|
||||
describe('PerFolderSortOrderService', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
shimInit();
|
||||
Setting.autoSaveEnabled = false;
|
||||
PerFolderSortOrderService.initialize();
|
||||
Setting.setValue('notes.perFolderSortOrderEnabled', true);
|
||||
});
|
||||
|
||||
test('get(), isSet() and set()', async () => {
|
||||
// Clear all per-folder sort order
|
||||
expect(PerFolderSortOrderService.isSet(folderId1)).toBe(false);
|
||||
|
||||
@@ -2,13 +2,13 @@ import { notesSortOrderFieldArray, notesSortOrderNextField, setNotesSortOrder }
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
|
||||
beforeAll(() => {
|
||||
shimInit();
|
||||
Setting.autoSaveEnabled = false;
|
||||
});
|
||||
|
||||
describe('notesSortOrderUtils', () => {
|
||||
|
||||
beforeAll(() => {
|
||||
shimInit();
|
||||
Setting.autoSaveEnabled = false;
|
||||
});
|
||||
|
||||
it('should always provide the same ordered fields', async () => {
|
||||
const expected = ['user_updated_time', 'user_created_time', 'title', 'order'];
|
||||
expect(notesSortOrderFieldArray()).toStrictEqual(expected);
|
||||
|
||||
@@ -72,10 +72,6 @@ async function main() {
|
||||
src: langSourceDir,
|
||||
dest: `${buildLibDir}/tinymce/langs`,
|
||||
},
|
||||
{
|
||||
src: resolve(__dirname, '../../pdf-viewer/dist'),
|
||||
dest: `${buildLibDir}/@joplin/pdf-viewer`,
|
||||
},
|
||||
];
|
||||
|
||||
const files = [
|
||||
@@ -93,10 +89,6 @@ async function main() {
|
||||
src: resolve(__dirname, '../../lib/services/plugins/sandboxProxy.js'),
|
||||
dest: `${buildLibDir}/@joplin/lib/services/plugins/sandboxProxy.js`,
|
||||
},
|
||||
{
|
||||
src: resolve(__dirname, '../../pdf-viewer/index.html'),
|
||||
dest: `${buildLibDir}/@joplin/pdf-viewer/index.html`,
|
||||
},
|
||||
];
|
||||
|
||||
// First we delete all the destination directories, then we copy the files.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
bracketSpacing: false,
|
||||
jsxBracketSameLine: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
||||
@@ -150,8 +150,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097684
|
||||
versionName "2.10.8"
|
||||
versionCode 2097687
|
||||
versionName "2.11.2"
|
||||
// ndk {
|
||||
// abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
// }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
|
||||
import { EditorSettings } from '../types';
|
||||
import { initCodeMirror } from './CodeMirror';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
@@ -9,6 +10,8 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import { forceParsing } from '@codemirror/language';
|
||||
import loadLangauges from './testUtil/loadLanguages';
|
||||
|
||||
import { expect, describe, it } from '@jest/globals';
|
||||
|
||||
|
||||
const createEditorSettings = (themeId: number) => {
|
||||
const themeData = themeStyle(themeId);
|
||||
@@ -23,6 +26,14 @@ const createEditorSettings = (themeId: number) => {
|
||||
};
|
||||
|
||||
describe('CodeMirror', () => {
|
||||
// This checks for a regression -- occasionally, when updating packages,
|
||||
// syntax highlighting in the CodeMirror editor stops working. This is usually
|
||||
// fixed by
|
||||
// 1. removing all `@codemirror/` and `@lezer/` dependencies from yarn.lock,
|
||||
// 2. upgrading all CodeMirror packages to the latest versions in package.json, and
|
||||
// 3. re-running `yarn install`.
|
||||
//
|
||||
// See https://github.com/laurent22/joplin/issues/7253
|
||||
it('should give headings a different style', async () => {
|
||||
const headerLineText = '# Testing...';
|
||||
const initialText = `${headerLineText}\nThis is a test.`;
|
||||
|
||||
@@ -239,17 +239,5 @@ describe('markdownCommands', () => {
|
||||
expect(sel.from).toBe('> Testing...> \n> \n'.length);
|
||||
expect(sel.to).toBe(editor.state.doc.length);
|
||||
});
|
||||
|
||||
it('toggling inline code should both create and navigate out of an inline code region', async () => {
|
||||
const initialDocText = 'Testing...\n\n';
|
||||
const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
|
||||
toggleCode(editor);
|
||||
editor.dispatch(editor.state.replaceSelection('f(x) = ...'));
|
||||
toggleCode(editor);
|
||||
|
||||
editor.dispatch(editor.state.replaceSelection(' is a function.'));
|
||||
expect(editor.state.doc.toString()).toBe('Testing...\n\n`f(x) = ...` is a function.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ interface Props {
|
||||
initialSelection?: Selection;
|
||||
style: ViewStyle;
|
||||
contentStyle?: ViewStyle;
|
||||
toolbarEnabled: boolean;
|
||||
|
||||
onChange: ChangeEventHandler;
|
||||
onSelectionChange: SelectionChangeEventHandler;
|
||||
@@ -364,6 +365,19 @@ function NoteEditor(props: Props, ref: any) {
|
||||
console.error('NoteEditor: webview error');
|
||||
}, []);
|
||||
|
||||
const toolbar = <MarkdownToolbar
|
||||
style={{
|
||||
// Don't show the markdown toolbar if there isn't enough space
|
||||
// for it:
|
||||
flexShrink: 1,
|
||||
}}
|
||||
editorSettings={editorSettings}
|
||||
editorControl={editorControl}
|
||||
selectionState={selectionState}
|
||||
searchState={searchState}
|
||||
onAttach={props.onAttach}
|
||||
/>;
|
||||
|
||||
// - `scrollEnabled` prevents iOS from scrolling the document (has no effect on Android)
|
||||
// when an editable region (e.g. a the full-screen NoteEditor) is focused.
|
||||
return (
|
||||
@@ -401,18 +415,7 @@ function NoteEditor(props: Props, ref: any) {
|
||||
searchState={searchState}
|
||||
/>
|
||||
|
||||
<MarkdownToolbar
|
||||
style={{
|
||||
// Don't show the markdown toolbar if there isn't enough space
|
||||
// for it:
|
||||
flexShrink: 1,
|
||||
}}
|
||||
editorSettings={editorSettings}
|
||||
editorControl={editorControl}
|
||||
selectionState={selectionState}
|
||||
searchState={searchState}
|
||||
onAttach={props.onAttach}
|
||||
/>
|
||||
{props.toolbarEnabled ? toolbar : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { FlatList, Text, StyleSheet, Button, View } = require('react-native');
|
||||
|
||||
import { Component } from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { FlatList, Text, StyleSheet, Button, View } from 'react-native';
|
||||
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { AppState } from '../utils/types';
|
||||
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { NoteItem } = require('./note-item.js');
|
||||
const time = require('@joplin/lib/time').default;
|
||||
const { themeStyle } = require('./global-style.js');
|
||||
|
||||
class NoteListComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
interface NoteListProps {
|
||||
themeId: string;
|
||||
dispatch: (action: any)=> void;
|
||||
notesSource: string;
|
||||
items: NoteEntity[];
|
||||
folders: FolderEntity[];
|
||||
noteSelectionEnabled?: boolean;
|
||||
selectedFolderId?: string;
|
||||
}
|
||||
|
||||
class NoteListComponent extends Component<NoteListProps> {
|
||||
private rootRef_: FlatList;
|
||||
private styles_: Record<string, StyleSheet.NamedStyles<any>>;
|
||||
|
||||
public constructor(props: NoteListProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
items: [],
|
||||
@@ -21,7 +38,7 @@ class NoteListComponent extends Component {
|
||||
this.createNotebookButton_click = this.createNotebookButton_click.bind(this);
|
||||
}
|
||||
|
||||
styles() {
|
||||
private styles() {
|
||||
const themeId = this.props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
@@ -47,7 +64,7 @@ class NoteListComponent extends Component {
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
createNotebookButton_click() {
|
||||
private createNotebookButton_click() {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Folder',
|
||||
@@ -55,34 +72,14 @@ class NoteListComponent extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
filterNotes(notes) {
|
||||
const todoFilter = 'all'; // Setting.value('todoFilter');
|
||||
if (todoFilter === 'all') return notes;
|
||||
|
||||
const now = time.unixMs();
|
||||
const maxInterval = 1000 * 60 * 60 * 24;
|
||||
const notRecentTime = now - maxInterval;
|
||||
|
||||
const output = [];
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
const note = notes[i];
|
||||
if (note.is_todo) {
|
||||
if (todoFilter === 'recent' && note.user_updated_time < notRecentTime && !!note.todo_completed) continue;
|
||||
if (todoFilter === 'nonCompleted' && !!note.todo_completed) continue;
|
||||
}
|
||||
output.push(note);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
public UNSAFE_componentWillReceiveProps(newProps: NoteListProps) {
|
||||
// Make sure scroll position is reset when switching from one folder to another or to a tag list.
|
||||
if (this.rootRef_ && newProps.notesSource !== this.props.notesSource) {
|
||||
this.rootRef_.scrollToOffset({ offset: 0, animated: false });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
// `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39
|
||||
|
||||
if (this.props.items.length) {
|
||||
@@ -109,7 +106,7 @@ class NoteListComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const NoteList = connect(state => {
|
||||
const NoteList = connect((state: AppState) => {
|
||||
return {
|
||||
items: state.notes,
|
||||
folders: state.folders,
|
||||
@@ -119,4 +116,4 @@ const NoteList = connect(state => {
|
||||
};
|
||||
})(NoteListComponent);
|
||||
|
||||
module.exports = { NoteList };
|
||||
export default NoteList;
|
||||
@@ -2,9 +2,12 @@ const React = require('react');
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { View, Dimensions, Alert, Button } from 'react-native';
|
||||
import FingerprintScanner from 'react-native-fingerprint-scanner';
|
||||
import { SensorInfo } from './sensorInfo';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import biometricAuthenticate from './biometricAuthenticate';
|
||||
|
||||
const logger = Logger.create('BiometricPopup');
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -13,24 +16,36 @@ interface Props {
|
||||
}
|
||||
|
||||
export default (props: Props) => {
|
||||
const [initialPromptDone, setInitialPromptDone] = useState(Setting.value('security.biometricsInitialPromptDone'));
|
||||
const [display, setDisplay] = useState(!!props.sensorInfo.supportedSensors && (props.sensorInfo.enabled || !initialPromptDone));
|
||||
// The initial prompt is there so that the user can choose to opt-in to
|
||||
// biometrics auth the first time the app is launched. However since it
|
||||
// doesn't work properly, we disable it. We only want the user to enable the
|
||||
// feature after they've read the description in the config screen.
|
||||
const [initialPromptDone, setInitialPromptDone] = useState(true); // useState(Setting.value('security.biometricsInitialPromptDone'));
|
||||
const [display, setDisplay] = useState(props.sensorInfo.enabled || !initialPromptDone);
|
||||
const [tryBiometricsCheck, setTryBiometricsCheck] = useState(initialPromptDone);
|
||||
|
||||
logger.info('Render start');
|
||||
logger.info('initialPromptDone', initialPromptDone);
|
||||
logger.info('display', display);
|
||||
logger.info('tryBiometricsCheck', tryBiometricsCheck);
|
||||
logger.info('props.sensorInfo', props.sensorInfo);
|
||||
|
||||
useEffect(() => {
|
||||
if (!display || !tryBiometricsCheck) return;
|
||||
|
||||
const biometricsCheck = async () => {
|
||||
logger.info('biometricsCheck: start');
|
||||
|
||||
try {
|
||||
await FingerprintScanner.authenticate({ description: _('Verify your identity') });
|
||||
setTryBiometricsCheck(false);
|
||||
await biometricAuthenticate();
|
||||
setDisplay(false);
|
||||
} catch (error) {
|
||||
Alert.alert(_('Could not verify your identify'), error.message);
|
||||
setTryBiometricsCheck(false);
|
||||
} finally {
|
||||
FingerprintScanner.release();
|
||||
Alert.alert(error.message);
|
||||
}
|
||||
|
||||
setTryBiometricsCheck(false);
|
||||
|
||||
logger.info('biometricsCheck: end');
|
||||
};
|
||||
|
||||
void biometricsCheck();
|
||||
@@ -41,6 +56,9 @@ export default (props: Props) => {
|
||||
if (!display) return;
|
||||
|
||||
const complete = (enableBiometrics: boolean) => {
|
||||
logger.info('complete: start');
|
||||
logger.info('complete: enableBiometrics:', enableBiometrics);
|
||||
|
||||
setInitialPromptDone(true);
|
||||
Setting.setValue('security.biometricsInitialPromptDone', true);
|
||||
Setting.setValue('security.biometricsEnabled', enableBiometrics);
|
||||
@@ -55,6 +73,8 @@ export default (props: Props) => {
|
||||
type: 'BIOMETRICS_DONE_SET',
|
||||
value: true,
|
||||
});
|
||||
|
||||
logger.info('complete: end');
|
||||
};
|
||||
|
||||
Alert.alert(
|
||||
@@ -73,7 +93,7 @@ export default (props: Props) => {
|
||||
},
|
||||
]
|
||||
);
|
||||
}, [initialPromptDone, props.sensorInfo.supportedSensors, display, props.dispatch]);
|
||||
}, [initialPromptDone, display, props.dispatch]);
|
||||
|
||||
const windowSize = useMemo(() => {
|
||||
return {
|
||||
@@ -83,12 +103,18 @@ export default (props: Props) => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
logger.info('effect 1: start');
|
||||
|
||||
if (!display) {
|
||||
logger.info('effect 1: display', display);
|
||||
|
||||
props.dispatch({
|
||||
type: 'BIOMETRICS_DONE_SET',
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('effect 1: end');
|
||||
}, [display, props.dispatch]);
|
||||
|
||||
const renderTryAgainButton = () => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import FingerprintScanner, { Errors } from 'react-native-fingerprint-scanner';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
const logger = Logger.create('biometricAuthenticate');
|
||||
|
||||
export default async () => {
|
||||
try {
|
||||
logger.info('Authenticate...');
|
||||
await FingerprintScanner.authenticate({ description: _('Verify your identity') });
|
||||
logger.info('Authenticate done');
|
||||
} catch (error) {
|
||||
const errorName = (error as Errors).name;
|
||||
|
||||
let errorMessage = error.message;
|
||||
if (errorName === 'FingerprintScannerNotEnrolled' || errorName === 'FingerprintScannerNotAvailable') {
|
||||
errorMessage = _('Biometric unlock is not setup on the device. Please set it up in order to unlock Joplin. If the device is on lockout, consider switching it off and on to reset biometrics scanning.');
|
||||
}
|
||||
|
||||
error.message = _('Could not verify your identify: %s', errorMessage);
|
||||
|
||||
logger.warn(error);
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
FingerprintScanner.release();
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import FingerprintScanner from 'react-native-fingerprint-scanner';
|
||||
const logger = Logger.create('sensorInfo');
|
||||
|
||||
export interface SensorInfo {
|
||||
enabled: boolean;
|
||||
@@ -8,11 +10,40 @@ export interface SensorInfo {
|
||||
}
|
||||
|
||||
export default async (): Promise<SensorInfo> => {
|
||||
// Early exit if the feature is disabled, so that we don't make any
|
||||
// FingerprintScanner scanner calls, since it seems they can fail and freeze
|
||||
// the app.
|
||||
|
||||
logger.info('Start');
|
||||
logger.info('security.biometricsEnabled', Setting.value('security.biometricsEnabled'));
|
||||
|
||||
if (!Setting.value('security.biometricsEnabled')) {
|
||||
return {
|
||||
enabled: false,
|
||||
sensorsHaveChanged: false,
|
||||
supportedSensors: '',
|
||||
};
|
||||
}
|
||||
|
||||
let hasChanged = false;
|
||||
let supportedSensors = '';
|
||||
|
||||
try {
|
||||
logger.info('Getting isSensorAvailable...');
|
||||
|
||||
// Note: If `isSensorAvailable()` doesn't return anything, it seems we
|
||||
// could assume that biometrics are not setup on the device, and thus we
|
||||
// can unlock the app. However that's not always correct - on some
|
||||
// devices (eg Galaxy S22), `isSensorAvailable()` will return nothing if
|
||||
// the device is on lockout - i.e. if the user gave the wrong
|
||||
// fingerprint multiple times.
|
||||
//
|
||||
// So we definitely can't unlock the app in that case, and it means
|
||||
// `isSensorAvailable()` is pretty much useless. Instead we ask for
|
||||
// fingerprint when the user turns on the feature and at that point we
|
||||
// know if the device supports biometrics or not.
|
||||
const result = await FingerprintScanner.isSensorAvailable();
|
||||
logger.info('isSensorAvailable result', result);
|
||||
supportedSensors = result;
|
||||
|
||||
if (result) {
|
||||
@@ -22,7 +53,7 @@ export default async (): Promise<SensorInfo> => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not check for biometrics sensor:', error);
|
||||
logger.warn('Could not check for biometrics sensor:', error);
|
||||
Setting.setValue('security.biometricsSupportedSensors', '');
|
||||
}
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@ const testCases: testCase[] = [
|
||||
|
||||
describe('getResponsiveValue', () => {
|
||||
|
||||
test('Should throw exception if value map is an empty object', () => {
|
||||
test('should throw exception if value map is an empty object', () => {
|
||||
const input = {};
|
||||
expect(() => getResponsiveValue(input)).toThrow('valueMap cannot be an empty object!');
|
||||
});
|
||||
|
||||
test('Should return correct values', () => {
|
||||
test('should return correct values', () => {
|
||||
const mockReturnValues = [
|
||||
{ width: 400 },
|
||||
{ width: 480 },
|
||||
|
||||
@@ -23,6 +23,7 @@ const { themeStyle } = require('../global-style.js');
|
||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
||||
import biometricAuthenticate from '../biometrics/biometricAuthenticate';
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent {
|
||||
public static navigationOptions(): any {
|
||||
@@ -463,7 +464,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
<Text key="label" style={this.styles().switchSettingText}>
|
||||
{label}
|
||||
</Text>
|
||||
<Switch key="control" style={this.styles().switchSettingControl} trackColor={{ false: theme.dividerColor }} value={value} onValueChange={(value: any) => updateSettingValue(key, value)} />
|
||||
<Switch key="control" style={this.styles().switchSettingControl} trackColor={{ false: theme.dividerColor }} value={value} onValueChange={(value: any) => void updateSettingValue(key, value)} />
|
||||
</View>
|
||||
{descriptionComp}
|
||||
</View>
|
||||
@@ -474,13 +475,39 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
return !hasDescription ? this.styles().settingContainer : this.styles().settingContainerNoBottomBorder;
|
||||
}
|
||||
|
||||
private async handleSetting(key: string, value: any): Promise<boolean> {
|
||||
// When the user tries to enable biometrics unlock, we ask for the
|
||||
// fingerprint or Face ID, and if it's correct we save immediately. If
|
||||
// it's not, we don't turn on the setting.
|
||||
if (key === 'security.biometricsEnabled' && !!value) {
|
||||
try {
|
||||
await biometricAuthenticate();
|
||||
shared.updateSettingValue(this, key, value);
|
||||
await this.saveButton_press();
|
||||
} catch (error) {
|
||||
shared.updateSettingValue(this, key, false);
|
||||
Alert.alert(error.message);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key === 'security.biometricsEnabled' && !value) {
|
||||
shared.updateSettingValue(this, key, value);
|
||||
await this.saveButton_press();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public settingToComponent(key: string, value: any) {
|
||||
const themeId = this.props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
const output: any = null;
|
||||
|
||||
const updateSettingValue = (key: string, value: any) => {
|
||||
return shared.updateSettingValue(this, key, value);
|
||||
const updateSettingValue = async (key: string, value: any) => {
|
||||
const handled = await this.handleSetting(key, value);
|
||||
if (!handled) shared.updateSettingValue(this, key, value);
|
||||
};
|
||||
|
||||
const md = Setting.settingMetadata(key);
|
||||
@@ -517,7 +544,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
onValueChange={(itemValue: string) => {
|
||||
updateSettingValue(key, itemValue);
|
||||
void updateSettingValue(key, itemValue);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -553,7 +580,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
</Text>
|
||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', flex: 1 }}>
|
||||
<Text style={this.styles().sliderUnits}>{unitLabel}</Text>
|
||||
<Slider key="control" style={{ flex: 1 }} step={md.step} minimumValue={minimum} maximumValue={maximum} value={value} onValueChange={value => updateSettingValue(key, value)} />
|
||||
<Slider key="control" style={{ flex: 1 }} step={md.step} minimumValue={minimum} maximumValue={maximum} value={value} onValueChange={value => void updateSettingValue(key, value)} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -577,7 +604,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
<Text key="label" style={this.styles().settingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<TextInput autoCorrect={false} autoComplete="off" selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} autoCapitalize="none" key="control" style={this.styles().settingControl} value={value} onChangeText={(value: any) => updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
|
||||
<TextInput autoCorrect={false} autoComplete="off" selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} autoCapitalize="none" key="control" style={this.styles().settingControl} value={value} onChangeText={(value: any) => void updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -1118,6 +1118,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
bodyComponent = <NoteEditor
|
||||
ref={this.editorRef}
|
||||
toolbarEnabled={this.props.toolbarEnabled}
|
||||
themeId={this.props.themeId}
|
||||
initialText={note.body}
|
||||
initialSelection={this.selection}
|
||||
@@ -1231,6 +1232,7 @@ const NoteScreen = connect((state: any) => {
|
||||
themeId: state.settings.theme,
|
||||
editorFont: [state.settings['style.editor.fontFamily']],
|
||||
editorFontSize: state.settings['style.editor.fontSize'],
|
||||
toolbarEnabled: state.settings['editor.mobile.toolbarEnabled'],
|
||||
ftsEnabled: state.settings['db.ftsEnabled'],
|
||||
sharedData: state.sharedData,
|
||||
showSideMenu: state.showSideMenu,
|
||||
|
||||
@@ -2,7 +2,7 @@ const React = require('react');
|
||||
import { AppState as RNAppState, View, StyleSheet, NativeEventSubscription } from 'react-native';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
const { NoteList } = require('../note-list.js');
|
||||
import NoteList from '../NoteList';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
@@ -254,7 +254,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
<ScreenHeader title={iconString + title} showBackButton={false} parentComponent={thisComp} sortButton_press={this.sortButton_press} folderPickerOptions={this.folderPickerOptions()} showSearchButton={true} showSideMenuButton={true} />
|
||||
<NoteList style={this.styles().noteList} />
|
||||
<NoteList />
|
||||
{actionButtonComp}
|
||||
<DialogBox
|
||||
ref={(dialogbox: any) => {
|
||||
|
||||
@@ -523,7 +523,7 @@
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.10.5;
|
||||
MARKETING_VERSION = 12.11.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -551,7 +551,7 @@
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.10.5;
|
||||
MARKETING_VERSION = 12.11.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -705,7 +705,7 @@
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.10.5;
|
||||
MARKETING_VERSION = 12.11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
@@ -736,7 +736,7 @@
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.10.5;
|
||||
MARKETING_VERSION = 12.11.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -306,7 +306,7 @@ PODS:
|
||||
- React-jsinspector (0.70.6)
|
||||
- React-logger (0.70.6):
|
||||
- glog
|
||||
- react-native-alarm-notification (2.10.0):
|
||||
- react-native-alarm-notification (2.11.0):
|
||||
- React
|
||||
- react-native-camera (4.2.1):
|
||||
- React-Core
|
||||
@@ -316,7 +316,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-camera/RN (4.2.1):
|
||||
- React-Core
|
||||
- react-native-document-picker (8.1.3):
|
||||
- react-native-document-picker (8.1.4):
|
||||
- React-Core
|
||||
- react-native-fingerprint-scanner (6.0.0):
|
||||
- React
|
||||
@@ -324,15 +324,15 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-get-random-values (1.8.0):
|
||||
- React-Core
|
||||
- react-native-image-picker (5.0.2):
|
||||
- react-native-image-picker (5.3.1):
|
||||
- React-Core
|
||||
- react-native-image-resizer (1.4.5):
|
||||
- React-Core
|
||||
- react-native-netinfo (9.3.7):
|
||||
- react-native-netinfo (9.3.8):
|
||||
- React-Core
|
||||
- react-native-rsa-native (2.0.5):
|
||||
- React
|
||||
- react-native-saf-x (2.10.2):
|
||||
- react-native-saf-x (2.11.0):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.5.0):
|
||||
- RCT-Folly
|
||||
@@ -432,7 +432,7 @@ PODS:
|
||||
- React
|
||||
- RNSecureRandom (1.0.1):
|
||||
- React
|
||||
- RNShare (8.2.0):
|
||||
- RNShare (8.2.1):
|
||||
- React-Core
|
||||
- RNVectorIcons (9.2.0):
|
||||
- React-Core
|
||||
@@ -714,17 +714,17 @@ SPEC CHECKSUMS:
|
||||
React-jsiexecutor: b4a65947391c658450151275aa406f2b8263178f
|
||||
React-jsinspector: 60769e5a0a6d4b32294a2456077f59d0266f9a8b
|
||||
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
|
||||
react-native-alarm-notification: 0f58eaa37a4188480536fd7ab62db9b1dfba392f
|
||||
react-native-alarm-notification: 26527410a6162d07a9dc57f4bbc62e94ff48e65d
|
||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||
react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c
|
||||
react-native-document-picker: a9bd26996d1b2e4f412dd186041714c79af381d0
|
||||
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||
react-native-geolocation: 69f4fd37650b8e7fee91816d395e62dd16f5ab8d
|
||||
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
|
||||
react-native-image-picker: a5dddebb4d2955ac4712a4ed66b00a85f62a63ac
|
||||
react-native-image-picker: ec9b713e248760bfa0f879f0715391de4651a7cb
|
||||
react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f
|
||||
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
|
||||
react-native-netinfo: fbc23bc2fe217155d85f2f7e0644b1654df8029b
|
||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||
react-native-saf-x: db5a33862e7aec0f9f2d4cccfe7264b09b234e2e
|
||||
react-native-saf-x: 9bd5238d3b43d76bbec64aa82c173ac20a4bce9f
|
||||
react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc
|
||||
react-native-slider: 33b8d190b59d4f67a541061bb91775d53d617d9d
|
||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||
@@ -751,7 +751,7 @@ SPEC CHECKSUMS:
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
||||
RNShare: b089c33619bbfb0a32bc4069c858b9274e694187
|
||||
RNShare: eaee3dd5a06dad397c7d3b14762007035c5de405
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
|
||||
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@joplin/app-mobile",
|
||||
"description": "Joplin for Mobile",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "2.10.0",
|
||||
"version": "2.11.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "react-native start --reset-cache",
|
||||
@@ -18,14 +18,14 @@
|
||||
"postinstall": "jetify && yarn run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "~2.10",
|
||||
"@joplin/react-native-alarm-notification": "~2.10",
|
||||
"@joplin/react-native-saf-x": "~2.10",
|
||||
"@joplin/renderer": "~2.10",
|
||||
"@joplin/lib": "~2.11",
|
||||
"@joplin/react-native-alarm-notification": "~2.11",
|
||||
"@joplin/react-native-saf-x": "~2.11",
|
||||
"@joplin/renderer": "~2.11",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/datetimepicker": "6.7.5",
|
||||
"@react-native-community/geolocation": "2.1.0",
|
||||
"@react-native-community/netinfo": "9.3.7",
|
||||
"@react-native-community/netinfo": "9.3.8",
|
||||
"@react-native-community/push-notification-ios": "1.10.1",
|
||||
"@react-native-community/slider": "4.4.2",
|
||||
"assert-browserify": "2.0.0",
|
||||
@@ -44,23 +44,23 @@
|
||||
"react-native-action-button": "2.8.5",
|
||||
"react-native-camera": "4.2.1",
|
||||
"react-native-dialogbox": "0.6.10",
|
||||
"react-native-document-picker": "8.1.3",
|
||||
"react-native-document-picker": "8.1.4",
|
||||
"react-native-dropdownalert": "4.5.1",
|
||||
"react-native-exit-app": "1.1.0",
|
||||
"react-native-file-viewer": "2.1.5",
|
||||
"react-native-fingerprint-scanner": "6.0.0",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-get-random-values": "1.8.0",
|
||||
"react-native-image-picker": "5.0.2",
|
||||
"react-native-image-picker": "5.3.1",
|
||||
"react-native-image-resizer": "1.4.5",
|
||||
"react-native-modal-datetime-picker": "14.0.1",
|
||||
"react-native-paper": "5.2.0",
|
||||
"react-native-paper": "5.5.2",
|
||||
"react-native-popup-menu": "0.16.1",
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "4.5.0",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "8.2.0",
|
||||
"react-native-share": "8.2.1",
|
||||
"react-native-side-menu-updated": "1.3.2",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
@@ -79,36 +79,36 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.16.0",
|
||||
"@babel/runtime": "7.16.3",
|
||||
"@codemirror/commands": "6.1.2",
|
||||
"@codemirror/commands": "6.2.2",
|
||||
"@codemirror/lang-cpp": "6.0.2",
|
||||
"@codemirror/lang-html": "6.4.0",
|
||||
"@codemirror/lang-html": "6.4.3",
|
||||
"@codemirror/lang-java": "6.0.1",
|
||||
"@codemirror/lang-javascript": "6.1.1",
|
||||
"@codemirror/lang-markdown": "6.0.5",
|
||||
"@codemirror/lang-javascript": "6.1.5",
|
||||
"@codemirror/lang-markdown": "6.1.0",
|
||||
"@codemirror/lang-php": "6.0.1",
|
||||
"@codemirror/lang-rust": "6.0.1",
|
||||
"@codemirror/language": "6.3.2",
|
||||
"@codemirror/legacy-modes": "6.3.1",
|
||||
"@codemirror/search": "6.2.3",
|
||||
"@codemirror/state": "6.1.4",
|
||||
"@codemirror/view": "6.7.1",
|
||||
"@joplin/tools": "~2.10",
|
||||
"@lezer/highlight": "1.1.3",
|
||||
"@codemirror/language": "6.6.0",
|
||||
"@codemirror/legacy-modes": "6.3.2",
|
||||
"@codemirror/search": "6.3.0",
|
||||
"@codemirror/state": "6.2.0",
|
||||
"@codemirror/view": "6.9.3",
|
||||
"@joplin/tools": "~2.11",
|
||||
"@lezer/highlight": "1.1.4",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/jest": "29.2.6",
|
||||
"@types/react-native": "0.70.6",
|
||||
"@types/react-redux": "7.1.25",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"execa": "4.1.0",
|
||||
"fs-extra": "11.1.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.4.3",
|
||||
"jest-environment-jsdom": "29.4.3",
|
||||
"jetifier": "2.0.0",
|
||||
"jsdom": "21.0.0",
|
||||
"jsdom": "21.1.1",
|
||||
"md5-file": "5.0.0",
|
||||
"metro-react-native-babel-preset": "0.72.3",
|
||||
"nodemon": "2.0.20",
|
||||
"nodemon": "2.0.22",
|
||||
"ts-jest": "29.0.5",
|
||||
"ts-loader": "9.4.2",
|
||||
"ts-node": "10.9.1",
|
||||
|
||||
@@ -501,7 +501,7 @@ async function initialize(dispatch: Function) {
|
||||
if (Setting.value('env') === 'prod') {
|
||||
await db.open({ name: getDatabaseName(currentProfile, isSubProfile) });
|
||||
} else {
|
||||
await db.open({ name: getDatabaseName(currentProfile, isSubProfile) });
|
||||
await db.open({ name: getDatabaseName(currentProfile, isSubProfile, '-3') });
|
||||
|
||||
// await db.clearForTesting();
|
||||
}
|
||||
@@ -984,6 +984,10 @@ class AppComponent extends React.Component {
|
||||
const biometricIsEnabled = !!this.state.sensorInfo && this.state.sensorInfo.enabled;
|
||||
const shouldShowMainContent = !biometricIsEnabled || this.props.biometricsDone;
|
||||
|
||||
logger.info('root.biometrics: biometricIsEnabled', biometricIsEnabled);
|
||||
logger.info('root.biometrics: shouldShowMainContent', shouldShowMainContent);
|
||||
logger.info('root.biometrics: this.state.sensorInfo', this.state.sensorInfo);
|
||||
|
||||
const mainContent = (
|
||||
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
|
||||
<SideMenu
|
||||
|
||||
@@ -24,9 +24,10 @@ export const getResourceDir = (profile: Profile, isSubProfile: boolean) => {
|
||||
return `${getProfilesRootDir()}/resources-${profile.id}`;
|
||||
};
|
||||
|
||||
export const getDatabaseName = (profile: Profile, isSubProfile: boolean) => {
|
||||
if (!isSubProfile) return 'joplin.sqlite';
|
||||
return `joplin-${profile.id}.sqlite`;
|
||||
// The suffix is for debugging only
|
||||
export const getDatabaseName = (profile: Profile, isSubProfile: boolean, suffix: string = '') => {
|
||||
if (!isSubProfile) return `joplin${suffix}.sqlite`;
|
||||
return `joplin-${profile.id}${suffix}.sqlite`;
|
||||
};
|
||||
|
||||
export const loadProfileConfig = async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"id": "<%= pluginId %>",
|
||||
"app_min_version": "2.10",
|
||||
"app_min_version": "2.11",
|
||||
"version": "1.0.0",
|
||||
"name": "<%= pluginName %>",
|
||||
"description": "<%= pluginDescription %>",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "generator-joplin",
|
||||
"version": "2.10.0",
|
||||
"version": "2.11.0",
|
||||
"description": "Scaffolds out a new Joplin plugin",
|
||||
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
|
||||
"author": {
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "2.4.2",
|
||||
"slugify": "1.6.5",
|
||||
"slugify": "1.6.6",
|
||||
"yeoman-generator": "5.8.0",
|
||||
"yosay": "2.0.2"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/htmlpack",
|
||||
"version": "2.10.2",
|
||||
"version": "2.11.0",
|
||||
"description": "Pack an HTML file and all its linked resources into a single HTML file",
|
||||
"main": "dist/index.js",
|
||||
"types": "src/index.ts",
|
||||
@@ -17,7 +17,7 @@
|
||||
"@joplin/fork-htmlparser2": "^4.1.43",
|
||||
"css": "3.0.0",
|
||||
"datauri": "4.1.0",
|
||||
"fs-extra": "11.1.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"html-entities": "1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -20,7 +20,7 @@ import Folder from './models/Folder';
|
||||
import BaseItem from './models/BaseItem';
|
||||
import Note from './models/Note';
|
||||
import Tag from './models/Tag';
|
||||
const { splitCommandString } = require('./string-utils.js');
|
||||
import { splitCommandString } from '@joplin/utils';
|
||||
import { reg } from './registry';
|
||||
import time from './time';
|
||||
import BaseSyncTarget from './BaseSyncTarget';
|
||||
@@ -192,6 +192,12 @@ export default class BaseApplication {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--safe-mode') {
|
||||
matched.isSafeMode = true;
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--open-dev-tools') {
|
||||
Setting.setConstant('flagOpenDevTools', true);
|
||||
argv.splice(0, 1);
|
||||
@@ -700,7 +706,7 @@ export default class BaseApplication {
|
||||
|
||||
flagContent = flagContent.trim();
|
||||
|
||||
let flags = splitCommandString(flagContent);
|
||||
let flags: any = splitCommandString(flagContent);
|
||||
flags.splice(0, 0, 'cmd');
|
||||
flags.splice(0, 0, 'node');
|
||||
|
||||
@@ -829,6 +835,10 @@ export default class BaseApplication {
|
||||
|
||||
appLogger.info(`Client ID: ${Setting.value('clientId')}`);
|
||||
|
||||
if (initArgs?.isSafeMode) {
|
||||
Setting.setValue('isSafeMode', true);
|
||||
}
|
||||
|
||||
if (Setting.value('firstStart')) {
|
||||
// If it's a sub-profile, the locale must come from the root
|
||||
// profile.
|
||||
|
||||
@@ -96,14 +96,20 @@ export default class SyncTargetOneDrive extends BaseSyncTarget {
|
||||
let context = Setting.value(`sync.${this.syncTargetId()}.context`);
|
||||
context = context === '' ? null : JSON.parse(context);
|
||||
let accountProperties = context ? context.accountProperties : null;
|
||||
const api = this.api();
|
||||
|
||||
if (!accountProperties) {
|
||||
accountProperties = await this.api_.execAccountPropertiesRequest();
|
||||
accountProperties = await api.execAccountPropertiesRequest();
|
||||
context ? context.accountProperties = accountProperties : context = { accountProperties: accountProperties };
|
||||
Setting.setValue(`sync.${this.syncTargetId()}.context`, JSON.stringify(context));
|
||||
}
|
||||
this.api_.setAccountProperties(accountProperties);
|
||||
api.setAccountProperties(accountProperties);
|
||||
const appDir = await this.api().appDirectory();
|
||||
const fileApi = new FileApi(appDir, new FileApiDriverOneDrive(this.api()));
|
||||
// the appDir might contain non-ASCII characters
|
||||
// /[^\u0021-\u00ff]/ is used in Node.js to detect the unescaped characters.
|
||||
// See https://github.com/nodejs/node/blob/bbbf97b6dae63697371082475dc8651a6a220336/lib/_http_client.js#L176
|
||||
const baseDir = RegExp(/[^\u0021-\u00ff]/).exec(appDir) !== null ? encodeURI(appDir) : appDir;
|
||||
const fileApi = new FileApi(baseDir, new FileApiDriverOneDrive(this.api()));
|
||||
fileApi.setSyncTargetId(this.syncTargetId());
|
||||
fileApi.setLogger(this.logger());
|
||||
return fileApi;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
const { setupDatabaseAndSynchronizer, switchClient, supportDir } = require('./testing/test-utils.js');
|
||||
const shim = require('./shim').default;
|
||||
const { enexXmlToHtml } = require('./import-enex-html-gen.js');
|
||||
@@ -50,6 +49,7 @@ const compareOutputToExpected = (options) => {
|
||||
const outputFile = fileWithPath(`${options.testName}.html`);
|
||||
const testTitle = `should convert from Enex to Html: ${options.testName}`;
|
||||
|
||||
// eslint-disable-next-line jest/require-top-level-describe
|
||||
it(testTitle, (async () => {
|
||||
const enexInput = await shim.fsDriver().readFile(inputFile);
|
||||
const expectedOutput = await shim.fsDriver().readFile(outputFile);
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
const markJsUtils = {};
|
||||
|
||||
const isInsideContainer = (node, tagName) => {
|
||||
if (!node) return false;
|
||||
|
||||
tagName = tagName.toLowerCase();
|
||||
|
||||
while (node) {
|
||||
if (node.tagName && node.tagName.toLowerCase() === tagName) return true;
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
|
||||
if (typeof keyword === 'string') {
|
||||
keyword = {
|
||||
@@ -15,12 +28,13 @@ markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
|
||||
if (isBasicSearch) accuracy = 'partially';
|
||||
if (keyword.type === 'regex') {
|
||||
accuracy = 'complementary';
|
||||
// Remove the trailing wildcard and "accuracy = complementary" will take care of
|
||||
// highlighting the relevant keywords.
|
||||
// Remove the trailing wildcard and "accuracy = complementary" will take
|
||||
// care of highlighting the relevant keywords.
|
||||
|
||||
// Known bug: it will also highlight word that contain the term as a suffix for example for "ent*", it will highlight "present"
|
||||
// which is incorrect (it should only highlight what starts with "ent") but for now will do. Mark.js doesn't have an option
|
||||
// to tweak this behaviour.
|
||||
// Known bug: it will also highlight word that contain the term as a
|
||||
// suffix for example for "ent*", it will highlight "present" which is
|
||||
// incorrect (it should only highlight what starts with "ent") but for
|
||||
// now will do. Mark.js doesn't have an option to tweak this behaviour.
|
||||
value = keyword.value.substr(0, keyword.value.length - 1);
|
||||
}
|
||||
|
||||
@@ -30,6 +44,18 @@ markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
|
||||
{},
|
||||
{
|
||||
accuracy: accuracy,
|
||||
filter: (node, _term, _totalCounter, _counter) => {
|
||||
// We exclude SVG because it creates a "<mark>" tag inside
|
||||
// the document, which is not a valid SVG tag. As a result
|
||||
// the content within that tag disappears.
|
||||
//
|
||||
// mark.js has an "exclude" parameter, but it doesn't work
|
||||
// so we use "filter" instead.
|
||||
//
|
||||
// https://github.com/joplin/plugin-abc-sheet-music
|
||||
if (isInsideContainer(node, 'SVG')) return false;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
extraOptions
|
||||
)
|
||||
|
||||
@@ -409,38 +409,36 @@ describe('models/Note', () => {
|
||||
expect(movedNote.conflict_original_id).toBe('');
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('models/Note_replacePaths', () => {
|
||||
|
||||
function testResourceReplacment(body: string, pathsToTry: string[], expected: string) {
|
||||
expect(Note['replaceResourceExternalToInternalLinks_'](pathsToTry, body)).toBe(expected);
|
||||
}
|
||||
test('Basic replacement', () => {
|
||||
|
||||
test('basic replacement', () => {
|
||||
const body = '';
|
||||
const pathsToTry = ['file:///C:Users/Username/resources'];
|
||||
const expected = '';
|
||||
testResourceReplacment(body, pathsToTry, expected);
|
||||
});
|
||||
|
||||
test('Replacement with spaces', () => {
|
||||
test('replacement with spaces', () => {
|
||||
const body = '';
|
||||
const pathsToTry = ['file:///C:Users/Username with spaces/resources'];
|
||||
const expected = '';
|
||||
testResourceReplacment(body, pathsToTry, expected);
|
||||
});
|
||||
|
||||
test('Replacement with Non-ASCII', () => {
|
||||
test('replacement with Non-ASCII', () => {
|
||||
const body = '';
|
||||
const pathsToTry = ['file:///C:Users/UsernameWithéàö/resources'];
|
||||
const expected = '';
|
||||
testResourceReplacment(body, pathsToTry, expected);
|
||||
});
|
||||
|
||||
test('Replacement with Non-ASCII and spaces', () => {
|
||||
test('replacement with Non-ASCII and spaces', () => {
|
||||
const body = '';
|
||||
const pathsToTry = ['file:///C:Users/Username With éàö/resources'];
|
||||
const expected = '';
|
||||
testResourceReplacment(body, pathsToTry, expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -874,16 +874,21 @@ export default class Note extends BaseItem {
|
||||
if (note.id === noteIds[0] && index === i) return defer();
|
||||
}
|
||||
|
||||
// If some of the target notes have order = 0, set the order field to user_created_time
|
||||
// (historically, all notes had the order field set to 0)
|
||||
// If some of the target notes have order = 0, set the order field to a reasonable
|
||||
// value to avoid moving the note. Using "smallest value / 2" (vs, for example,
|
||||
// subtracting a constant) ensures items remain in their current position at the
|
||||
// end, without ever reaching 0.
|
||||
let hasSetOrder = false;
|
||||
let previousOrder = 0;
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
const note = notes[i];
|
||||
if (!note.order) {
|
||||
const updatedNote = await this.updateNoteOrder_(note, note.user_created_time);
|
||||
const newOrder = previousOrder ? previousOrder / 2 : note.user_created_time;
|
||||
const updatedNote = await this.updateNoteOrder_(note, newOrder);
|
||||
notes[i] = updatedNote;
|
||||
hasSetOrder = true;
|
||||
}
|
||||
previousOrder = notes[i].order;
|
||||
}
|
||||
|
||||
if (hasSetOrder) notes = await getSortedNotes(folderId);
|
||||
|
||||
@@ -24,6 +24,9 @@ describe('models/Note_CustomSortOrder', () => {
|
||||
// (which normally is the creation timestamp). So if the user tries to move notes
|
||||
// in the middle of other notes with order = 0, the order of all these notes is
|
||||
// initialised at this point.
|
||||
// Also check that the order-0 notes stay in their previous position when their
|
||||
// order values are initialized. There was at one point a bug where they popped to
|
||||
// the top.
|
||||
|
||||
const folder1 = await Folder.save({});
|
||||
const folder2 = await Folder.save({});
|
||||
@@ -32,6 +35,7 @@ describe('models/Note_CustomSortOrder', () => {
|
||||
notes1.push(await Note.save({ order: 0, parent_id: folder1.id })); await time.msleep(2);
|
||||
notes1.push(await Note.save({ order: 0, parent_id: folder1.id })); await time.msleep(2);
|
||||
notes1.push(await Note.save({ order: 0, parent_id: folder1.id })); await time.msleep(2);
|
||||
notes1.push(await Note.save({ order: 1000, parent_id: folder1.id })); await time.msleep(2);
|
||||
|
||||
const notes2 = [];
|
||||
notes2.push(await Note.save({ parent_id: folder2.id })); await time.msleep(2);
|
||||
@@ -45,12 +49,13 @@ describe('models/Note_CustomSortOrder', () => {
|
||||
};
|
||||
}
|
||||
|
||||
await Note.insertNotesAt(folder1.id, notes2.map(n => n.id), 1);
|
||||
await Note.insertNotesAt(folder1.id, notes2.map(n => n.id), 2);
|
||||
|
||||
const newNotes1 = [
|
||||
await Note.load(notes1[0].id),
|
||||
await Note.load(notes1[1].id),
|
||||
await Note.load(notes1[2].id),
|
||||
await Note.load(notes1[3].id),
|
||||
];
|
||||
|
||||
// Check that timestamps haven't changed - moving a note should not change the user timestamps
|
||||
@@ -64,12 +69,13 @@ describe('models/Note_CustomSortOrder', () => {
|
||||
order: Note.customOrderByColumns(),
|
||||
});
|
||||
|
||||
expect(sortedNotes.length).toBe(5);
|
||||
expect(sortedNotes[0].id).toBe(notes1[2].id);
|
||||
expect(sortedNotes[1].id).toBe(notes2[0].id);
|
||||
expect(sortedNotes[2].id).toBe(notes2[1].id);
|
||||
expect(sortedNotes[3].id).toBe(notes1[1].id);
|
||||
expect(sortedNotes[4].id).toBe(notes1[0].id);
|
||||
expect(sortedNotes.length).toBe(6);
|
||||
expect(sortedNotes[0].id).toBe(notes1[3].id);
|
||||
expect(sortedNotes[1].id).toBe(notes1[2].id);
|
||||
expect(sortedNotes[2].id).toBe(notes2[0].id);
|
||||
expect(sortedNotes[3].id).toBe(notes2[1].id);
|
||||
expect(sortedNotes[4].id).toBe(notes1[1].id);
|
||||
expect(sortedNotes[5].id).toBe(notes1[0].id);
|
||||
}));
|
||||
|
||||
it('should bump system but not user updated time when changing sort value', (async () => {
|
||||
|
||||
@@ -1050,6 +1050,17 @@ class Setting extends BaseModel {
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
'editor.mobile.toolbarEnabled': {
|
||||
value: true,
|
||||
type: SettingItemType.Bool,
|
||||
section: 'note',
|
||||
public: true,
|
||||
appTypes: [AppType.Mobile],
|
||||
label: () => _('Enable the Markdown toolbar'),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
// Works around a bug in which additional space is visible beneath the toolbar on some devices.
|
||||
// See https://github.com/laurent22/joplin/pull/6823
|
||||
'editor.mobile.removeSpaceBelowToolbar': {
|
||||
@@ -1384,7 +1395,7 @@ class Setting extends BaseModel {
|
||||
useCustomPdfViewer: {
|
||||
value: false,
|
||||
type: SettingItemType.Bool,
|
||||
public: true,
|
||||
public: false,
|
||||
advanced: true,
|
||||
appTypes: [AppType.Desktop],
|
||||
label: () => 'Use custom PDF viewer (Beta)',
|
||||
@@ -1634,10 +1645,17 @@ class Setting extends BaseModel {
|
||||
storage: SettingStorage.Database,
|
||||
},
|
||||
|
||||
// The biometrics feature is disabled by default and marked as beta
|
||||
// because it seems to cause a freeze or slow down startup on
|
||||
// certain devices. May be the reason for:
|
||||
//
|
||||
// - https://discourse.joplinapp.org/t/on-android-when-joplin-gets-started-offline/29951/1
|
||||
// - https://github.com/laurent22/joplin/issues/7956
|
||||
'security.biometricsEnabled': {
|
||||
value: false,
|
||||
type: SettingItemType.Bool,
|
||||
label: () => _('Use biometrics to secure access to the app'),
|
||||
label: () => `${_('Use biometrics to secure access to the app')} (Beta)`,
|
||||
description: () => 'Important: This is a beta feature and it is not compatible with certain devices. If the app no longer starts after enabling this or is very slow to start, please uninstall and reinstall the app.',
|
||||
public: true,
|
||||
appTypes: [AppType.Mobile],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "2.10.2",
|
||||
"version": "2.11.0",
|
||||
"description": "Joplin Core library",
|
||||
"author": "Laurent Cozic",
|
||||
"homepage": "",
|
||||
@@ -34,10 +34,11 @@
|
||||
"@joplin/fork-htmlparser2": "^4.1.43",
|
||||
"@joplin/fork-sax": "^1.2.47",
|
||||
"@joplin/fork-uslug": "^1.0.8",
|
||||
"@joplin/htmlpack": "^2.10.2",
|
||||
"@joplin/renderer": "^2.10.2",
|
||||
"@joplin/htmlpack": "~2.11",
|
||||
"@joplin/renderer": "~2.11",
|
||||
"@joplin/turndown": "^4.0.65",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.47",
|
||||
"@joplin/utils": "~2.11",
|
||||
"@types/nanoid": "3.0.0",
|
||||
"async-mutex": "0.4.0",
|
||||
"base-64": "1.0.0",
|
||||
@@ -53,7 +54,7 @@
|
||||
"fast-xml-parser": "3.21.1",
|
||||
"follow-redirects": "1.15.2",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "11.1.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"hpagent": "1.2.0",
|
||||
"html-entities": "1.4.0",
|
||||
"html-minifier": "4.0.0",
|
||||
@@ -68,7 +69,7 @@
|
||||
"moment": "2.29.4",
|
||||
"multiparty": "4.2.3",
|
||||
"mustache": "4.2.0",
|
||||
"nanoid": "3.3.4",
|
||||
"nanoid": "3.3.6",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-notifier": "10.0.1",
|
||||
"node-persist": "3.1.3",
|
||||
@@ -82,7 +83,7 @@
|
||||
"reselect": "4.1.7",
|
||||
"server-destroy": "1.0.1",
|
||||
"sprintf-js": "1.1.2",
|
||||
"sqlite3": "5.1.4",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
"string-to-stream": "3.0.1",
|
||||
"tar": "6.1.13",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
const { splitCommandString } = require('../../string-utils');
|
||||
import { splitCommandString } from '@joplin/utils';
|
||||
import { spawn } from 'child_process';
|
||||
import Logger from '../../Logger';
|
||||
import Setting from '../../models/Setting';
|
||||
|
||||
@@ -110,6 +110,8 @@ export default class ReportService {
|
||||
let syncedCount = 0;
|
||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
||||
const d = BaseItem.syncItemDefinitions_[i];
|
||||
// ref: https://github.com/laurent22/joplin/issues/7940#issuecomment-1473709148
|
||||
if (d.className === 'MasterKey') continue;
|
||||
const ItemClass = BaseItem.getClass(d.className);
|
||||
const o = {
|
||||
total: await ItemClass.count(),
|
||||
|
||||
@@ -100,6 +100,11 @@ export default class PluginService extends BaseService {
|
||||
return this.plugins_;
|
||||
}
|
||||
|
||||
public enabledPlugins(pluginSettings: PluginSettings): Plugins {
|
||||
const enabledPlugins = Object.fromEntries(Object.entries(this.plugins_).filter((p) => this.pluginEnabled(pluginSettings, p[0])));
|
||||
return enabledPlugins;
|
||||
}
|
||||
|
||||
public get pluginIds(): string[] {
|
||||
return Object.keys(this.plugins_);
|
||||
}
|
||||
@@ -108,6 +113,10 @@ export default class PluginService extends BaseService {
|
||||
return this.isSafeMode_;
|
||||
}
|
||||
|
||||
public get appVersion(): string {
|
||||
return this.appVersion_;
|
||||
}
|
||||
|
||||
public set isSafeMode(v: boolean) {
|
||||
this.isSafeMode_ = v;
|
||||
}
|
||||
|
||||
@@ -215,21 +215,21 @@ export default class RepositoryApi {
|
||||
return this.manifests_;
|
||||
}
|
||||
|
||||
public async canBeUpdatedPlugins(installedManifests: PluginManifest[]): Promise<string[]> {
|
||||
public async canBeUpdatedPlugins(installedManifests: PluginManifest[], appVersion: string): Promise<string[]> {
|
||||
const output = [];
|
||||
|
||||
for (const manifest of installedManifests) {
|
||||
const canBe = await this.pluginCanBeUpdated(manifest.id, manifest.version);
|
||||
const canBe = await this.pluginCanBeUpdated(manifest.id, manifest.version, appVersion);
|
||||
if (canBe) output.push(manifest.id);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async pluginCanBeUpdated(pluginId: string, installedVersion: string): Promise<boolean> {
|
||||
public async pluginCanBeUpdated(pluginId: string, installedVersion: string, appVersion: string): Promise<boolean> {
|
||||
const manifest = (await this.manifests()).find(m => m.id === pluginId);
|
||||
if (!manifest) return false;
|
||||
return compareVersions(installedVersion, manifest.version) < 0;
|
||||
return compareVersions(installedVersion, manifest.version) < 0 && compareVersions(appVersion, manifest.app_min_version) >= 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -431,6 +431,7 @@ describe('services_SearchEngine', () => {
|
||||
['title:abcd efgh', { _: ['efgh'], title: ['abcd'] }],
|
||||
['title:abcd', { title: ['abcd'] }],
|
||||
['"abcd efgh"', { _: ['abcd efgh'] }],
|
||||
['"abcd efgh" ijkl', { _: ['abcd efgh', 'ijkl'] }],
|
||||
['title:abcd title:efgh', { title: ['abcd', 'efgh'] }],
|
||||
];
|
||||
|
||||
|
||||
@@ -5,20 +5,31 @@ import Note from '../../models/Note';
|
||||
import BaseModel from '../../BaseModel';
|
||||
import ItemChangeUtils from '../ItemChangeUtils';
|
||||
import shim from '../../shim';
|
||||
import filterParser from './filterParser';
|
||||
import filterParser, { Term } from './filterParser';
|
||||
import queryBuilder from './queryBuilder';
|
||||
import { ItemChangeEntity, NoteEntity } from '../database/types';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { pregQuote, scriptType, removeDiacritics } = require('../../string-utils.js');
|
||||
|
||||
enum SearchType {
|
||||
Auto = 'auto',
|
||||
Basic = 'basic',
|
||||
Nonlatin = 'nonlatin',
|
||||
Fts = 'fts',
|
||||
}
|
||||
|
||||
interface SearchOptions {
|
||||
searchType: SearchType;
|
||||
}
|
||||
|
||||
export default class SearchEngine {
|
||||
|
||||
public static instance_: SearchEngine = null;
|
||||
public static relevantFields = 'id, title, body, user_created_time, user_updated_time, is_todo, todo_completed, todo_due, parent_id, latitude, longitude, altitude, source_url';
|
||||
public static SEARCH_TYPE_AUTO = 'auto';
|
||||
public static SEARCH_TYPE_BASIC = 'basic';
|
||||
public static SEARCH_TYPE_NONLATIN_SCRIPT = 'nonlatin';
|
||||
public static SEARCH_TYPE_FTS = 'fts';
|
||||
public static SEARCH_TYPE_AUTO = SearchType.Auto;
|
||||
public static SEARCH_TYPE_BASIC = SearchType.Basic;
|
||||
public static SEARCH_TYPE_NONLATIN_SCRIPT = SearchType.Nonlatin;
|
||||
public static SEARCH_TYPE_FTS = SearchType.Fts;
|
||||
|
||||
public dispatch: Function = (_o: any) => {};
|
||||
private logger_ = new Logger();
|
||||
@@ -417,7 +428,7 @@ export default class SearchEngine {
|
||||
|
||||
const trimQuotes = (str: string) => str.startsWith('"') ? str.substr(1, str.length - 2) : str;
|
||||
|
||||
let allTerms: any[] = [];
|
||||
let allTerms: Term[] = [];
|
||||
|
||||
try {
|
||||
allTerms = filterParser(query);
|
||||
@@ -429,7 +440,20 @@ export default class SearchEngine {
|
||||
const titleTerms = allTerms.filter(x => x.name === 'title' && !x.negated).map(x => trimQuotes(x.value));
|
||||
const bodyTerms = allTerms.filter(x => x.name === 'body' && !x.negated).map(x => trimQuotes(x.value));
|
||||
|
||||
const terms: any = { _: textTerms, 'title': titleTerms, 'body': bodyTerms };
|
||||
interface ComplexTerm {
|
||||
type: 'regex' | 'text';
|
||||
value: string;
|
||||
scriptType: any;
|
||||
valueRegex?: RegExp;
|
||||
}
|
||||
|
||||
interface Terms {
|
||||
_: (string | ComplexTerm)[];
|
||||
title: (string | ComplexTerm)[];
|
||||
body: (string | ComplexTerm)[];
|
||||
}
|
||||
|
||||
const terms: Terms = { _: textTerms, 'title': titleTerms, 'body': bodyTerms };
|
||||
|
||||
// Filter terms:
|
||||
// - Convert wildcards to regex
|
||||
@@ -438,7 +462,9 @@ export default class SearchEngine {
|
||||
|
||||
let termCount = 0;
|
||||
const keys = [];
|
||||
for (const col in terms) {
|
||||
for (const col2 in terms) {
|
||||
const col = col2 as '_' | 'title' | 'body';
|
||||
|
||||
if (!terms.hasOwnProperty(col)) continue;
|
||||
|
||||
if (!terms[col].length) {
|
||||
@@ -447,7 +473,7 @@ export default class SearchEngine {
|
||||
}
|
||||
|
||||
for (let i = terms[col].length - 1; i >= 0; i--) {
|
||||
const term = terms[col][i];
|
||||
const term = terms[col][i] as string;
|
||||
|
||||
// SQlLite FTS doesn't allow "*" queries and neither shall we
|
||||
if (term === '*') {
|
||||
@@ -468,12 +494,16 @@ export default class SearchEngine {
|
||||
}
|
||||
|
||||
//
|
||||
// The object "allTerms" is used for query construction purposes (this contains all the filter terms)
|
||||
// Since this is used for the FTS match query, we need to normalize text, title and body terms.
|
||||
// Note, we're not normalizing terms like tag because these are matched using SQL LIKE operator and so we must preserve their diacritics.
|
||||
// The object "allTerms" is used for query construction purposes (this
|
||||
// contains all the filter terms) Since this is used for the FTS match
|
||||
// query, we need to normalize text, title and body terms. Note, we're
|
||||
// not normalizing terms like tag because these are matched using SQL
|
||||
// LIKE operator and so we must preserve their diacritics.
|
||||
//
|
||||
// The object "terms" only include text, title, body terms and is used for highlighting.
|
||||
// By not normalizing the text, title, body in "terms", highlighting still works correctly for words with diacritics.
|
||||
// The object "terms" only include text, title, body terms and is used
|
||||
// for highlighting. By not normalizing the text, title, body in
|
||||
// "terms", highlighting still works correctly for words with
|
||||
// diacritics.
|
||||
//
|
||||
|
||||
allTerms = allTerms.map(x => {
|
||||
@@ -521,9 +551,9 @@ export default class SearchEngine {
|
||||
const searchOptions: any = {};
|
||||
|
||||
for (const key of parsedQuery.keys) {
|
||||
if (parsedQuery.terms[key].length === 0) continue;
|
||||
if ((parsedQuery.terms as any)[key].length === 0) continue;
|
||||
|
||||
const term = parsedQuery.terms[key].map((x: any) => x.value).join(' ');
|
||||
const term = (parsedQuery.terms as any)[key].map((x: Term) => x.value).join(' ');
|
||||
if (key === '_') searchOptions.anywherePattern = `*${term}*`;
|
||||
if (key === 'title') searchOptions.titlePattern = `*${term}*`;
|
||||
if (key === 'body') searchOptions.bodyPattern = `*${term}*`;
|
||||
@@ -561,12 +591,13 @@ export default class SearchEngine {
|
||||
return SearchEngine.SEARCH_TYPE_FTS;
|
||||
}
|
||||
|
||||
public async search(searchString: string, options: any = null) {
|
||||
public async search(searchString: string, options: SearchOptions = null) {
|
||||
if (!searchString) return [];
|
||||
|
||||
options = Object.assign({}, {
|
||||
options = {
|
||||
searchType: SearchEngine.SEARCH_TYPE_AUTO,
|
||||
}, options);
|
||||
...options,
|
||||
};
|
||||
|
||||
const searchType = this.determineSearchType_(searchString, options.searchType);
|
||||
const parsedQuery = await this.parseQuery(searchString);
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('services_SearchFilter', () => {
|
||||
for (const searchType of [SearchEngine.SEARCH_TYPE_FTS, SearchEngine.SEARCH_TYPE_NONLATIN_SCRIPT]) {
|
||||
|
||||
describe(`search type ${searchType}`, () => {
|
||||
it('Check case insensitivity for filter keywords', (async () => {
|
||||
it('check case insensitivity for filter keywords', (async () => {
|
||||
let rows;
|
||||
const notebook1 = await Folder.save({ title: 'folderA' });
|
||||
const notebook2 = await Folder.save({ title: 'folderB' });
|
||||
@@ -311,7 +311,7 @@ describe('services_SearchFilter', () => {
|
||||
}));
|
||||
|
||||
|
||||
it('should support filtering by tags', (async () => {
|
||||
it('should support filtering by tags (2)', (async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'peace talks', body: 'battle ground' });
|
||||
const n2 = await Note.save({ title: 'mouse', body: 'mister' });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
interface Term {
|
||||
export interface Term {
|
||||
name: string;
|
||||
value: string;
|
||||
negated: boolean;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import gotoAnythingStyleQuery from './gotoAnythingStyleQuery';
|
||||
|
||||
describe('searchengine/gotoAnythingStyleQuery', () => {
|
||||
|
||||
it('should prepare queries', () => {
|
||||
const testCases: [string, string][] = [
|
||||
['hello', 'hello*'],
|
||||
['hello welc', 'hello* welc*'],
|
||||
];
|
||||
|
||||
for (const [input, expected] of testCases) {
|
||||
const actual = gotoAnythingStyleQuery(input);
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -138,80 +138,6 @@ function commandArgumentsToString(args) {
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
function splitCommandString(command, options = null) {
|
||||
options = options || {};
|
||||
if (!('handleEscape' in options)) {
|
||||
options.handleEscape = true;
|
||||
}
|
||||
|
||||
const args = [];
|
||||
let state = 'start';
|
||||
let current = '';
|
||||
let quote = '"';
|
||||
let escapeNext = false;
|
||||
for (let i = 0; i < command.length; i++) {
|
||||
const c = command[i];
|
||||
|
||||
if (state === 'quotes') {
|
||||
if (c !== quote) {
|
||||
current += c;
|
||||
} else {
|
||||
args.push(current);
|
||||
current = '';
|
||||
state = 'start';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (escapeNext) {
|
||||
current += c;
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c === '\\' && options.handleEscape) {
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c === '"' || c === '\'') {
|
||||
state = 'quotes';
|
||||
quote = c;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state === 'arg') {
|
||||
if (c === ' ' || c === '\t') {
|
||||
args.push(current);
|
||||
current = '';
|
||||
state = 'start';
|
||||
} else {
|
||||
current += c;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c !== ' ' && c !== '\t') {
|
||||
state = 'arg';
|
||||
current += c;
|
||||
}
|
||||
}
|
||||
|
||||
if (state === 'quotes') {
|
||||
throw new Error(`Unclosed quote in command line: ${command}`);
|
||||
}
|
||||
|
||||
if (current !== '') {
|
||||
args.push(current);
|
||||
}
|
||||
|
||||
if (args.length <= 0) {
|
||||
throw new Error('Empty command line');
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function splitCommandBatch(commandBatch) {
|
||||
const commandLines = [];
|
||||
const eol = '\n';
|
||||
@@ -368,4 +294,4 @@ function scriptType(s) {
|
||||
return 'en';
|
||||
}
|
||||
|
||||
module.exports = Object.assign({ formatCssSize, camelCaseToDash, removeDiacritics, substrWithEllipsis, nextWhitespaceIndex, escapeFilename, wrap, splitCommandString, splitCommandBatch, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType, commandArgumentsToString }, stringUtilsCommon);
|
||||
module.exports = Object.assign({ formatCssSize, camelCaseToDash, removeDiacritics, substrWithEllipsis, nextWhitespaceIndex, escapeFilename, wrap, splitCommandBatch, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType, commandArgumentsToString }, stringUtilsCommon);
|
||||
|
||||
@@ -60,7 +60,7 @@ const globalStyle: any = {
|
||||
toolbarPadding: 6,
|
||||
appearance: 'light',
|
||||
mainPadding: 12,
|
||||
topRowHeight: 81,
|
||||
topRowHeight: 50,
|
||||
editorPaddingLeft: 8,
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,17 @@ export function credentialDir() {
|
||||
throw new Error(`Could not find credential directory in any of these paths: ${JSON.stringify(toTry)}`);
|
||||
}
|
||||
|
||||
export const hasCredentialFile = (filename: string) => {
|
||||
let d = '';
|
||||
try {
|
||||
d = credentialDir();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pathExistsSync(`${d}/${filename}`);
|
||||
};
|
||||
|
||||
export function credentialFile(filename: string) {
|
||||
const rootDir = credentialDir();
|
||||
const output = `${rootDir}/${filename}`;
|
||||
|
||||
@@ -86,6 +86,7 @@ describe('getPluginLists', () => {
|
||||
);
|
||||
plugins[plugin.manifest.id] = plugin;
|
||||
}
|
||||
|
||||
const v = versionInfo(packageInfo, plugins);
|
||||
|
||||
expect(v.body).toMatch(/\n\nPlugin1: 1\nPlugin2: 1\nPlugin3: 1/);
|
||||
@@ -110,6 +111,7 @@ describe('getPluginLists', () => {
|
||||
|
||||
plugins[plugin.manifest.id] = plugin;
|
||||
}
|
||||
|
||||
const v = versionInfo(packageInfo, plugins);
|
||||
|
||||
const body = '\n';
|
||||
|
||||
@@ -16,6 +16,8 @@ function getPluginLists(plugins: Plugins): PluginList {
|
||||
}
|
||||
}
|
||||
|
||||
pluginList.sort(Intl.Collator().compare);
|
||||
|
||||
let completeList = '';
|
||||
let summary = '';
|
||||
if (pluginList.length > 0) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/pdf-viewer",
|
||||
"version": "2.10.0",
|
||||
"version": "2.11.0",
|
||||
"description": "Provides embedded PDF viewers for Joplin",
|
||||
"main": "dist/main.js",
|
||||
"types": "src/main.ts",
|
||||
@@ -28,7 +28,7 @@
|
||||
"css-loader": "6.7.3",
|
||||
"jest": "29.4.3",
|
||||
"jest-environment-jsdom": "29.4.3",
|
||||
"style-loader": "3.3.1",
|
||||
"style-loader": "3.3.2",
|
||||
"ts-jest": "29.0.5",
|
||||
"ts-loader": "9.4.2",
|
||||
"typescript": "4.9.4",
|
||||
@@ -39,11 +39,11 @@
|
||||
"@fortawesome/fontawesome-svg-core": "6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.1.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@joplin/lib": "~2.10",
|
||||
"@joplin/lib": "~2.11",
|
||||
"async-mutex": "0.4.0",
|
||||
"pdfjs-dist": "2.16.105",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"styled-components": "5.3.5"
|
||||
"styled-components": "5.3.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ function loadFile(filePath: string) {
|
||||
|
||||
describe('pdfData', () => {
|
||||
|
||||
test('Should have correct page count', async () => {
|
||||
test('should have correct page count', async () => {
|
||||
const file = await loadFile(pdfFilePath1);
|
||||
const pdf = new PdfDocument(document);
|
||||
await pdf.loadDoc(file);
|
||||
@@ -39,7 +39,7 @@ describe('pdfData', () => {
|
||||
// }).rejects.toThrowError();
|
||||
// });
|
||||
|
||||
test('Should get correct page size', async () => {
|
||||
test('should get correct page size', async () => {
|
||||
const file = await loadFile(pdfFilePath1);
|
||||
const pdf = new PdfDocument(document);
|
||||
await pdf.loadDoc(file);
|
||||
@@ -48,7 +48,7 @@ describe('pdfData', () => {
|
||||
expect(size.width).toBeCloseTo(594.95996);
|
||||
});
|
||||
|
||||
test('Should calculate scaled size', async () => {
|
||||
test('should calculate scaled size', async () => {
|
||||
const file = await loadFile(pdfFilePath1);
|
||||
const pdf = new PdfDocument(document);
|
||||
await pdf.loadDoc(file);
|
||||
@@ -56,7 +56,7 @@ describe('pdfData', () => {
|
||||
expect(scaledSize.scale).toBeCloseTo(0.336157);
|
||||
});
|
||||
|
||||
test('Should get correct active page', async () => {
|
||||
test('should get correct active page', async () => {
|
||||
const file = await loadFile(pdfFilePath1);
|
||||
const pdf = new PdfDocument(document);
|
||||
await pdf.loadDoc(file);
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as path from 'path';
|
||||
import * as process from 'process';
|
||||
import validatePluginId from '@joplin/lib/services/plugins/utils/validatePluginId';
|
||||
import validatePluginVersion from '@joplin/lib/services/plugins/utils/validatePluginVersion';
|
||||
import { execCommand2, resolveRelativePathWithinDir, gitPullTry, gitRepoCleanTry, gitRepoClean } from '@joplin/tools/tool-utils.js';
|
||||
import { resolveRelativePathWithinDir, gitPullTry, gitRepoCleanTry, gitRepoClean } from '@joplin/tools/tool-utils.js';
|
||||
import checkIfPluginCanBeAdded from './lib/checkIfPluginCanBeAdded';
|
||||
import updateReadme from './lib/updateReadme';
|
||||
import { NpmPackage } from './lib/types';
|
||||
@@ -17,6 +17,7 @@ import gitCompareUrl from './lib/gitCompareUrl';
|
||||
import commandUpdateRelease from './commands/updateRelease';
|
||||
import { isJoplinPluginPackage, readJsonFile } from './lib/utils';
|
||||
import { applyManifestOverrides, getObsoleteManifests, readManifestOverrides } from './lib/overrideUtils';
|
||||
import { execCommand } from '@joplin/utils';
|
||||
|
||||
function pluginInfoFromSearchResults(results: any[]): NpmPackage[] {
|
||||
const output: NpmPackage[] = [];
|
||||
@@ -49,7 +50,7 @@ async function checkPluginRepository(dirPath: string, dryRun: boolean) {
|
||||
async function extractPluginFilesFromPackage(existingManifests: any, workDir: string, packageName: string, destDir: string): Promise<any> {
|
||||
const previousDir = chdir(workDir);
|
||||
|
||||
await execCommand2(`npm install ${packageName} --save --ignore-scripts`, { showStderr: false, showStdout: false });
|
||||
await execCommand(`npm install ${packageName} --save --ignore-scripts`, { showStderr: false, showStdout: false });
|
||||
|
||||
const pluginDir = resolveRelativePathWithinDir(workDir, 'node_modules', packageName, 'publish');
|
||||
|
||||
@@ -154,7 +155,7 @@ async function processNpmPackage(npmPackage: NpmPackage, repoDir: string, dryRun
|
||||
|
||||
await fs.mkdirp(packageTempDir);
|
||||
chdir(packageTempDir);
|
||||
await execCommand2('npm init --yes --loglevel silent', { quiet: true });
|
||||
await execCommand('npm init --yes --loglevel silent', { quiet: true });
|
||||
|
||||
let actionType: ProcessingActionType = ProcessingActionType.Update;
|
||||
let manifests: any = {};
|
||||
@@ -200,8 +201,8 @@ async function processNpmPackage(npmPackage: NpmPackage, repoDir: string, dryRun
|
||||
|
||||
if (!dryRun) {
|
||||
if (!(await gitRepoClean())) {
|
||||
await execCommand2('git add -A', { showStdout: false });
|
||||
await execCommand2(['git', 'commit', '-m', commitMessage(actionType, manifest, previousManifest, npmPackage, error)], { showStdout: false });
|
||||
await execCommand('git add -A', { showStdout: false });
|
||||
await execCommand(['git', 'commit', '-m', commitMessage(actionType, manifest, previousManifest, npmPackage, error)], { showStdout: false });
|
||||
} else {
|
||||
console.info('Nothing to commit');
|
||||
}
|
||||
@@ -227,14 +228,14 @@ async function commandBuild(args: CommandBuildArgs) {
|
||||
if (!dryRun) {
|
||||
if (!(await gitRepoClean())) {
|
||||
console.info('Updating README...');
|
||||
await execCommand2('git add -A');
|
||||
await execCommand2('git commit -m "Update README"');
|
||||
await execCommand('git add -A');
|
||||
await execCommand('git commit -m "Update README"');
|
||||
}
|
||||
}
|
||||
|
||||
chdir(previousDir);
|
||||
|
||||
const searchResults = (await execCommand2('npm search joplin-plugin --searchlimit 5000 --json', { showStdout: false, showStderr: false })).trim();
|
||||
const searchResults = (await execCommand('npm search joplin-plugin --searchlimit 5000 --json', { showStdout: false, showStderr: false })).trim();
|
||||
const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults));
|
||||
|
||||
for (const npmPackage of npmPackages) {
|
||||
@@ -245,11 +246,11 @@ async function commandBuild(args: CommandBuildArgs) {
|
||||
await commandUpdateRelease(args);
|
||||
|
||||
if (!(await gitRepoClean())) {
|
||||
await execCommand2('git add -A');
|
||||
await execCommand2('git commit -m "Update stats"');
|
||||
await execCommand('git add -A');
|
||||
await execCommand('git commit -m "Update stats"');
|
||||
}
|
||||
|
||||
await execCommand2('git push');
|
||||
await execCommand('git push');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user