You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-27 20:29:45 +02:00
Compare commits
180 Commits
server-v2.
...
test-binar
Author | SHA1 | Date | |
---|---|---|---|
|
376de4ccbd | ||
|
ed58ed91fd | ||
|
a0fd942a1c | ||
|
4842500f0a | ||
|
84c7f28ec5 | ||
|
f3eea43d24 | ||
|
8babaddbcb | ||
|
13cdaabb17 | ||
|
a94aa21088 | ||
|
6116bed4e3 | ||
|
fabd0b4dda | ||
|
6b72f86e7b | ||
|
02cf546124 | ||
|
eecb012d64 | ||
|
04e9b40769 | ||
|
efdbaeb397 | ||
|
46425b920c | ||
|
f5be43c2ac | ||
|
080541a2fe | ||
|
0b3919fd62 | ||
|
7dc638edf4 | ||
|
3b686194d8 | ||
|
5c2640f88f | ||
|
eca0f92dff | ||
|
260fa6c038 | ||
|
8ec6bc9138 | ||
|
93fa92369b | ||
|
bc6c5ab7a7 | ||
|
1826625e4f | ||
|
20b8fb2719 | ||
|
f813e71b29 | ||
|
02422a6e31 | ||
|
69a34e87f3 | ||
|
cbeaa16b61 | ||
|
05917ac142 | ||
|
897fd0f727 | ||
|
9edb402b18 | ||
|
36ae58ffe1 | ||
|
5ef8b86957 | ||
|
c7228cfcd6 | ||
|
7a791dbf98 | ||
|
a543588527 | ||
|
998ad142a6 | ||
|
b6a53f96f7 | ||
|
841a9c6e09 | ||
|
d25c078ef0 | ||
|
0c8de68b80 | ||
|
44d93d52d3 | ||
|
073bec9e8c | ||
|
e6a8c2bea5 | ||
|
81c316cd2c | ||
|
659c851960 | ||
|
572701d9a0 | ||
|
ff69ac17be | ||
|
b44945b3a0 | ||
|
2584224026 | ||
|
46136871bf | ||
|
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 |
@@ -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/
|
||||
@@ -404,6 +405,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
|
||||
@@ -846,6 +848,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 +864,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
|
||||
|
@@ -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': [
|
||||
{
|
||||
|
3
.github/scripts/run_ci.sh
vendored
3
.github/scripts/run_ci.sh
vendored
@@ -180,9 +180,6 @@ cd "$ROOT_DIR/packages/app-desktop"
|
||||
|
||||
if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
echo "Step: Building and publishing desktop application..."
|
||||
# cd "$ROOT_DIR/packages/tools"
|
||||
# node bundleDefaultPlugins.js
|
||||
cd "$ROOT_DIR/packages/app-desktop"
|
||||
USE_HARD_LINKS=false yarn run dist
|
||||
elif [[ $IS_LINUX = 1 ]] && [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then
|
||||
echo "Step: Building Docker Image..."
|
||||
|
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
|
||||
|
6
.github/workflows/github-actions-main.yml
vendored
6
.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:
|
||||
@@ -92,6 +93,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 +131,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:
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -392,6 +392,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
|
||||
@@ -834,6 +835,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 +851,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
|
||||
|
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@@ -780,6 +780,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 +948,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 +1004,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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
|
@@ -24,7 +24,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
|
||||
|
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,
|
||||
|
13
package.json
13
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,7 +91,7 @@
|
||||
"@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": {
|
||||
|
@@ -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, {
|
||||
|
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
sz_program=${SZA_PATH:-7za}
|
||||
sz_type=${SZA_ARCHIVE_TYPE:-xz}
|
||||
|
||||
case $1 in
|
||||
-d) "$sz_program" e -si -so -t${sz_type} ;;
|
||||
*) "$sz_program" a f -si -so -t${sz_type} -mx${SZA_COMPRESSION_LEVEL:-9} ;;
|
||||
esac 2> /dev/null
|
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Vladimir Krivosheev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
@@ -0,0 +1 @@
|
||||
7-Zip precompiled binaries.
|
2
packages/app-desktop/build/defaultPlugins/io.github.jackgruber.backup/7zip-bin/index.d.ts
vendored
Normal file
2
packages/app-desktop/build/defaultPlugins/io.github.jackgruber.backup/7zip-bin/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export const path7za: string
|
||||
export const path7x: string
|
@@ -0,0 +1,22 @@
|
||||
"use strict"
|
||||
|
||||
const path = require("path")
|
||||
|
||||
function getPath() {
|
||||
if (process.env.USE_SYSTEM_7ZA === "true") {
|
||||
return "7za"
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
return path.join(__dirname, "mac", process.arch, "7za")
|
||||
}
|
||||
else if (process.platform === "win32") {
|
||||
return path.join(__dirname, "win", process.arch, "7za.exe")
|
||||
}
|
||||
else {
|
||||
return path.join(__dirname, "linux", process.arch, "7za")
|
||||
}
|
||||
}
|
||||
|
||||
exports.path7za = getPath()
|
||||
exports.path7x = path.join(__dirname, "7x.sh")
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
BASEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
rm -rf /tmp/7z-linux
|
||||
mkdir /tmp/7z-linux
|
||||
cp "$BASEDIR/do-build.sh" /tmp/7z-linux/do-build.sh
|
||||
docker run --rm -v /tmp/7z-linux:/project buildpack-deps:xenial /project/do-build.sh
|
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
apt-get update -qq
|
||||
apt-get upgrade -qq
|
||||
|
||||
echo "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-5.0 main" > /etc/apt/sources.list.d/llvm.list
|
||||
curl -L http://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add -
|
||||
apt-get update -qq
|
||||
apt-get install -qq bzip2 yasm clang-5.0 lldb-5.0 lld-5.0
|
||||
|
||||
ln -s /usr/bin/clang-5.0 /usr/bin/clang
|
||||
ln -s /usr/bin/clang++-5.0 /usr/bin/clang++
|
||||
|
||||
mkdir -p /tmp/7z
|
||||
cd /tmp/7z
|
||||
curl -L http://downloads.sourceforge.net/project/p7zip/p7zip/16.02/p7zip_16.02_src_all.tar.bz2 | tar -xj -C . --strip-components 1
|
||||
cp makefile.linux_clang_amd64_asm makefile.machine
|
||||
make -j4
|
||||
mv bin/7za /project/7za
|
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "7zip-bin",
|
||||
"description": "7-Zip precompiled binaries",
|
||||
"version": "5.1.1",
|
||||
"files": [
|
||||
"*.js",
|
||||
"7x.sh",
|
||||
"index.d.ts",
|
||||
"linux",
|
||||
"mac",
|
||||
"win"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": "develar/7zip-bin",
|
||||
"keywords": [
|
||||
"7zip",
|
||||
"7z",
|
||||
"7za"
|
||||
]
|
||||
}
|
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"id": "io.github.jackgruber.backup",
|
||||
"app_min_version": "2.1.3",
|
||||
"version": "1.1.1",
|
||||
"name": "Simple Backup",
|
||||
"description": "Plugin to create manual and automatic backups.",
|
||||
"author": "JackGruber",
|
||||
"homepage_url": "https://github.com/JackGruber/joplin-plugin-backup/blob/master/README.md",
|
||||
"repository_url": "https://github.com/JackGruber/joplin-plugin-backup",
|
||||
"keywords": ["backup", "jex", "export", "zip", "7zip", "encrypted"]
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
#joplin-plugin-content {
|
||||
width: fit-content;
|
||||
background-color: var(--joplin-background-color);
|
||||
color: var(--joplin-color);
|
||||
}
|
||||
|
||||
#backuperror {
|
||||
width: fit-content;
|
||||
display: block;
|
||||
flex-direction: column;
|
||||
min-width: 300px;
|
||||
overflow-wrap: break-word;
|
||||
font-size: var(--joplin-font-size);
|
||||
font-family: var(--joplin-font-family);
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"id": "plugin.calebjohn.rich-markdown",
|
||||
"app_min_version": "2.4",
|
||||
"version": "0.8.3",
|
||||
"name": "Rich Markdown",
|
||||
"description": "Helping you ditch the markdown viewer for good.",
|
||||
"author": "Caleb John",
|
||||
"homepage_url": "https://github.com/CalebJohn/joplin-rich-markdown#readme",
|
||||
"repository_url": "https://github.com/CalebJohn/joplin-rich-markdown",
|
||||
"keywords": [
|
||||
"editor",
|
||||
"visual"
|
||||
],
|
||||
"_publish_hash": "sha256:7059ff05f3fcac2ead5b8f4cb04ec66830be55e02407408a39ab20e536055f5d",
|
||||
"_publish_commit": "main:9816b21068ce09014e8ef8adb0bd31bc20343247"
|
||||
}
|
Binary file not shown.
@@ -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,
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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,11 +1,12 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.10.8",
|
||||
"version": "2.11.1",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dist": "yarn run electronRebuild && npx electron-builder",
|
||||
"bundleDefaultPlugins": "cd ../tools && node bundleDefaultPlugins.js",
|
||||
"build": "gulp build",
|
||||
"postinstall": "yarn run build",
|
||||
"electronBuilder": "gulp electronBuilder",
|
||||
@@ -90,7 +91,11 @@
|
||||
"CFBundleURLName": "org.joplinapp.x-callback-url"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"binaries": [
|
||||
"Contents/Resources/build/defaultPlugins/io.github.jackgruber.backup/7zip-bin/mac/arm64/7za",
|
||||
"Contents/Resources/build/defaultPlugins/io.github.jackgruber.backup/7zip-bin/mac/x64/7za"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"icon": "../../Assets/LinuxIcons",
|
||||
@@ -107,7 +112,9 @@
|
||||
},
|
||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~2.10",
|
||||
"@electron/notarize": "1.2.3",
|
||||
"@electron/rebuild": "3.2.10",
|
||||
"@joplin/tools": "~2.11",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.2.6",
|
||||
"@types/node": "18.11.18",
|
||||
@@ -115,9 +122,7 @@
|
||||
"@types/react-redux": "7.1.25",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"electron": "19.1.4",
|
||||
"electron-builder": "23.6.0",
|
||||
"electron-notarize": "1.2.2",
|
||||
"electron-rebuild": "3.2.9",
|
||||
"electron-builder": "24.1.2",
|
||||
"glob": "8.1.0",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.4.3",
|
||||
@@ -136,9 +141,9 @@
|
||||
"@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/pdf-viewer": "~2.11",
|
||||
"@joplin/renderer": "~2.11",
|
||||
"async-mutex": "0.4.0",
|
||||
"codemirror": "5.65.9",
|
||||
"color": "3.2.1",
|
||||
@@ -147,7 +152,7 @@
|
||||
"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",
|
||||
"immer": "7.0.15",
|
||||
"keytar": "7.9.0",
|
||||
@@ -163,17 +168,17 @@
|
||||
"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);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const electron_notarize = require('electron-notarize');
|
||||
const electron_notarize = require('@electron/notarize');
|
||||
const execCommand = require('./execCommand');
|
||||
|
||||
function isDesktopAppTag(tagName) {
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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.0",
|
||||
"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': {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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' });
|
||||
|
@@ -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);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user