1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
palerdot
c7299cf679 fix(desktop): linux folder display bug fix 2023-03-10 12:36:40 +05:30
413 changed files with 22119 additions and 23749 deletions

View File

@@ -66,7 +66,6 @@ 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/
@@ -74,7 +73,6 @@ packages/turndown/
packages/pdf-viewer/dist
plugin_types/
readme/
packages/react-native-vosk/lib/
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
packages/app-cli/app/LinkSelector.js
@@ -397,7 +395,6 @@ packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/SearchPanel.js
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteList.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
@@ -407,7 +404,6 @@ 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
@@ -418,15 +414,12 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/search.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
packages/app-mobile/gulpfile.js
packages/app-mobile/root.js
packages/app-mobile/services/AlarmServiceDriver.android.js
packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/vosk.dummy.js
packages/app-mobile/services/voiceTyping/vosk.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/utils/ShareExtension.js
@@ -483,7 +476,6 @@ packages/lib/SyncTargetOneDrive.js
packages/lib/SyncTargetRegistry.js
packages/lib/Synchronizer.js
packages/lib/TaskQueue.js
packages/lib/WelcomeUtils.js
packages/lib/array.js
packages/lib/callbackUrlUtils.js
packages/lib/callbackUrlUtils.test.js
@@ -519,7 +511,6 @@ packages/lib/import-enex-md-gen.js
packages/lib/import-enex-md-gen.test.js
packages/lib/import-enex.js
packages/lib/locale.js
packages/lib/locale.test.js
packages/lib/markdownUtils.js
packages/lib/markdownUtils.test.js
packages/lib/markdownUtils2.test.js
@@ -718,7 +709,6 @@ packages/lib/services/searchengine/SearchFilter.test.js
packages/lib/services/searchengine/filterParser.js
packages/lib/services/searchengine/filterParser.test.js
packages/lib/services/searchengine/gotoAnythingStyleQuery.js
packages/lib/services/searchengine/gotoAnythingStyleQuery.test.js
packages/lib/services/searchengine/queryBuilder.js
packages/lib/services/share/ShareService.js
packages/lib/services/share/ShareService.test.js
@@ -845,8 +835,6 @@ packages/renderer/index.js
packages/renderer/noteStyle.js
packages/renderer/pathUtils.js
packages/renderer/utils.js
packages/tools/build-release-stats.js
packages/tools/build-welcome.js
packages/tools/buildServerDocker.js
packages/tools/buildServerDocker.test.js
packages/tools/bundleDefaultPlugins.js
@@ -857,7 +845,6 @@ 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
@@ -873,7 +860,6 @@ 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
@@ -881,8 +867,6 @@ packages/tools/website/updateDownloadPage.js
packages/tools/website/updateNews.js
packages/tools/website/utils/applyTranslations.js
packages/tools/website/utils/applyTranslations.test.js
packages/tools/website/utils/convertLinksToLocale.js
packages/tools/website/utils/convertLinksToLocale.test.js
packages/tools/website/utils/frontMatter.js
packages/tools/website/utils/news.js
packages/tools/website/utils/openGraph.js

View File

@@ -91,10 +91,6 @@ 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',
@@ -159,7 +155,6 @@ module.exports = {
// 'react-hooks',
'import',
'promise',
'jest',
],
'overrides': [
{
@@ -180,7 +175,9 @@ module.exports = {
'project': './tsconfig.eslint.json',
},
'rules': {
'@typescript-eslint/explicit-member-accessibility': ['error'],
// Warn only because it would make it difficult to convert JS classes to TypeScript, unless we
// make everything public which is not great. New code however should specify member accessibility.
'@typescript-eslint/explicit-member-accessibility': ['warn'],
'@typescript-eslint/type-annotation-spacing': ['error', { 'before': false, 'after': true }],
'@typescript-eslint/no-inferrable-types': ['error', { 'ignoreParameters': true, 'ignoreProperties': true }],
'@typescript-eslint/comma-dangle': ['error', {

View File

@@ -183,32 +183,12 @@ if [[ $GIT_TAG_NAME = v* ]]; then
# cd "$ROOT_DIR/packages/tools"
# node bundleDefaultPlugins.js
cd "$ROOT_DIR/packages/app-desktop"
if [ "$IS_MACOS" == "1" ]; then
# This is to fix this error:
#
# Exit code: ENOENT. spawn /usr/bin/python ENOENT
#
# Ref: https://github.com/electron-userland/electron-builder/issues/6767#issuecomment-1096589528
#
# It can be removed once we upgrade to electron-builder@23, however we
# cannot currently do this due to this error:
# https://github.com/laurent22/joplin/issues/8149
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn run dist
else
USE_HARD_LINKS=false yarn run dist
fi
USE_HARD_LINKS=false yarn run dist
elif [[ $IS_LINUX = 1 ]] && [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then
echo "Step: Building Docker Image..."
cd "$ROOT_DIR"
yarn run buildServerDocker --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
else
echo "Step: Building but *not* publishing desktop application..."
if [ "$IS_MACOS" == "1" ]; then
# See above why we need to specify Python
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn run dist --publish=never
else
USE_HARD_LINKS=false yarn run dist --publish=never
fi
USE_HARD_LINKS=false yarn run dist --publish=never
fi

View File

@@ -6,7 +6,6 @@ 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 }}
@@ -17,7 +16,6 @@ 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

View File

@@ -7,13 +7,12 @@ on:
jobs:
CLAAssistant:
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: contributor-assistant/github-action@v2.3.0
uses: contributor-assistant/github-action@v2.2.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret

View File

@@ -6,7 +6,6 @@ permissions:
issues: write
jobs:
ProcessStaleIssues:
if: github.repository == 'laurent22/joplin'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4

View File

@@ -2,7 +2,6 @@ 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 }}
@@ -15,7 +14,7 @@ jobs:
Main:
needs: pre_job
# We always process server or desktop release tags, because they also publish the release
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'))
if: 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:
@@ -71,9 +70,7 @@ jobs:
- uses: olegtarasov/get-tag@v2.1
- uses: actions/setup-node@v2
with:
# We need to pin the version to 18.15, because 18.16+ fails with this error:
# https://github.com/facebook/react-native/issues/36440
node-version: '18.15.0'
node-version: '18'
- name: Install Yarn
run: |
@@ -95,7 +92,6 @@ 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 }}
@@ -133,7 +129,7 @@ jobs:
ServerDockerImage:
needs: pre_job
if: github.repository == 'laurent22/joplin' && needs.pre_job.outputs.should_skip != 'true'
if: needs.pre_job.outputs.should_skip != 'true'
runs-on: ${{ matrix.os }}
strategy:
matrix:

14
.gitignore vendored
View File

@@ -383,7 +383,6 @@ packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/SearchPanel.js
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteList.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
@@ -393,7 +392,6 @@ 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
@@ -404,15 +402,12 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/search.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
packages/app-mobile/gulpfile.js
packages/app-mobile/root.js
packages/app-mobile/services/AlarmServiceDriver.android.js
packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/vosk.dummy.js
packages/app-mobile/services/voiceTyping/vosk.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/utils/ShareExtension.js
@@ -469,7 +464,6 @@ packages/lib/SyncTargetOneDrive.js
packages/lib/SyncTargetRegistry.js
packages/lib/Synchronizer.js
packages/lib/TaskQueue.js
packages/lib/WelcomeUtils.js
packages/lib/array.js
packages/lib/callbackUrlUtils.js
packages/lib/callbackUrlUtils.test.js
@@ -505,7 +499,6 @@ packages/lib/import-enex-md-gen.js
packages/lib/import-enex-md-gen.test.js
packages/lib/import-enex.js
packages/lib/locale.js
packages/lib/locale.test.js
packages/lib/markdownUtils.js
packages/lib/markdownUtils.test.js
packages/lib/markdownUtils2.test.js
@@ -704,7 +697,6 @@ packages/lib/services/searchengine/SearchFilter.test.js
packages/lib/services/searchengine/filterParser.js
packages/lib/services/searchengine/filterParser.test.js
packages/lib/services/searchengine/gotoAnythingStyleQuery.js
packages/lib/services/searchengine/gotoAnythingStyleQuery.test.js
packages/lib/services/searchengine/queryBuilder.js
packages/lib/services/share/ShareService.js
packages/lib/services/share/ShareService.test.js
@@ -831,8 +823,6 @@ packages/renderer/index.js
packages/renderer/noteStyle.js
packages/renderer/pathUtils.js
packages/renderer/utils.js
packages/tools/build-release-stats.js
packages/tools/build-welcome.js
packages/tools/buildServerDocker.js
packages/tools/buildServerDocker.test.js
packages/tools/bundleDefaultPlugins.js
@@ -843,7 +833,6 @@ 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
@@ -859,7 +848,6 @@ 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
@@ -867,8 +855,6 @@ packages/tools/website/updateDownloadPage.js
packages/tools/website/updateNews.js
packages/tools/website/utils/applyTranslations.js
packages/tools/website/utils/applyTranslations.test.js
packages/tools/website/utils/convertLinksToLocale.js
packages/tools/website/utils/convertLinksToLocale.test.js
packages/tools/website/utils/frontMatter.js
packages/tools/website/utils/news.js
packages/tools/website/utils/openGraph.js

View File

@@ -14,8 +14,7 @@
"@joplin/turndown-plugin-gfm",
"@joplin/tools",
"@joplin/react-native-saf-x",
"@joplin/react-native-alarm-notification",
"@joplin/utils"
"@joplin/react-native-alarm-notification"
]
}
]

View File

@@ -1,127 +0,0 @@
diff --git a/android/build.gradle b/android/build.gradle
index 6afcbbf0cc8ca2d69dd78077d61e59a90b2136bb..9f8d72b4ec5b2b3d290975d6a255917c95300854 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -67,19 +67,19 @@ repositories {
}
// Generate UUIDs for each models contained in android/src/main/assets/
-tasks.register('genUUID') {
- doLast {
- fileTree(dir: "$rootDir/app/src/main/assets", exclude: ['*/*']).visit { fileDetails ->
- if (fileDetails.directory) {
- def odir = file("$rootDir/app/src/main/assets/$fileDetails.relativePath")
- def ofile = file("$odir/uuid")
- mkdir odir
- ofile.text = UUID.randomUUID().toString()
- }
- }
- }
-}
-preBuild.dependsOn genUUID
+// tasks.register('genUUID') {
+// doLast {
+// fileTree(dir: "$rootDir/app/src/main/assets", exclude: ['*/*']).visit { fileDetails ->
+// if (fileDetails.directory) {
+// def odir = file("$rootDir/app/src/main/assets/$fileDetails.relativePath")
+// def ofile = file("$odir/uuid")
+// mkdir odir
+// ofile.text = UUID.randomUUID().toString()
+// }
+// }
+// }
+// }
+// preBuild.dependsOn genUUID
def kotlin_version = getExtOrDefault('kotlinVersion')
diff --git a/android/src/main/java/com/reactnativevosk/VoskModule.kt b/android/src/main/java/com/reactnativevosk/VoskModule.kt
index 0e2b6595b1b2cf1ee01c6c64239c4b0ea37fce19..f3da440bc2863a59db6d2d1691c54d8d4870cb3f 100644
--- a/android/src/main/java/com/reactnativevosk/VoskModule.kt
+++ b/android/src/main/java/com/reactnativevosk/VoskModule.kt
@@ -19,13 +19,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
return "Vosk"
}
+ @ReactMethod
+ fun addListener(type: String?) {
+ // Keep: Required for RN built in Event Emitter Calls.
+ }
+
+ @ReactMethod
+ fun removeListeners(type: Int?) {
+ // Keep: Required for RN built in Event Emitter Calls.
+ }
+
override fun onResult(hypothesis: String) {
// Get text data from string object
val text = getHypothesisText(hypothesis)
// Stop recording if data found
if (text != null && text.isNotEmpty()) {
- cleanRecognizer();
+ // Don't auto-stop the recogniser - we want to do that when the user
+ // presses on "stop" only.
+ // cleanRecognizer();
sendEvent("onResult", text)
}
}
@@ -153,6 +165,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
cleanRecognizer();
}
+ @ReactMethod
+ fun stopOnly() {
+ if (speechService != null) {
+ speechService!!.stop()
+ }
+ }
+
+ @ReactMethod
+ fun cleanup() {
+ if (speechService != null) {
+ speechService!!.shutdown();
+ speechService = null
+ }
+ if (recognizer != null) {
+ recognizer!!.close();
+ recognizer = null;
+ }
+ }
+
@ReactMethod
fun unload() {
cleanRecognizer();
diff --git a/lib/typescript/index.d.ts b/lib/typescript/index.d.ts
index 441e41cc402cca3a60b34978ef4fea976076259c..a173acebb4b314402550442ad471e0f7c706e3c4 100644
--- a/lib/typescript/index.d.ts
+++ b/lib/typescript/index.d.ts
@@ -10,6 +10,8 @@ export default class Vosk {
currentRegisteredEvents: EmitterSubscription[];
start: (grammar?: string[] | null) => Promise<String>;
stop: () => void;
+ stopOnly: () => void;
+ cleanup: () => void;
unload: () => void;
onResult: (onResult: (e: VoskEvent) => void) => EventSubscription;
onFinalResult: (onFinalResult: (e: VoskEvent) => void) => EventSubscription;
diff --git a/src/index.tsx b/src/index.tsx
index d9f90c921d89b1b4d85e145443ed3376546a368a..29e4068dbd7500828a73145bd25497a52c9bf638 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -69,6 +69,15 @@ export default class Vosk {
VoskModule.stop();
};
+ stopOnly = () => {
+ VoskModule.stopOnly();
+ };
+
+ cleanup = () => {
+ this.cleanListeners();
+ VoskModule.cleanup();
+ };
+
unload = () => {
this.cleanListeners();
VoskModule.unload();

View File

@@ -1,30 +0,0 @@
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
index a8abd71833879201e3438b2fa51d712a311c4551..ffe9c2c6dfa5c703ba76b65d94d5dd6784102c19 100644
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
@@ -591,7 +591,7 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
// ignored.printStackTrace();
}
- RNFetchBlobFileResp rnFetchBlobFileResp = (RNFetchBlobFileResp) responseBody;
+ RNFetchBlobFileResp rnFetchBlobFileResp = new RNFetchBlobFileResp(responseBody);
if(rnFetchBlobFileResp != null && !rnFetchBlobFileResp.isDownloadComplete()){
callback.invoke("Download interrupted.", null);
diff --git a/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java b/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java
index 2470eef612308c15a89dfea5a1f16937469be29f..965f8becc195965907699182c764ec9e51811450 100644
--- a/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java
+++ b/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java
@@ -35,6 +35,12 @@ public class RNFetchBlobFileResp extends ResponseBody {
FileOutputStream ofStream;
boolean isEndMarkerReceived;
+ // ref: https://github.com/joltup/rn-fetch-blob/issues/490#issuecomment-990899440
+ public RNFetchBlobFileResp(ResponseBody body) {
+ super();
+ this.originalBody = body;
+ }
+
public RNFetchBlobFileResp(ReactApplicationContext ctx, String taskId, ResponseBody body, String path, boolean overwrite) throws IOException {
super();
this.rctContext = ctx;

View File

@@ -1,136 +0,0 @@
<mxfile host="Electron" modified="2023-04-29T09:42:39.598Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/21.2.1 Chrome/112.0.5615.87 Electron/24.1.2 Safari/537.36" etag="apmX4QvXCQymGu7gtKJn" version="21.2.1" type="device">
<diagram name="Page-1" id="5f0bae14-7c28-e335-631c-24af17079c00">
<mxGraphModel dx="1244" dy="759" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" background="none" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="93vzSs2z7RmF_nCAYhdf-1" value="Front end" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="465" y="120" width="170" height="60" as="geometry" />
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-4" value="Service" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="320" y="280" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-5" value="Service" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="490" y="280" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-6" value="Service" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="670" y="280" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-10" value="Model" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="320" y="430" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-11" value="Model" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="490" y="430" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-12" value="Model" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="670" y="430" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-14" value="SQLite database" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" vertex="1" parent="1">
<mxGeometry x="490" y="580" width="122.5" height="100" as="geometry" />
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-19" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-10" target="93vzSs2z7RmF_nCAYhdf-4">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-20" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-11" target="93vzSs2z7RmF_nCAYhdf-4">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-21" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-12" target="93vzSs2z7RmF_nCAYhdf-4">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-22" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-10" target="93vzSs2z7RmF_nCAYhdf-5">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-23" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-11" target="93vzSs2z7RmF_nCAYhdf-5">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-24" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-12" target="93vzSs2z7RmF_nCAYhdf-5">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-25" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-10" target="93vzSs2z7RmF_nCAYhdf-6">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-26" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-11" target="93vzSs2z7RmF_nCAYhdf-6">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-27" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-12" target="93vzSs2z7RmF_nCAYhdf-6">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-28" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-10" target="93vzSs2z7RmF_nCAYhdf-14">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-29" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-14" target="93vzSs2z7RmF_nCAYhdf-11">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-30" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-14" target="93vzSs2z7RmF_nCAYhdf-12">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="700" y="440" as="sourcePoint" />
<mxPoint x="750" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-31" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-4" target="93vzSs2z7RmF_nCAYhdf-1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="620" y="440" as="sourcePoint" />
<mxPoint x="670" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-32" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-5" target="93vzSs2z7RmF_nCAYhdf-1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="620" y="440" as="sourcePoint" />
<mxPoint x="670" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-33" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-6" target="93vzSs2z7RmF_nCAYhdf-1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="620" y="440" as="sourcePoint" />
<mxPoint x="670" y="390" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-37" value="BACKEND" style="swimlane;whiteSpace=wrap;html=1;swimlaneFillColor=none;shadow=0;" vertex="1" parent="1">
<mxGeometry x="280" y="230" width="560" height="480" as="geometry" />
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-38" value="JSON config file" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;" vertex="1" parent="1">
<mxGeometry x="910" y="420" width="80" height="100" as="geometry" />
</mxCell>
<mxCell id="93vzSs2z7RmF_nCAYhdf-39" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="93vzSs2z7RmF_nCAYhdf-37" target="93vzSs2z7RmF_nCAYhdf-38">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="830" y="480" as="sourcePoint" />
<mxPoint x="800" y="580" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -1,70 +0,0 @@
<mxfile host="Electron" modified="2023-04-29T10:24:42.580Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/21.2.1 Chrome/112.0.5615.87 Electron/24.1.2 Safari/537.36" etag="kcPEKHJGaBvNGFhEOF2g" version="21.2.1" type="device">
<diagram name="Page-1" id="5f0bae14-7c28-e335-631c-24af17079c00">
<mxGraphModel dx="1306" dy="797" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" background="none" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="t8PL5avYcYxuv0YEq-6K-7" value="Joplin Server" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="320" y="40" width="630" height="465" as="geometry">
<mxRectangle x="350" y="300" width="120" height="30" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-2" value="Server application" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="t8PL5avYcYxuv0YEq-6K-7">
<mxGeometry x="270" y="92.5" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-3" value="PostgreSQL" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" vertex="1" parent="t8PL5avYcYxuv0YEq-6K-7">
<mxGeometry x="190" y="262.5" width="140" height="110" as="geometry" />
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-5" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="t8PL5avYcYxuv0YEq-6K-7" source="t8PL5avYcYxuv0YEq-6K-3" target="t8PL5avYcYxuv0YEq-6K-2">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="280" y="232.5" as="sourcePoint" />
<mxPoint x="330" y="182.5" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-11" value="Note metadata,&lt;br&gt;user accounts, etc." style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="t8PL5avYcYxuv0YEq-6K-5">
<mxGeometry x="0.0586" y="1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-4" value="AWS S3" style="shape=cube;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;darkOpacity=0.05;darkOpacity2=0.1;" vertex="1" parent="t8PL5avYcYxuv0YEq-6K-7">
<mxGeometry x="430" y="277.5" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-6" value="Note and attachment&lt;br&gt;content" style="endArrow=classic;startArrow=classic;html=1;rounded=0;exitX=0;exitY=0;exitDx=50;exitDy=0;exitPerimeter=0;" edge="1" parent="t8PL5avYcYxuv0YEq-6K-7" source="t8PL5avYcYxuv0YEq-6K-4" target="t8PL5avYcYxuv0YEq-6K-2">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="390" y="242.5" as="sourcePoint" />
<mxPoint x="440" y="192.5" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-12" value="Reverse proxy" style="whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="t8PL5avYcYxuv0YEq-6K-7">
<mxGeometry x="70" y="97.5" width="75" height="75" as="geometry" />
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-13" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;" edge="1" parent="t8PL5avYcYxuv0YEq-6K-7" source="t8PL5avYcYxuv0YEq-6K-12" target="t8PL5avYcYxuv0YEq-6K-2">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="180" y="160" as="sourcePoint" />
<mxPoint x="230" y="110" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-18" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="t8PL5avYcYxuv0YEq-6K-7" source="t8PL5avYcYxuv0YEq-6K-14" target="t8PL5avYcYxuv0YEq-6K-2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-14" value="Env file (config)" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;" vertex="1" parent="t8PL5avYcYxuv0YEq-6K-7">
<mxGeometry x="540" y="90" width="60" height="85" as="geometry" />
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-8" value="Joplin Application (mobile, desktop, ...)" style="whiteSpace=wrap;html=1;aspect=fixed;" vertex="1" parent="1">
<mxGeometry x="40" y="110" width="130" height="130" as="geometry" />
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-9" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="t8PL5avYcYxuv0YEq-6K-8" target="t8PL5avYcYxuv0YEq-6K-12">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="130" y="270" as="sourcePoint" />
<mxPoint x="310" y="230" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="t8PL5avYcYxuv0YEq-6K-10" value="HTTP REST requests" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="t8PL5avYcYxuv0YEq-6K-9">
<mxGeometry x="0.0435" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -657,16 +657,6 @@ footer .bottom-links-row p {
font-size: 1.1em;
}
.language-switcher {
display: inline;
}
.language-switcher > button {
border: none;
background-color: transparent;
color: #0557ba;
}
/*****************************************************************
WHAT'S NEW PAGE
*****************************************************************/
@@ -790,7 +780,6 @@ 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;
}
@@ -958,41 +947,6 @@ 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
@@ -1014,15 +968,6 @@ 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;
}
}
/*****************************************************************

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

File diff suppressed because one or more lines are too long

5016
Assets/WebsiteAssets/js/bootstrap5.0.2.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -9,9 +9,7 @@ function getOs() {
function getFilename(path) {
if (!path) return '';
const s = path.split('/');
const urlWithParams = s.pop();
const s2 = urlWithParams.split('?');
return s2[0];
return s.pop();
}
function getMobileOs() {

View File

@@ -1,49 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 08 May 2023 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 08 May 2023 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 2.10]]></title><description><![CDATA[<p>Great news! Joplin 2.10 is here and we've made some amazing improvements and bug fixes, with a focus on the mobile app.</p>
<h1>New design for &quot;New note&quot; and &quot;New to-do&quot; buttons<a name="new-design-for-new-note-and-new-to-do-buttons" href="#new-design-for-new-note-and-new-to-do-buttons" class="heading-anchor">🔗</a></h1>
<p>We're excited to announce that we've made it even easier to create new notes and to-do lists with new designs for the &quot;New note&quot; and &quot;New to-do&quot; buttons.</p>
<p>If there is enough space, the button labels will be shown in full:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-new-note-1.png" alt=""></p>
<p>While for those who prefer a more narrow note list, only the button icons will be shown:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-new-note-2.png" alt=""></p>
<p>It's a small improvement, but we're confident it will make the app even more intuitive for new users.</p>
<h1>Fixes and improvements<a name="fixes-and-improvements" href="#fixes-and-improvements" class="heading-anchor">🔗</a></h1>
<p>This version includes 30 bug fixes and 16 general improvements. Let's dive into some of the highlights:</p>
<ul>
<li>
<p>Self Not Found and Pedro have been working tirelessly on improving the pasting of plain text in the application, and we're happy to say that there is now a brand new &quot;Paste as text&quot; option in the Edit menu.</p>
</li>
<li>
<p>For our amazing plugin developers out there, we have added a few new APIs and fixed a bug that was preventing certain plugins from starting. You can now get even more creative with your plugins!</p>
</li>
<li>
<p>Tao Klerks, has been hard at work fixing and improving the custom sort order of the note list. No more notes in the wrong position when you drop them! Plus, custom sort order is now synchronised too.</p>
</li>
<li>
<p>We've also fixed a few bugs with our Web Clipper, including an issue where certain pages wouldn't import their images. A new Web Clipper has also been released, so you can clip to your heart's content!</p>
</li>
</ul>
<p>Last but not least, we have modernised both the desktop and mobile application modules, just <a href="https://joplinapp.org/news/20221115-renovate/">as we previously announced</a>. Although these changes may not be visible to you, they required a lot of work! But the result is that our applications are now more stable and secure, and it will be easier to maintain them in the long run. We're using a tool called <a href="https://www.mend.io/free-developer-tools/renovate/">Renovate</a>, which will automatically propose package updates that we will review. In total, we've updated a whopping 633 packages so far!</p>
<h1>Android app is available in the Play Store<a name="android-app-is-available-in-the-play-store" href="#android-app-is-available-in-the-play-store" class="heading-anchor">🔗</a></h1>
<p>Our latest version, 2.10, is now back in the Play Store and ready for download! Although we had to skip 2.9 due to some of Google's requirements, we worked hard to ensure that our app complies with their standards, and we are excited to announce that we are back and better than ever! Our iOS version is also available, so you can continue to enjoy the app regardless of the platform you use.</p>
<h2>Biometrics support<a name="biometrics-support" href="#biometrics-support" class="heading-anchor">🔗</a></h2>
<p>To make your experience even more secure, our Android and iOS apps now support biometric unlock! With just a quick scan of your fingerprint or Face ID, you can unlock your app in no time. To enable this beta feature, just head over to the settings and click on &quot;Use biometrics to secure access to the app&quot;.</p>
<p>We've tested this feature thoroughly during prerelease, and have already fixed all known issues. However we still consider it as a beta feature for now, so if you run into any issues please let us know.</p>
<h2>Support for multiple profile<a name="support-for-multiple-profile" href="#support-for-multiple-profile" class="heading-anchor">🔗</a></h2>
<p>We're thrilled to announce that multiple profiles are now supported in our mobile app! To create a new profile, simply go to the Configuration screen and click on &quot;Manage profiles&quot; under the Tools section.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-biometrics-1.png" alt=""></p>
<p>From there, you can easily add or remove profiles as needed.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-biometrics-2.png" alt=""></p>
<p>Once multiple profiles are setup, you will see a new option in the sidebar to quickly toggle from one profile to another:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-biometrics-3.png" alt=""></p>
<p>Once you've set up multiple profiles, you can easily toggle between them using the new option in the sidebar. This feature is perfect for separating your personal and work notes into independent collections.</p>
<h2>Support for realtime search<a name="support-for-realtime-search" href="#support-for-realtime-search" class="heading-anchor">🔗</a></h2>
<p>Our mobile app now has an improved search function that performs text searches in real time! No more waiting for the search results to load, they'll appear instantly as you type.</p>
<h2>Improved filesystem sync performance<a name="improved-filesystem-sync-performance" href="#improved-filesystem-sync-performance" class="heading-anchor">🔗</a></h2>
<p>Thanks to the hard work of jd1378, the sync no longer freezes during filesystem synchronisation. We know how frustrating that can be, and we're thrilled to have solved this issue. Getting filesystem sync to work on Android is never easy due to the restrictions put in place by Google, especially since they frequently change, but we're committed to delivering the best possible experience for our Android users.</p>
]]></description><link>https://joplinapp.org/news/20230508-release-2-10/</link><guid isPermaLink="false">20230508-release-2-10</guid><pubDate>Mon, 08 May 2023 00:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.10</twitter-text></item><item><title><![CDATA[Joplin will participate in JdLL 2023!]]></title><description><![CDATA[<p>On 1 and 2 April 2023, we will have a stand for Joplin at the <a href="https://www.jdll.org/">Journées du Logiciel Libre</a> in Lyon, France. The JdLL has been taking place in Lyon for 24 years and is a popular open source conference in France. We had a stand in 2020 and 2021 but that was cancelled due to Covid, so this year is a first for Joplin!</p>
<p>Admission is free, so don't hesitate to come and meet us, exchange ideas and learn more about Joplin!</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230202-jdll.jpg" alt="Joplin at JdLL 2023"></p>
]]></description><link>https://joplinapp.org/news/20230302-jdll-2023/</link><guid isPermaLink="false">20230302-jdll-2023</guid><pubDate>Thu, 02 Mar 2023 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Introducing the "GitHub Actions Raw Log Viewer" extension for Chrome]]></title><description><![CDATA[<p>If you've ever used GitHub Actions, you will find that they provide by default a nice coloured output for the log. It looks good and it's even interactive! (You can click to collapse/expand blocks of text) But unfortunately it doesn't scale to large workflows, like we have for Joplin - the log can freeze and it will take forever to search for something. Indeed searching is done in &quot;real time&quot;... which mostly means it will freeze for a minute or two for each letter you type in the search box. Not great.</p>
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 16 Jan 2023 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 16 Jan 2023 00:00:00 GMT</pubDate><item><title><![CDATA[Introducing the "GitHub Actions Raw Log Viewer" extension for Chrome]]></title><description><![CDATA[<p>If you've ever used GitHub Actions, you will find that they provide by default a nice coloured output for the log. It looks good and it's even interactive! (You can click to collapse/expand blocks of text) But unfortunately it doesn't scale to large workflows, like we have for Joplin - the log can freeze and it will take forever to search for something. Indeed searching is done in &quot;real time&quot;... which mostly means it will freeze for a minute or two for each letter you type in the search box. Not great.</p>
<p>Thankfully GitHub provides an alternative access: the raw logs. This is much better because they will open as plain text, without any styling or JS magic, which means you can use the browser native search and it will be fast.</p>
<p>But now the problem is that raw logs look like this:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-ga-raw-log.png" alt="Raw log without extension"></p>
@@ -313,4 +268,35 @@
<p>- <a href="https://community.letsencrypt.org/t/issues-with-electron-and-expired-root/160991">Issue with Electron and expired root</a> on Let's Encrypt</p>
<p>- <a href="https://github.com/electron/electron/issues/31212">Let's Encrypt root CA isn't working properly</a> on Electron GitHub repository</p>
<p><strong>Update:</strong> I have implemented a temporary fix on Joplin Cloud which should solve the issue for now. If you're still having some issues please let me know. An updated desktop app will be available later on with a more permanent fix.</p>
]]></description><link>https://joplinapp.org/news/20210930-163458/</link><guid isPermaLink="false">20210930-163458</guid><pubDate>Thu, 30 Sep 2021 16:34:58 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
]]></description><link>https://joplinapp.org/news/20210930-163458/</link><guid isPermaLink="false">20210930-163458</guid><pubDate>Thu, 30 Sep 2021 16:34:58 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin 2.4 is available!]]></title><description><![CDATA[<p>Joplin 2.4 is now available on desktop, mobile and CLI. Here's what's new in this release:</p>
<h3>Sync Wizard Dialog<a name="sync-wizard-dialog" href="#sync-wizard-dialog" class="heading-anchor">🔗</a></h3>
<p>A new Sync Wizard Dialog has been added to simplify setting up sync on new clients.</p>
<p>The dialog shows the main sync targets, their differences, and makes it easy to choose one and start synchronising. This is mostly aimed at new users or those perhaps less technical. Those who are self hosting or using complex setups will still easily find what they need from a link on that dialog (or in Config &gt; Synchronisation like before).</p>
<p>Sync setup on mobile has been slightly improved too - now on a new client, instead of asking you to sync with Dropbox directly (which may not be what you want), it jumps to the Config &gt; Synchronisation section where you can select the sync target</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20210929-144036_0.png" alt=""></p>
<h3>Disable synchronisation<a name="disable-synchronisation" href="#disable-synchronisation" class="heading-anchor">🔗</a></h3>
<p>It's a small change but something that's been asked many time - it's now possible to disable synchronisation entirely by selecting &quot;None&quot; as a sync target. Previously that could be done in a hacky way, by selecting a non-configured sync target. Now it's clearer and easier to do.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20210929-144036_1.png" alt=""></p>
<h3>Add back support for deprecated plugins<a name="add-back-support-for-deprecated-plugins" href="#add-back-support-for-deprecated-plugins" class="heading-anchor">🔗</a></h3>
<p>Recently some plugins stopped working because deprecated plugin APIs had been removed. It had been planned for a long time but I suspect the warnings weren't visible enough so plugin developers didn't act on them, and as a result many plugins stopped working.</p>
<p>This is now fixed in the latest version. A selected number of plugins will have access to these old deprecated APIs, which means they will start working again. This was mainly affecting ambrt's plugins such as &quot;Convert Text To New Note&quot; or the popular &quot;Embed Search&quot; plugin.</p>
<h3>Add support for recommended plugins<a name="add-support-for-recommended-plugins" href="#add-support-for-recommended-plugins" class="heading-anchor">🔗</a></h3>
<p>As mentioned in an earlier post, we now support <a href="https://www.patreon.com/posts/introducing-in-55618802">recommended plugins</a>. These recommended plugins appear on top when searching and are identified by a small crown.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20210929-144036_2.png" alt=""></p>
<h3>End to End Encryption improvements<a name="end-to-end-encryption-improvements" href="#end-to-end-encryption-improvements" class="heading-anchor">🔗</a></h3>
<p>Like most recent releases, v2.4 includes a few improvement to the End to End Encryption (E2EE) system. The goal is to make it easier to use, to make it more reliable and to support the future use case of sharing encrypted notebooks or notes.</p>
<p>One important change is the support for a master password. This single password will be responsible to encrypt various keys, including some that will be automatically generated. Thanks to this, it won't be necessary to ask to enter a new password every time a key needs to be encrypted, since the master password can be used. It will also be easier to manage since you'll only have one password to remember instead of a different one for each notebook you might have shared.</p>
<p>Finally, it's now possible to disable a master key. What it means is that it will no longer show up in the list of master keys, and will also no longer generate a warning asking you to enter the password. In some case you might have forgotten it and no longer need it key, so you can now disable it.</p>
<h3>Custom CSS<a name="custom-css" href="#custom-css" class="heading-anchor">🔗</a></h3>
<p>This version also introduces a few internal change to better support custom CSS. In particular the colours now come from a CSS file, which could potentially be overridden, and new UI elements are styled using stylesheets, which likewise could be overridden.</p>
<p>Those are just first steps, but eventually these changes will make it easier to style the UI and create new themes.</p>
<h3>Bug fixes<a name="bug-fixes" href="#bug-fixes" class="heading-anchor">🔗</a></h3>
<p>This release also includes about 30 various bug fixes and improvements.</p>
<p>A notable one is a fix for GotoAnything, which recently wasn't working on first try.</p>
<p>The plugin screen has also been improved so that search works even when GitHub is down or blocked, as it is in China in particular.</p>
]]></description><link>https://joplinapp.org/news/20210929-144036/</link><guid isPermaLink="false">20210929-144036</guid><pubDate>Wed, 29 Sep 2021 14:40:36 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Introducing recommended plugins in the next Joplin version]]></title><description><![CDATA[<p>A common request from new users is how to know which plugin is safe to install or not. In fact probably all of them are safe but as a new user that's not necessarily easy to know. So to help with this, the next version of Joplin will support recommended plugins - those will be plugins that meet our standards of quality and performance, and they will be indicated by a small crown tag inside the plugin box. Recommended plugins will also appear on top when searching.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20210901-113415_0.png" alt=""></p>
<p>For now, since we don't have a review process, the recommended plugins are those developed by the Joplin team and frequent contributors, because we know those are safe to use.</p>
<p>Later we might have a review process and add more recommended plugins. That being said, in the meantime even if a plugin is not marked as recommended, there's a good chance it is still safe and have good performance too. Often you can search for it on the forum and if it's active with many users commenting, you're most likely good to go.</p>
<p>But if there's any doubt, the recommended tag is a good way to be sure.</p>
]]></description><link>https://joplinapp.org/news/20210901-113415/</link><guid isPermaLink="false">20210901-113415</guid><pubDate>Wed, 01 Sep 2021 11:34:15 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>

View File

@@ -421,7 +421,7 @@
</div>
<script
src="{{jsBaseUrl}}/bootstrap5.0.2.bundle.min.js"
src="{{jsBaseUrl}}/bootstrap5.0.2.min.js"
rel="preload"
as="script"
></script>

View File

@@ -85,11 +85,6 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
{{> footer}}
</div>
<script
src="{{jsBaseUrl}}/bootstrap5.0.2.bundle.min.js"
rel="preload"
as="script"
></script>
<script src="{{{assetUrls.js.script}}}"></script>
{{> analytics}}

View File

@@ -17,21 +17,6 @@
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
<a href="{{forumUrl}}" class="fw500">Forum</a>
<a href="{{baseUrl}}/cn/" class="fw500">中文</a>
<!--
<div class="dropdown language-switcher">
<button class="fw500" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
Language
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</div>
-->
{{#showJoplinCloudLinks}}
{{> joplinCloudButton}}
{{/showJoplinCloudLinks}}
@@ -39,8 +24,7 @@
</div>
<div class="col-9 text-right d-block d-md-none navbar-mobile-content">
{{> twitterLink}}
<a href="{{baseUrl}}/cn/" class="fw500 chinese-page-link">中文</a>
{{> joplinCloudButton}}
<a href="{{baseUrl}}/cn/" class="fw500">中文</a>
{{> supportButton}}
<span class="pointer"

View File

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

View File

@@ -1 +1 @@
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500 twitter-link"><i class="fab fa-twitter"></i></a>
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500"><i class="fab fa-twitter"></i></a>

View File

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

View File

@@ -134,16 +134,10 @@ else
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION:-no version} installed."
fi
# Check if it's an update or a new install
DOWNLOAD_TYPE="New"
if [[ -f ~/.joplin/Joplin.AppImage ]]; then
DOWNLOAD_TYPE="Update"
fi
#-----------------------------------------------------
print 'Downloading Joplin...'
TEMP_DIR=$(mktemp -d)
wget -O "${TEMP_DIR}/Joplin.AppImage" "https://objects.joplinusercontent.com/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage?source=LinuxInstallScript&type=$DOWNLOAD_TYPE"
wget -O "${TEMP_DIR}/Joplin.AppImage" "https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage"
wget -O "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
#-----------------------------------------------------

View File

@@ -1,5 +1,5 @@
<!-- DONATELINKS -->
[![Donate using PayPal](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/Donate-PayPal-green.svg)](https://www.paypal.com/donate/?business=E8JMYD2LQ8MMA&no_recurring=0&item_name=I+rely+on+donations+to+maintain+and+improve+the+Joplin+open+source+project.+Thank+you+for+your+help+-+it+makes+a+difference%21&currency_code=EUR) [![Sponsor on GitHub](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/GitHub-Badge.svg)](https://github.com/sponsors/laurent22/) [![Become a patron](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/Patreon-Badge.svg)](https://www.patreon.com/joplin) [![Donate using IBAN](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/Donate-IBAN.svg)](https://joplinapp.org/donate/#donations)
[![Donate using PayPal](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=E8JMYD2LQ8MMA&lc=GB&item_name=Joplin+Development&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) [![Sponsor on GitHub](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/GitHub-Badge.svg)](https://github.com/sponsors/laurent22/) [![Become a patron](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/Patreon-Badge.svg)](https://www.patreon.com/joplin) [![Donate using IBAN](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/badges/Donate-IBAN.svg)](https://joplinapp.org/donate/#donations)
<!-- DONATELINKS -->
<img width="64" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png" align="left" /> **Joplin** is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](#markdown).
@@ -22,11 +22,11 @@ Three types of applications are available: for **desktop** (Windows, macOS and L
Operating System | Download
---|---
Windows (32 and 64-bit) | <a href='https://objects.joplinusercontent.com/v2.10.18/Joplin-Setup-2.10.18.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
macOS | <a href='https://objects.joplinusercontent.com/v2.10.18/Joplin-2.10.18.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
Linux | <a href='https://objects.joplinusercontent.com/v2.10.18/Joplin-2.10.18.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v2.9.17/Joplin-Setup-2.9.17.exe'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v2.9.17/Joplin-2.9.17.dmg'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v2.9.17/Joplin-2.9.17.AppImage'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
**On Windows**, you may also use the <a href='https://objects.joplinusercontent.com/v2.10.18/JoplinPortable.exe?source=JoplinWebsite&type=New'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Windows**, you may also use the <a href='https://github.com/laurent22/joplin/releases/download/v2.9.17/JoplinPortable.exe'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Linux**, the recommended way is to use the following installation script as it will handle the desktop icon too:
@@ -36,7 +36,7 @@ Linux | <a href='https://objects.joplinusercontent.com/v2.10.18/Joplin-2.10.18.A
Operating System | Download | Alt. Download
---|---|---
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://objects.joplinusercontent.com/android-v2.9.8/joplin-v2.9.8.apk?source=JoplinWebsite&type=New) [32-bit](https://objects.joplinusercontent.com/android-v2.9.8/joplin-v2.9.8-32bit.apk?source=JoplinWebsite&type=New)
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.9.8/joplin-v2.9.8.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.9.8/joplin-v2.9.8-32bit.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeIOS.png'/></a> | -
## Terminal application
@@ -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://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;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://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&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-github&amp;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,11 +72,14 @@ 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/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) | |
| <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) | | | |
<!-- SPONSORS-GITHUB -->
<!-- TOC -->
@@ -125,11 +128,9 @@ A community maintained list of these distributions can be found here: [Unofficia
- [How to build the apps](https://github.com/laurent22/joplin/blob/dev/BUILD.md)
- [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)
- [Note History spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/history.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)
- [Overall Architecture spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/architecture.md)
- [Plugin Architecture spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/plugins.md)
- [Search Sorting spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/search_sorting.md)
- [E2EE: Technical spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/e2ee.md)
@@ -504,7 +505,6 @@ 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
@@ -530,44 +530,44 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 79%
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 22%
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 57%
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 80%
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 23%
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 58%
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 45%
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | Fejby | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 77%
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [MrKanister](mailto:pueblos_spatulas@aleeas.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 44%
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 99%
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 25%
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 100%
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 99%
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 29%
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 80%
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 77%
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 78%
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 81%
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 78%
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 79%
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 88%
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 87%
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 88%
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 55%
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [X3NO](mailto:X3NO@disroot.org) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Douglas Leão](mailto:djlsplays@gmail.com) | 87%
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 72%
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 50%
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [X3NO](mailto:X3NO@disroot.org) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Renato Nunes Bastos](mailto:rnbastos@gmail.com) | 88%
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 73%
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 51%
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 80%
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 36%
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 77%
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 78%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 72%
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 87%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Dmitriy Q](mailto:krotesk@mail.ru) | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 88%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Dmitriy Q](mailto:krotesk@mail.ru) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 65%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [wh201906](mailto:wh201906@yandex.com) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [wh201906](mailto:wh201906@yandex.com) | 97%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 89%

File diff suppressed because one or more lines are too long

View File

@@ -1,52 +1,26 @@
const gulp = require('gulp');
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 utils = require('./packages/tools/gulp/utils');
const tasks = {
updateIgnoredTypeScriptBuild: require('./packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
buildCommandIndex: require('./packages/tools/gulp/tasks/buildCommandIndex'),
completePublishAll: {
fn: async () => {
await execCommand('git', ['add', '-A']);
await execCommand('git', ['commit', '-m', 'Releasing sub-packages']);
await utils.execCommandVerbose('git', ['add', '-A']);
await utils.execCommandVerbose('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 execCommand('lerna', ['publish', 'from-package', '-y', '--no-verify-access']);
await utils.execCommandVerbose('lerna', ['publish', 'from-package', '-y', '--no-verify-access']);
await execCommand('yarn', ['install']);
await execCommand('git', ['add', '-A']);
await execCommand('git', ['commit', '-m', 'Lock file']);
await utils.execCommandVerbose('yarn', ['install']);
await utils.execCommandVerbose('git', ['add', '-A']);
await utils.execCommandVerbose('git', ['commit', '-m', 'Lock file']);
await execCommand('git', ['push']);
await utils.execCommandVerbose('git', ['push']);
},
},
build: {
@@ -59,14 +33,12 @@ const tasks = {
// faster, especially when having to rebuild after adding a
// dependency.
if (process.env.BUILD_SEQUENCIAL === '1') {
await execCommand('yarn', ['run', 'buildSequential']);
await utils.execCommandVerbose('yarn', ['run', 'buildSequential']);
} else {
await execCommand('yarn', ['run', 'buildParallel']);
await utils.execCommandVerbose('yarn', ['run', 'buildParallel']);
}
},
},
};
for (const taskName in tasks) {
gulp.task(taskName, tasks[taskName].fn);
}
utils.registerGulpTasks(gulp, tasks);

View File

@@ -107,10 +107,7 @@
".eslintignore": true,
".gitignore": true,
".vscode/*": true,
".yarn/cache": true,
".yarn/install-state.gz": true,
".yarn/plugins": true,
".yarn/releases": true,
".yarn": true,
"*.sublime-workspace": true,
"**/_mydocs": true,
"**/_mydocs/EnexSamples/*.enex": true,
@@ -332,7 +329,6 @@
"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,

View File

@@ -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": "node packages/tools/gulp/tasks/buildCommandIndexRun.js",
"buildCommandIndex": "gulp buildCommandIndex",
"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": "node packages/tools/gulp/tasks/updateIgnoredTypeScriptBuildRun.js",
"updateIgnored": "gulp updateIgnoredTypeScriptBuild",
"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,7 +64,6 @@
}
},
"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",
@@ -72,16 +71,14 @@
"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",
"execa": "5.1.1",
"fs-extra": "11.1.1",
"fs-extra": "11.1.0",
"glob": "8.1.0",
"gulp": "4.0.2",
"husky": "3.1.0",
"lerna": "3.22.1",
"lint-staged": "13.2.1",
"lint-staged": "13.1.2",
"madge": "6.0.0",
"npm-package-json-lint": "6.4.0",
"typedoc": "0.17.8",
@@ -91,12 +88,10 @@
"@types/fs-extra": "9.0.13",
"http-server": "14.1.1",
"node-gyp": "9.3.1",
"nodemon": "2.0.22"
"nodemon": "2.0.20"
},
"packageManager": "yarn@3.3.1",
"resolutions": {
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
"rn-fetch-blob@0.12.0": "patch:rn-fetch-blob@npm%3A0.12.0#./.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch",
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch"
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch"
}
}

View File

@@ -6,14 +6,14 @@ interface LinkStoreEntry {
}
class LinkSelector {
private noteId_: string;
private scrollTop_: number;
private renderedText_: string;
private currentLinkIndex_: number;
private linkStore_: LinkStoreEntry[];
private linkRegex_: RegExp;
noteId_: string;
scrollTop_: number;
renderedText_: string;
currentLinkIndex_: number;
linkStore_: LinkStoreEntry[];
linkRegex_: RegExp;
public constructor() {
constructor() {
this.noteId_ = null;
this.scrollTop_ = null; // used so 'o' won't open unhighlighted link after scrolling
this.renderedText_ = null;
@@ -22,22 +22,22 @@ class LinkSelector {
this.linkRegex_ = /http:\/\/[0-9.]+:[0-9]+\/[0-9]+/g;
}
public get link(): string | null {
get link(): string | null {
if (this.currentLinkIndex_ === null) return null;
return this.linkStore_[this.currentLinkIndex_].link;
}
public get noteX(): number | null {
get noteX(): number | null {
if (this.currentLinkIndex_ === null) return null;
return this.linkStore_[this.currentLinkIndex_].noteX;
}
public get noteY(): number | null {
get noteY(): number | null {
if (this.currentLinkIndex_ === null) return null;
return this.linkStore_[this.currentLinkIndex_].noteY;
}
public findLinks(renderedText: string): LinkStoreEntry[] {
findLinks(renderedText: string): LinkStoreEntry[] {
const newLinkStore: LinkStoreEntry[] = [];
const lines: string[] = renderedText.split('\n');
for (let i = 0; i < lines.length; i++) {
@@ -56,19 +56,19 @@ class LinkSelector {
return newLinkStore;
}
public updateText(renderedText: string): void {
updateText(renderedText: string): void {
this.currentLinkIndex_ = null;
this.renderedText_ = renderedText;
this.linkStore_ = this.findLinks(this.renderedText_);
}
public updateNote(textWidget: any): void {
updateNote(textWidget: any): void {
this.noteId_ = textWidget.noteId;
this.scrollTop_ = textWidget.scrollTop_;
this.updateText(textWidget.renderedText_);
}
public scrollWidget(textWidget: any): void {
scrollWidget(textWidget: any): void {
if (this.currentLinkIndex_ === null) return;
const noteY = this.linkStore_[this.currentLinkIndex_].noteY;
@@ -93,7 +93,7 @@ class LinkSelector {
return;
}
public changeLink(textWidget: any, offset: number): void | null {
changeLink(textWidget: any, offset: number): void | null {
if (textWidget.noteId !== this.noteId_) {
this.updateNote(textWidget);
this.changeLink(textWidget, offset);
@@ -123,7 +123,7 @@ class LinkSelector {
return;
}
public openLink(textWidget: any): void {
openLink(textWidget: any): void {
if (textWidget.noteId !== this.noteId_) return;
if (textWidget.renderedText_ !== this.renderedText_) return;
if (textWidget.scrollTop_ !== this.scrollTop_) return;

View File

@@ -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/utils');
const { splitCommandString } = require('@joplin/lib/string-utils.js');
const { reg } = require('@joplin/lib/registry.js');
const { _ } = require('@joplin/lib/locale');
const shim = require('@joplin/lib/shim').default;

View File

@@ -9,8 +9,7 @@ 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 } = require('@joplin/utils');
const { splitCommandBatch } = require('@joplin/lib/string-utils');
const { splitCommandString, splitCommandBatch } = require('@joplin/lib/string-utils');
const { _ } = require('@joplin/lib/locale');
const fs = require('fs-extra');
const { cliUtils } = require('./cli-utils.js');

View File

@@ -7,80 +7,80 @@ export default class BaseCommand {
protected prompt_: any = null;
protected dispatcher_: any;
public usage(): string {
usage(): string {
throw new Error('Usage not defined');
}
public encryptionCheck(item: any) {
encryptionCheck(item: any) {
if (item && item.encryption_applied) throw new Error(_('Cannot change encrypted item'));
}
public description() {
description() {
throw new Error('Description not defined');
}
public async action(_args: any) {
async action(_args: any) {
throw new Error('Action not defined');
}
public compatibleUis() {
compatibleUis() {
return ['cli', 'gui'];
}
public supportsUi(ui: string) {
supportsUi(ui: string) {
return this.compatibleUis().indexOf(ui) >= 0;
}
public options(): any[] {
options(): any[] {
return [];
}
public hidden() {
hidden() {
return false;
}
public enabled() {
enabled() {
return true;
}
public cancellable() {
cancellable() {
return false;
}
public async cancel() {}
async cancel() {}
public name() {
name() {
const r = this.usage().split(' ');
return r[0];
}
public setDispatcher(fn: Function) {
setDispatcher(fn: Function) {
this.dispatcher_ = fn;
}
public dispatch(action: any) {
dispatch(action: any) {
if (!this.dispatcher_) throw new Error('Dispatcher not defined');
return this.dispatcher_(action);
}
public setStdout(fn: Function) {
setStdout(fn: Function) {
this.stdout_ = fn;
}
public stdout(text: string) {
stdout(text: string) {
if (this.stdout_) this.stdout_(text);
}
public setPrompt(fn: Function) {
setPrompt(fn: Function) {
this.prompt_ = fn;
}
public async prompt(message: string, options: any = null) {
async prompt(message: string, options: any = null) {
if (!this.prompt_) throw new Error('Prompt is undefined');
return await this.prompt_(message, options);
}
public metadata() {
metadata() {
return {
name: this.name(),
usage: this.usage(),
@@ -89,7 +89,7 @@ export default class BaseCommand {
};
}
public logger() {
logger() {
return reg.logger();
}
}

View File

@@ -12,15 +12,15 @@ const imageType = require('image-type');
const readChunk = require('read-chunk');
class Command extends BaseCommand {
public usage() {
usage() {
return 'e2ee <command> [path]';
}
public description() {
description() {
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.'); // `generate-ppk`
}
public options() {
options() {
return [
// This is here mostly for testing - shouldn't be used
['-p, --password <password>', 'Use this password as master password (For security reasons, it is not recommended to use this option).'],
@@ -30,7 +30,7 @@ class Command extends BaseCommand {
];
}
public async action(args: any) {
async action(args: any) {
const options = args.options;
const askForMasterKey = async (error: any) => {

View File

@@ -1,6 +1,6 @@
const fs = require('fs-extra');
const BaseCommand = require('./base-command').default;
const { splitCommandString } = require('@joplin/utils');
const { splitCommandString } = require('@joplin/lib/string-utils.js');
const uuid = require('@joplin/lib/uuid').default;
const { app } = require('./app.js');
const { _ } = require('@joplin/lib/locale');

View File

@@ -6,22 +6,22 @@ import Folder from '@joplin/lib/models/Folder';
import { FolderEntity } from '@joplin/lib/services/database/types';
class Command extends BaseCommand {
public usage() {
usage() {
return 'mkbook <new-notebook>';
}
public description() {
description() {
return _('Creates a new notebook.');
}
public options() {
options() {
return [
['-p, --parent <parent-notebook>', _('Create a new notebook under a parent notebook.')],
];
}
// validDestinationFolder check for presents and ambiguous folders
public async validDestinationFolder(targetFolder: string) {
async validDestinationFolder(targetFolder: string) {
const destinationFolder = await app().loadItem(BaseModel.TYPE_FOLDER, targetFolder);
if (!destinationFolder) {
@@ -36,14 +36,14 @@ class Command extends BaseCommand {
return destinationFolder;
}
public async saveAndSwitchFolder(newFolder: FolderEntity) {
async saveAndSwitchFolder(newFolder: FolderEntity) {
const folder = await Folder.save(newFolder, { userSideValidation: true });
app().switchCurrentFolder(folder);
}
public async action(args: any) {
async action(args: any) {
const targetFolder = args.options.parent;
const newFolder: FolderEntity = {

View File

@@ -23,19 +23,19 @@ function settingTypeToSchemaType(type: SettingItemType): string {
}
class Command extends BaseCommand {
public usage() {
usage() {
return 'settingschema <file>';
}
public description() {
description() {
return 'Build the setting schema file';
}
public enabled() {
enabled() {
return false;
}
public async action(args: any) {
async action(args: any) {
const schema: Record<string, any> = {
title: 'JSON schema for Joplin setting files',
'$id': Setting.schemaUrl,

View File

@@ -21,15 +21,15 @@ class Command extends BaseCommand {
private releaseLockFn_: Function = null;
private oneDriveApiUtils_: any = null;
public usage() {
usage() {
return 'sync';
}
public description() {
description() {
return _('Synchronises with remote storage.');
}
public options() {
options() {
return [
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
['--upgrade', _('Upgrade the sync target to the latest version.')],
@@ -45,7 +45,7 @@ class Command extends BaseCommand {
return locker.check(filePath);
}
public async doAuth() {
async doAuth() {
const syncTarget = reg.syncTarget(this.syncTargetId_);
const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_);
@@ -89,18 +89,18 @@ class Command extends BaseCommand {
return false;
}
public cancelAuth() {
cancelAuth() {
if (this.oneDriveApiUtils_) {
this.oneDriveApiUtils_.cancelOAuthDance();
return;
}
}
public doingAuth() {
doingAuth() {
return !!this.oneDriveApiUtils_;
}
public async action(args: any) {
async action(args: any) {
this.releaseLockFn_ = null;
// Lock is unique per profile/database
@@ -238,7 +238,7 @@ class Command extends BaseCommand {
cleanUp();
}
public async cancel() {
async cancel() {
if (this.doingAuth()) {
this.cancelAuth();
return;
@@ -263,7 +263,7 @@ class Command extends BaseCommand {
this.syncTargetId_ = null;
}
public cancellable() {
cancellable() {
return true;
}
}

View File

@@ -18,19 +18,19 @@ function itemCount(args: any) {
}
class Command extends BaseCommand {
public usage() {
usage() {
return 'testing <command> [arg0]';
}
public description() {
description() {
return 'testing';
}
public enabled() {
enabled() {
return false;
}
public options(): any[] {
options(): any[] {
return [
['--folder-count <count>', 'Folders to create'],
['--note-count <count>', 'Notes to create'],
@@ -40,7 +40,7 @@ class Command extends BaseCommand {
];
}
public async action(args: any) {
async action(args: any) {
const { command, options } = args;
if (command === 'populate') {

View File

@@ -5,7 +5,7 @@ const stripAnsi = require('strip-ansi');
const { handleAutocompletion } = require('../autocompletion.js');
export default class StatusBarWidget extends BaseWidget {
public constructor() {
constructor() {
super();
this.promptState_ = null;
@@ -14,20 +14,20 @@ export default class StatusBarWidget extends BaseWidget {
this.items_ = [];
}
public get name() {
get name() {
return 'statusBar';
}
public get canHaveFocus() {
get canHaveFocus() {
return false;
}
public setItemAt(index: number, text: string) {
setItemAt(index: number, text: string) {
this.items_[index] = stripAnsi(text).trim();
this.invalidate();
}
public async prompt(initialText = '', promptString: any = null, options: any = null) {
async prompt(initialText = '', promptString: any = null, options: any = null) {
if (this.promptState_) throw new Error('Another prompt already active');
if (promptString === null) promptString = ':';
if (options === null) options = {};
@@ -53,15 +53,15 @@ export default class StatusBarWidget extends BaseWidget {
return this.promptState_.promise;
}
public get promptActive() {
get promptActive() {
return !!this.promptState_;
}
public get history() {
get history() {
return this.history_;
}
public resetCursor() {
resetCursor() {
if (!this.promptActive) return;
if (!this.inputEventEmitter_) return;
@@ -70,7 +70,7 @@ export default class StatusBarWidget extends BaseWidget {
this.term.moveTo(this.absoluteInnerX + termutils.textLength(this.promptState_.promptString) + this.inputEventEmitter_.getInput().length, this.absoluteInnerY);
}
public render() {
render() {
super.render();
const doSaveCursor = !this.promptActive;

View File

@@ -35,7 +35,7 @@ export default class PluginRunner extends BasePluginRunner {
private eventHandlers_: EventHandlers = {};
private activeSandboxCalls_: any = {};
public constructor() {
constructor() {
super();
this.eventHandler = this.eventHandler.bind(this);
@@ -64,7 +64,7 @@ export default class PluginRunner extends BasePluginRunner {
};
}
public async run(plugin: Plugin, sandbox: Global): Promise<void> {
async run(plugin: Plugin, sandbox: Global): Promise<void> {
return new Promise((resolve: Function, reject: Function) => {
const onStarted = () => {
plugin.off('started', onStarted);

View File

@@ -30,36 +30,34 @@
2019,
2020,
2021,
2022,
2023
2022
],
"owner": "Laurent Cozic"
},
"version": "2.11.0",
"version": "2.10.3",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"
},
"dependencies": {
"@joplin/lib": "~2.11",
"@joplin/renderer": "~2.11",
"@joplin/utils": "~2.11",
"aws-sdk": "2.1340.0",
"@joplin/lib": "~2.10",
"@joplin/renderer": "~2.10",
"aws-sdk": "2.1290.0",
"chalk": "4.1.2",
"compare-version": "0.1.2",
"fs-extra": "11.1.1",
"fs-extra": "11.1.0",
"html-entities": "1.4.0",
"image-type": "3.1.0",
"keytar": "7.9.0",
"md5": "2.3.0",
"node-rsa": "1.1.1",
"open": "8.4.2",
"open": "8.4.1",
"proper-lockfile": "4.1.2",
"read-chunk": "2.1.0",
"server-destroy": "1.0.1",
"sharp": "0.32.0",
"sharp": "0.31.3",
"sprintf-js": "1.1.2",
"sqlite3": "5.1.6",
"sqlite3": "5.1.4",
"string-padding": "1.0.2",
"strip-ansi": "6.0.1",
"tcp-port-used": "1.0.2",
@@ -70,7 +68,7 @@
"yargs-parser": "21.1.1"
},
"devDependencies": {
"@joplin/tools": "~2.11",
"@joplin/tools": "~2.10",
"@types/fs-extra": "9.0.13",
"@types/jest": "29.2.6",
"@types/node": "18.11.18",

View File

@@ -1 +0,0 @@
<div class="jop-noMdConv"><svg class="jop-noMdConv"><style class="jop-noMdConv">&lt;/svg>&lt;iframe srcdoc="&lt;script>top.require('child_process').execSync('calc')&lt;/script>">&lt;/iframe>&lt;/div>

View File

@@ -1 +0,0 @@
<div><svg><style></svg><iframe srcdoc="<script>top.require('child_process').execSync('calc')</script>"></iframe></div>

View File

@@ -1 +0,0 @@
<a href="#" class="jop-noMdConv">XSS</a>

View File

@@ -1 +0,0 @@
<a data-from-md="" href="javascript:top.require('child_process').execSync('open -a Calculator')">XSS</a>

View File

@@ -1,5 +1,3 @@
/* 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';

View File

@@ -47,11 +47,9 @@ 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', '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);
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);
}));
});

View File

@@ -19,7 +19,7 @@
import * as fs from 'fs-extra';
import { homedir } from 'os';
import { execCommand } from '@joplin/utils';
import { execCommand2 } from '@joplin/tools/tool-utils';
import { chdir } from 'process';
const minUserNum = 1;
@@ -66,7 +66,7 @@ const processUser = async (userNum: number) => {
await chdir(cliDir);
await execCommand(['yarn', 'run', 'start-no-build', '--', '--profile', profileDir, 'batch', commandFile]);
await execCommand2(['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 execCommand(['yarn', 'run', 'build']);
await execCommand2(['yarn', 'run', 'build']);
const focusUserNum = 0;

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper [DEV]",
"version": "2.11.2",
"version": "2.10.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self'; object-src 'self'",

View File

@@ -126,4 +126,4 @@
"react-app"
]
}
}
}

View File

@@ -33,7 +33,7 @@ export default class ElectronAppWrapper {
private pluginWindows_: PluginWindows = {};
private initialCallbackUrl_: string = null;
public constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean, initialCallbackUrl: string) {
constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean, initialCallbackUrl: string) {
this.electronApp_ = electronApp;
this.env_ = env;
this.isDebugMode_ = isDebugMode;
@@ -41,31 +41,31 @@ export default class ElectronAppWrapper {
this.initialCallbackUrl_ = initialCallbackUrl;
}
public electronApp() {
electronApp() {
return this.electronApp_;
}
public setLogger(v: Logger) {
setLogger(v: Logger) {
this.logger_ = v;
}
public logger() {
logger() {
return this.logger_;
}
public window() {
window() {
return this.win_;
}
public env() {
env() {
return this.env_;
}
public initialCallbackUrl() {
initialCallbackUrl() {
return this.initialCallbackUrl_;
}
public createWindow() {
createWindow() {
// Set to true to view errors if the application does not start
const debugEarlyBugs = this.env_ === 'dev' || this.isDebugMode_;
@@ -236,11 +236,11 @@ export default class ElectronAppWrapper {
}
}
public registerPluginWindow(pluginId: string, window: any) {
registerPluginWindow(pluginId: string, window: any) {
this.pluginWindows_[pluginId] = window;
}
public async waitForElectronAppReady() {
async waitForElectronAppReady() {
if (this.electronApp().isReady()) return Promise.resolve();
return new Promise<void>((resolve) => {
@@ -253,25 +253,25 @@ export default class ElectronAppWrapper {
});
}
public quit() {
quit() {
this.electronApp_.quit();
}
public exit(errorCode = 0) {
exit(errorCode = 0) {
this.electronApp_.exit(errorCode);
}
public trayShown() {
trayShown() {
return !!this.tray_;
}
// This method is used in macOS only to hide the whole app (and not just the main window)
// including the menu bar. This follows the macOS way of hiding an app.
public hide() {
hide() {
this.electronApp_.hide();
}
public buildDir() {
buildDir() {
if (this.buildDir_) return this.buildDir_;
let dir = `${__dirname}/build`;
if (!fs.pathExistsSync(dir)) {
@@ -283,7 +283,7 @@ export default class ElectronAppWrapper {
return dir;
}
private trayIconFilename_() {
trayIconFilename_() {
let output = '';
if (process.platform === 'darwin') {
@@ -298,7 +298,7 @@ export default class ElectronAppWrapper {
}
// Note: this must be called only after the "ready" event of the app has been dispatched
public createTray(contextMenu: any) {
createTray(contextMenu: any) {
try {
this.tray_ = new Tray(`${this.buildDir()}/icons/${this.trayIconFilename_()}`);
this.tray_.setToolTip(this.electronApp_.name);
@@ -312,13 +312,13 @@ export default class ElectronAppWrapper {
}
}
public destroyTray() {
destroyTray() {
if (!this.tray_) return;
this.tray_.destroy();
this.tray_ = null;
}
public ensureSingleInstance() {
ensureSingleInstance() {
if (this.env_ === 'dev') return false;
const gotTheLock = this.electronApp_.requestSingleInstanceLock();
@@ -347,7 +347,7 @@ export default class ElectronAppWrapper {
return false;
}
public async start() {
async start() {
// Since we are doing other async things before creating the window, we might miss
// the "ready" event. So we use the function below to make sure that the app is ready.
await this.waitForElectronAppReady();
@@ -375,7 +375,7 @@ export default class ElectronAppWrapper {
});
}
public async openCallbackUrl(url: string) {
async openCallbackUrl(url: string) {
this.win_.webContents.send('asynchronous-message', 'openCallbackUrl', {
url: url,
});

View File

@@ -3,7 +3,7 @@ import appReducer, { createAppDefaultState } from './app.reducer';
describe('app.reducer', () => {
it('should handle DIALOG_OPEN', async () => {
it('DIALOG_OPEN', async () => {
const state: AppState = createAppDefaultState({}, {});
let newState = appReducer(state, {

View File

@@ -21,7 +21,7 @@ export class Bridge {
private electronWrapper_: ElectronAppWrapper;
private lastSelectedPaths_: LastSelectedPath;
public constructor(electronWrapper: ElectronAppWrapper) {
constructor(electronWrapper: ElectronAppWrapper) {
this.electronWrapper_ = electronWrapper;
this.lastSelectedPaths_ = {
file: null,
@@ -29,11 +29,11 @@ export class Bridge {
};
}
public electronApp() {
electronApp() {
return this.electronWrapper_;
}
public electronIsDev() {
electronIsDev() {
return !this.electronApp().electronApp().isPackaged;
}
@@ -60,18 +60,14 @@ export class Bridge {
return `${__dirname}/vendor`;
}
public env() {
env() {
return this.electronWrapper_.env();
}
public processArgv() {
processArgv() {
return process.argv;
}
public getLocale = () => {
return this.electronApp().electronApp().getLocale();
};
// Applies to electron-context-menu@3:
//
// For now we have to disable spell checking in non-editor text
@@ -118,44 +114,44 @@ export class Bridge {
});
}
public window() {
window() {
return this.electronWrapper_.window();
}
public showItemInFolder(fullPath: string) {
showItemInFolder(fullPath: string) {
return require('electron').shell.showItemInFolder(toSystemSlashes(fullPath));
}
public newBrowserWindow(options: any) {
newBrowserWindow(options: any) {
return new BrowserWindow(options);
}
public windowContentSize() {
windowContentSize() {
if (!this.window()) return { width: 0, height: 0 };
const s = this.window().getContentSize();
return { width: s[0], height: s[1] };
}
public windowSize() {
windowSize() {
if (!this.window()) return { width: 0, height: 0 };
const s = this.window().getSize();
return { width: s[0], height: s[1] };
}
public windowSetSize(width: number, height: number) {
windowSetSize(width: number, height: number) {
if (!this.window()) return;
return this.window().setSize(width, height);
}
public openDevTools() {
openDevTools() {
return this.window().webContents.openDevTools();
}
public closeDevTools() {
closeDevTools() {
return this.window().webContents.closeDevTools();
}
public async showSaveDialog(options: any) {
async showSaveDialog(options: any) {
const { dialog } = require('electron');
if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPaths_.file) options.defaultPath = this.lastSelectedPaths_.file;
@@ -166,7 +162,7 @@ export class Bridge {
return filePath;
}
public async showOpenDialog(options: OpenDialogOptions = null) {
async showOpenDialog(options: OpenDialogOptions = null) {
const { dialog } = require('electron');
if (!options) options = {};
let fileType = 'file';
@@ -181,13 +177,13 @@ export class Bridge {
}
// Don't use this directly - call one of the showXxxxxxxMessageBox() instead
private showMessageBox_(window: any, options: any): number {
showMessageBox_(window: any, options: any): number {
const { dialog } = require('electron');
if (!window) window = this.window();
return dialog.showMessageBoxSync(window, options);
}
public showErrorMessageBox(message: string) {
showErrorMessageBox(message: string) {
return this.showMessageBox_(this.window(), {
type: 'error',
message: message,
@@ -195,7 +191,7 @@ export class Bridge {
});
}
public showConfirmMessageBox(message: string, options: any = null) {
showConfirmMessageBox(message: string, options: any = null) {
options = {
buttons: [_('OK'), _('Cancel')],
...options,
@@ -212,7 +208,7 @@ export class Bridge {
}
/* returns the index of the clicked button */
public showMessageBox(message: string, options: any = null) {
showMessageBox(message: string, options: any = null) {
if (options === null) options = {};
const result = this.showMessageBox_(this.window(), Object.assign({}, {
@@ -224,7 +220,7 @@ export class Bridge {
return result;
}
public showInfoMessageBox(message: string, options: any = {}) {
showInfoMessageBox(message: string, options: any = {}) {
const result = this.showMessageBox_(this.window(), Object.assign({}, {
type: 'info',
message: message,
@@ -233,35 +229,35 @@ export class Bridge {
return result === 0;
}
public setLocale(locale: string) {
setLocale(locale: string) {
setLocale(locale);
}
public get Menu() {
get Menu() {
return require('electron').Menu;
}
public get MenuItem() {
get MenuItem() {
return require('electron').MenuItem;
}
public openExternal(url: string) {
openExternal(url: string) {
return require('electron').shell.openExternal(url);
}
public async openItem(fullPath: string) {
async openItem(fullPath: string) {
return require('electron').shell.openPath(toSystemSlashes(fullPath));
}
public screen() {
screen() {
return require('electron').screen;
}
public shouldUseDarkColors() {
shouldUseDarkColors() {
return nativeTheme.shouldUseDarkColors;
}
public addEventListener(name: string, fn: Function) {
addEventListener(name: string, fn: Function) {
if (name === 'nativeThemeUpdated') {
nativeTheme.on('updated', fn);
} else {
@@ -269,7 +265,7 @@ export class Bridge {
}
}
public restart(linuxSafeRestart = true) {
restart(linuxSafeRestart = true) {
// Note that in this case we are not sending the "appClose" event
// to notify services and component that the app is about to close
// but for the current use-case it's not really needed.

View File

@@ -97,8 +97,7 @@ async function fetchLatestRelease(options: CheckForUpdateOptions) {
}
if (found) {
downloadUrl = asset.browser_download_url.replace('github.com/laurent22/joplin/releases/download', 'objects.joplinusercontent.com');
downloadUrl.concat('?source=DesktopApp&type=Update');
downloadUrl = asset.browser_download_url;
break;
}
}

View File

@@ -2,6 +2,7 @@ const React = require('react');
const { connect } = require('react-redux');
const { clipboard } = require('electron');
import ExtensionBadge from './ExtensionBadge';
import bridge from '../services/bridge';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import ClipperServer from '@joplin/lib/ClipperServer';
@@ -10,29 +11,37 @@ import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { AppState } from '../app.reducer';
class ClipperConfigScreenComponent extends React.Component {
public constructor() {
constructor() {
super();
this.copyToken_click = this.copyToken_click.bind(this);
}
private disableClipperServer_click() {
disableClipperServer_click() {
Setting.setValue('clipperServer.autoStart', false);
void ClipperServer.instance().stop();
}
private enableClipperServer_click() {
enableClipperServer_click() {
Setting.setValue('clipperServer.autoStart', true);
void ClipperServer.instance().start();
}
private copyToken_click() {
chromeButton_click() {
void bridge().openExternal('https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek');
}
firefoxButton_click() {
void bridge().openExternal('https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/');
}
copyToken_click() {
clipboard.writeText(this.props.apiToken);
alert(_('Token has been copied to the clipboard!'));
}
private renewToken_click() {
renewToken_click() {
if (confirm(_('Are you sure you want to renew the authorisation token?'))) {
void EncryptionService.instance()
.generateApiToken()
@@ -43,7 +52,7 @@ class ClipperConfigScreenComponent extends React.Component {
}
}
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const containerStyle = Object.assign({}, theme.containerStyle, {

View File

@@ -26,9 +26,9 @@ const settingKeyToControl: any = {
class ConfigScreenComponent extends React.Component<any, any> {
private rowStyle_: any = null;
rowStyle_: any = null;
public constructor(props: any) {
constructor(props: any) {
super(props);
shared.init(this, reg);
@@ -55,15 +55,15 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.handleSettingButton = this.handleSettingButton.bind(this);
}
private async checkSyncConfig_() {
async checkSyncConfig_() {
await shared.checkSyncConfig(this, this.state.settings);
}
public UNSAFE_componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({ settings: this.props.settings });
}
public componentDidMount() {
componentDidMount() {
if (this.props.defaultSection) {
this.setState({ selectedSectionName: this.props.defaultSection }, () => {
this.switchSection(this.props.defaultSection);
@@ -93,7 +93,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
}
}
public sectionByName(name: string) {
sectionByName(name: string) {
const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings });
for (const section of sections) {
if (section.name === name) return section;
@@ -102,7 +102,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
throw new Error(`Invalid section name: ${name}`);
}
public screenFromName(screenName: string) {
screenFromName(screenName: string) {
if (screenName === 'encryption') return <EncryptionConfigScreen/>;
if (screenName === 'server') return <ClipperConfigScreen themeId={this.props.themeId}/>;
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.themeId}/>;
@@ -110,7 +110,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
throw new Error(`Invalid screen name: ${screenName}`);
}
public switchSection(name: string) {
switchSection(name: string) {
const section = this.sectionByName(name);
let screenName = '';
if (section.isScreen) {
@@ -125,11 +125,11 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.setState({ selectedSectionName: section.name, screenName: screenName });
}
private sidebar_selectionChange(event: any) {
sidebar_selectionChange(event: any) {
this.switchSection(event.section.name);
}
public renderSectionDescription(section: any) {
renderSectionDescription(section: any) {
const description = Setting.sectionDescription(section.name);
if (!description) return null;
@@ -141,7 +141,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
);
}
public sectionToComponent(key: string, section: any, settings: any, selected: boolean) {
sectionToComponent(key: string, section: any, settings: any, selected: boolean) {
const theme = themeStyle(this.props.themeId);
const createSettingComponents = (advanced: boolean) => {
@@ -284,7 +284,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
return description ? <div style={this.descriptionStyle(themeId)}>{description}</div> : null;
}
public settingToComponent(key: string, value: any) {
settingToComponent(key: string, value: any) {
const theme = themeStyle(this.props.themeId);
const output: any = null;
@@ -657,26 +657,26 @@ class ConfigScreenComponent extends React.Component<any, any> {
}
}
public async onApplyClick() {
async onApplyClick() {
shared.saveSettings(this);
await this.checkNeedRestart();
}
public async onSaveClick() {
async onSaveClick() {
shared.saveSettings(this);
await this.checkNeedRestart();
this.props.dispatch({ type: 'NAV_BACK' });
}
public onCancelClick() {
onCancelClick() {
this.props.dispatch({ type: 'NAV_BACK' });
}
public hasChanges() {
hasChanges() {
return !!this.state.changedSettingKeys.length;
}
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const style = Object.assign({},

View File

@@ -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), pluginService.appVersion);
const pluginIds = await repoApi().canBeUpdatedPlugins(pluginItems.map(p => p.manifest));
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, pluginService.appVersion]);
}, [manifestsLoaded, pluginItems]);
const onDelete = useCallback(async (event: ItemEvent) => {
const item = event.item;

View File

@@ -13,19 +13,19 @@ interface Props {
class DropboxLoginScreenComponent extends React.Component<any, any> {
private shared_: any;
shared_: any;
public constructor(props: Props) {
constructor(props: Props) {
super(props);
this.shared_ = new Shared(this, (msg: string) => bridge().showInfoMessageBox(msg), (msg: string) => bridge().showErrorMessageBox(msg));
}
public UNSAFE_componentWillMount() {
UNSAFE_componentWillMount() {
this.shared_.refreshUrl();
}
public render() {
render() {
const style = this.props.style;
const theme = themeStyle(this.props.themeId);

View File

@@ -32,7 +32,7 @@ export default class ErrorBoundary extends React.Component<Props, State> {
public state: State = { error: null, errorInfo: null, pluginInfos: [], plugins: {} };
public componentDidCatch(error: any, errorInfo: ErrorInfo) {
componentDidCatch(error: any, errorInfo: ErrorInfo) {
if (typeof error === 'string') error = { message: error };
const pluginInfos: PluginInfo[] = [];
@@ -58,7 +58,7 @@ export default class ErrorBoundary extends React.Component<Props, State> {
this.setState({ error, errorInfo, pluginInfos, plugins });
}
public componentDidMount() {
componentDidMount() {
const onAppClose = () => {
ipcRenderer.send('asynchronous-message', 'appCloseReply', {
canClose: true,
@@ -68,12 +68,12 @@ export default class ErrorBoundary extends React.Component<Props, State> {
ipcRenderer.on('appClose', onAppClose);
}
public renderMessage() {
renderMessage() {
const message = this.props.message || 'Joplin encountered a fatal error and could not continue.';
return <p>{message}</p>;
}
public render() {
render() {
if (this.state.error) {
const safeMode_click = async () => {
Setting.setValue('isSafeMode', true);

View File

@@ -11,17 +11,17 @@ interface Props {
}
class HelpButtonComponent extends React.Component<Props> {
public constructor(props: Props) {
constructor(props: Props) {
super(props);
this.onClick = this.onClick.bind(this);
}
public onClick() {
onClick() {
if (this.props.onClick) this.props.onClick();
}
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const style = Object.assign({}, this.props.style, { color: theme.color, textDecoration: 'none' });
const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 };

View File

@@ -9,7 +9,7 @@ interface Props {
}
class IconButton extends React.Component<Props> {
public render() {
render() {
const style = this.props.style;
const theme = themeStyle(this.props.themeId);
const iconStyle = {

View File

@@ -24,7 +24,7 @@ interface State {
}
class ImportScreenComponent extends React.Component<Props, State> {
public UNSAFE_componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({
doImport: true,
filePath: this.props.filePath,
@@ -32,7 +32,7 @@ class ImportScreenComponent extends React.Component<Props, State> {
});
}
public UNSAFE_componentWillReceiveProps(newProps: Props) {
UNSAFE_componentWillReceiveProps(newProps: Props) {
if (newProps.filePath) {
this.setState(
{
@@ -47,13 +47,13 @@ class ImportScreenComponent extends React.Component<Props, State> {
}
}
public componentDidMount() {
componentDidMount() {
if (this.state.filePath && this.state.doImport) {
void this.doImport();
}
}
public addMessage(key: string, text: string) {
addMessage(key: string, text: string) {
const messages = this.state.messages.slice();
messages.push({ key: key, text: text });
@@ -61,7 +61,7 @@ class ImportScreenComponent extends React.Component<Props, State> {
this.setState({ messages: messages });
}
public uniqueMessages() {
uniqueMessages() {
const output = [];
const messages = this.state.messages.slice();
const foundKeys = [];
@@ -74,7 +74,7 @@ class ImportScreenComponent extends React.Component<Props, State> {
return output;
}
public async doImport() {
async doImport() {
const filePath = this.props.filePath;
const folderTitle = await Folder.findUniqueItemTitle(filename(filePath));
@@ -109,7 +109,7 @@ class ImportScreenComponent extends React.Component<Props, State> {
this.setState({ doImport: false });
}
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const messages = this.uniqueMessages();

View File

@@ -1,7 +1,4 @@
import * as React from 'react';
import Logger from '@joplin/lib/Logger';
const logger = Logger.create('ItemList');
interface Props {
style: any;
@@ -24,7 +21,7 @@ class ItemList extends React.Component<Props, State> {
private scrollTop_: number;
private listRef: any;
public constructor(props: Props) {
constructor(props: Props) {
super(props);
this.scrollTop_ = 0;
@@ -36,12 +33,12 @@ class ItemList extends React.Component<Props, State> {
this.onDrop = this.onDrop.bind(this);
}
public visibleItemCount(props: Props = undefined) {
visibleItemCount(props: Props = undefined) {
if (typeof props === 'undefined') props = this.props;
return Math.ceil(props.style.height / props.itemHeight);
}
public updateStateItemIndexes(props: Props = undefined) {
updateStateItemIndexes(props: Props = undefined) {
if (typeof props === 'undefined') props = this.props;
const topItemIndex = Math.floor(this.scrollTop_ / props.itemHeight);
@@ -50,66 +47,42 @@ 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,
});
}
public offsetTop() {
offsetTop() {
return this.listRef.current ? this.listRef.current.offsetTop : 0;
}
public offsetScroll() {
offsetScroll() {
return this.scrollTop_;
}
public UNSAFE_componentWillMount() {
UNSAFE_componentWillMount() {
this.updateStateItemIndexes();
}
public UNSAFE_componentWillReceiveProps(newProps: Props) {
UNSAFE_componentWillReceiveProps(newProps: Props) {
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) {
onScroll(event: any) {
this.scrollTop_ = event.target.scrollTop;
this.updateStateItemIndexes();
}
public onKeyDown(event: any) {
onKeyDown(event: any) {
if (this.props.onKeyDown) this.props.onKeyDown(event);
}
public onDrop(event: any) {
onDrop(event: any) {
if (this.props.onNoteDrop) this.props.onNoteDrop(event);
}
public makeItemIndexVisible(itemIndex: number) {
makeItemIndexVisible(itemIndex: number) {
const top = Math.min(this.props.items.length - 1, this.state.topItemIndex);
const bottom = Math.max(0, this.state.bottomItemIndex);
@@ -146,7 +119,7 @@ class ItemList extends React.Component<Props, State> {
// return true;
// }
public render() {
render() {
const items = this.props.items;
const style = Object.assign({}, this.props.style, {
overflowX: 'hidden',

View File

@@ -123,7 +123,7 @@ class MainScreenComponent extends React.Component<Props, State> {
private styles_: any;
private promptOnClose_: Function;
public constructor(props: Props) {
constructor(props: Props) {
super(props);
this.state = {
@@ -250,11 +250,11 @@ class MainScreenComponent extends React.Component<Props, State> {
return this.updateLayoutPluginViews(output, plugins);
}
private window_resize() {
window_resize() {
this.updateRootLayoutSize();
}
public setupAppCloseHandling() {
setupAppCloseHandling() {
this.waitForNotesSavedIID_ = null;
// This event is dispached from the main process when the app is about
@@ -289,11 +289,11 @@ class MainScreenComponent extends React.Component<Props, State> {
});
}
private notePropertiesDialog_close() {
notePropertiesDialog_close() {
this.setState({ notePropertiesDialogOptions: {} });
}
private noteContentPropertiesDialog_close() {
noteContentPropertiesDialog_close() {
this.setState({ noteContentPropertiesDialogOptions: {} });
}
@@ -305,14 +305,14 @@ class MainScreenComponent extends React.Component<Props, State> {
this.setState({ shareFolderDialogOptions: { visible: false, folderId: '' } });
}
public updateMainLayout(layout: LayoutItem) {
updateMainLayout(layout: LayoutItem) {
this.props.dispatch({
type: 'MAIN_LAYOUT_SET',
value: layout,
});
}
public updateRootLayoutSize() {
updateRootLayoutSize() {
this.updateMainLayout(produce(this.props.mainLayout, (draft: any) => {
const s = this.rootLayoutSize();
draft.width = s.width;
@@ -320,7 +320,7 @@ class MainScreenComponent extends React.Component<Props, State> {
}));
}
public componentDidUpdate(prevProps: Props, prevState: State) {
componentDidUpdate(prevProps: Props, prevState: State) {
if (prevProps.style.width !== this.props.style.width ||
prevProps.style.height !== this.props.style.height ||
this.messageBoxVisible(prevProps) !== this.messageBoxVisible(this.props)
@@ -383,24 +383,24 @@ class MainScreenComponent extends React.Component<Props, State> {
}
}
public layoutModeListenerKeyDown(event: any) {
layoutModeListenerKeyDown(event: any) {
if (event.key !== 'Escape') return;
if (!this.props.layoutMoveMode) return;
void CommandService.instance().execute('toggleLayoutMoveMode');
}
public componentDidMount() {
componentDidMount() {
window.addEventListener('keydown', this.layoutModeListenerKeyDown);
}
public componentWillUnmount() {
componentWillUnmount() {
this.unregisterCommands();
window.removeEventListener('resize', this.window_resize);
window.removeEventListener('keydown', this.layoutModeListenerKeyDown);
}
public async waitForNoteToSaved(noteId: string) {
async waitForNoteToSaved(noteId: string) {
while (noteId && this.props.editorNoteStatuses[noteId] === 'saving') {
// eslint-disable-next-line no-console
console.info('Waiting for note to be saved...', this.props.editorNoteStatuses);
@@ -408,7 +408,7 @@ class MainScreenComponent extends React.Component<Props, State> {
}
}
public async printTo_(target: string, options: any) {
async printTo_(target: string, options: any) {
// Concurrent print calls are disallowed to avoid incorrect settings being restored upon completion
if (this.isPrinting_) {
// eslint-disable-next-line no-console
@@ -449,23 +449,23 @@ class MainScreenComponent extends React.Component<Props, State> {
this.isPrinting_ = false;
}
public rootLayoutSize() {
rootLayoutSize() {
return {
width: window.innerWidth,
height: this.rowHeight(),
};
}
public rowHeight() {
rowHeight() {
if (!this.props) return 0;
return this.props.style.height - (this.messageBoxVisible() ? this.messageBoxHeight() : 0);
}
public messageBoxHeight() {
messageBoxHeight() {
return 50;
}
public styles(themeId: number, width: number, height: number, messageBoxVisible: boolean) {
styles(themeId: number, width: number, height: number, messageBoxVisible: boolean) {
const styleKey = [themeId, width, height, messageBoxVisible].join('_');
if (styleKey === this.styleKey_) return this.styles_;
@@ -539,7 +539,7 @@ class MainScreenComponent extends React.Component<Props, State> {
);
}
public renderNotification(theme: any, styles: any) {
renderNotification(theme: any, styles: any) {
if (!this.messageBoxVisible()) return null;
const onViewStatusScreen = () => {
@@ -658,33 +658,33 @@ class MainScreenComponent extends React.Component<Props, State> {
);
}
public messageBoxVisible(props: Props = null) {
messageBoxVisible(props: Props = null) {
if (!props) props = this.props;
return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode || this.showShareInvitationNotification(props) || this.props.needApiAuth || this.props.showInstallTemplatesPlugin;
}
public registerCommands() {
registerCommands() {
for (const command of commands) {
CommandService.instance().registerRuntime(command.declaration.name, command.runtime(this));
}
}
public unregisterCommands() {
unregisterCommands() {
for (const command of commands) {
CommandService.instance().unregisterRuntime(command.declaration.name);
}
}
private resizableLayout_resize(event: any) {
resizableLayout_resize(event: any) {
this.updateMainLayout(event.layout);
}
private resizableLayout_moveButtonClick(event: MoveButtonClickEvent) {
resizableLayout_moveButtonClick(event: MoveButtonClickEvent) {
const newLayout = move(this.props.mainLayout, event.itemKey, event.direction);
this.updateMainLayout(newLayout);
}
private resizableLayout_renderItem(key: string, event: any) {
resizableLayout_renderItem(key: string, event: any) {
// Key should never be undefined but somehow it can happen, also not
// clear how. For now in this case render nothing so that the app
// doesn't crash.
@@ -770,7 +770,7 @@ class MainScreenComponent extends React.Component<Props, State> {
}
}
public renderPluginDialogs() {
renderPluginDialogs() {
const output = [];
const infos = pluginUtils.viewInfosByType(this.props.plugins, 'webview');
@@ -801,7 +801,7 @@ class MainScreenComponent extends React.Component<Props, State> {
);
}
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const style = Object.assign(
{

View File

@@ -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, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import PluginService from '@joplin/lib/services/plugins/PluginService';
const packageInfo = require('../packageInfo.js');
const { clipboard } = require('electron');
const Menu = bridge().Menu;
@@ -128,7 +128,6 @@ interface Props {
customCss: string;
locale: string;
profileConfig: ProfileConfig;
pluginSettings: PluginSettings;
}
const commandNames: string[] = menuCommandNames();
@@ -488,7 +487,8 @@ function useMenu(props: Props) {
}
function _showAbout() {
const v = versionInfo(packageInfo, PluginService.instance().enabledPlugins(props.pluginSettings));
const v = versionInfo(packageInfo, PluginService.instance().plugins);
const copyToClipboard = bridge().showMessageBox(v.message, {
icon: `${bridge().electronApp().buildDir()}/icons/128x128.png`,
@@ -931,7 +931,6 @@ 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,
@@ -987,7 +986,6 @@ 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,

View File

@@ -9,7 +9,7 @@ interface Props {
}
class NavigatorComponent extends React.Component<Props> {
public UNSAFE_componentWillReceiveProps(newProps: Props) {
UNSAFE_componentWillReceiveProps(newProps: Props) {
if (newProps.route) {
const screenInfo = this.props.screens[newProps.route.routeName];
const devMarker = Setting.value('env') === 'dev' ? ` (DEV - ${Setting.value('profileDir')})` : '';
@@ -21,7 +21,7 @@ class NavigatorComponent extends React.Component<Props> {
}
}
public updateWindowTitle(title: string) {
updateWindowTitle(title: string) {
try {
if (bridge().window()) bridge().window().setTitle(title);
} catch (error) {
@@ -29,7 +29,7 @@ class NavigatorComponent extends React.Component<Props> {
}
}
public render() {
render() {
if (!this.props.route) throw new Error('Route must not be null');
const route = this.props.route;

View File

@@ -77,6 +77,16 @@ 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;
@@ -1066,24 +1076,24 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
}
}
async function onKeyDown(event: any) {
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
// 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);
// 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();
}
}
}
function onPasteAsText() {
pasteAsPlainText(null);
async function onPasteAsText() {
await onPaste(createSyntheticClipboardEventWithoutHTML());
}
editor.on(TinyMceEditorEvents.KeyUp, onKeyUp);

View File

@@ -631,7 +631,7 @@ const mapStateToProps = (state: AppState) => {
], whenClauseContext)[0],
contentMaxWidth: state.settings['style.editor.contentMaxWidth'],
isSafeMode: state.settings.isSafeMode,
useCustomPdfViewer: false, // state.settings.useCustomPdfViewer,
useCustomPdfViewer: state.settings.useCustomPdfViewer,
};
};

View File

@@ -3,7 +3,6 @@ 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';
@@ -24,7 +23,6 @@ export interface MarkupToHtmlOptions {
useCustomPdfViewer?: boolean;
noteId?: string;
vendorDir?: string;
platformName?: string;
}
export default function useMarkupToHtml(deps: HookDependencies) {
@@ -42,7 +40,6 @@ export default function useMarkupToHtml(deps: HookDependencies) {
options = {
replaceResourceInternalToExternalLinks: false,
resourceInfos: {},
platformName: shim.platformName(),
...options,
};
@@ -65,7 +62,6 @@ export default function useMarkupToHtml(deps: HookDependencies) {
postMessageSyntax: 'ipcProxySendToHost',
splitted: true,
externalAssetsOnly: true,
codeHighlightCacheKey: 'useMarkupToHtml',
}, options));
return result;

View File

@@ -241,7 +241,6 @@ 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) => {
@@ -295,7 +294,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 && !props.focusedField;
const doRefocus = props.notes.length < previousNotes.length;
for (let i = 0; i < props.notes.length; i++) {
if (props.notes[i].id === id) {
@@ -312,7 +311,8 @@ const NoteListComponent = (props: Props) => {
if (previousVisible !== props.visible) {
updateSizeState();
}
}, [previousSelectedNoteIds, previousNotes, previousVisible, props.selectedNoteIds, props.notes, props.focusedField, props.visible]);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [previousSelectedNoteIds, previousNotes, previousVisible, props.selectedNoteIds, props.notes]);
const scrollNoteIndex_ = (keyCode: any, ctrlKey: any, metaKey: any, noteIndex: any) => {
@@ -559,7 +559,6 @@ const mapStateToProps = (state: AppState) => {
highlightedWords: state.highlightedWords,
plugins: state.pluginService.plugins,
customCss: state.customCss,
focusedField: state.focusedField,
};
};

View File

@@ -23,5 +23,4 @@ export interface Props {
highlightedWords: string[];
provisionalNoteIds: string[];
visible: boolean;
focusedField: string;
}

View File

@@ -1,6 +1,6 @@
import { AppState } from '../../app.reducer';
import * as React from 'react';
import { useEffect, useRef, useMemo, useState } from 'react';
import { useEffect, useRef } from 'react';
import SearchBar from '../SearchBar/SearchBar';
import Button, { ButtonLevel, ButtonSize, buttonSizePx } from '../Button/Button';
import CommandService from '@joplin/lib/services/CommandService';
@@ -11,13 +11,6 @@ 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;
@@ -25,15 +18,6 @@ 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`
@@ -50,9 +34,7 @@ const StyledButton = styled(Button)`
width: auto;
height: 26px;
min-height: 26px;
min-width: 37px;
max-width: none;
white-space: nowrap;
flex: 1 0 auto;
.fa, .fas {
font-size: 11px;
@@ -72,13 +54,7 @@ const StyledPairButtonR = styled(Button)`
width: auto;
`;
const TopRow = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
`;
const BottomRow = styled.div`
const RowContainer = styled.div`
display: flex;
flex-direction: row;
flex: 1 1 auto;
@@ -92,102 +68,7 @@ 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));
@@ -246,63 +127,64 @@ function NoteListControls(props: Props) {
if (!props.showNewNoteButtons) return null;
return (
<TopRow className="new-note-todo-buttons">
<StyledButton ref={newNoteRef}
<RowContainer>
<StyledButton
className="new-note-button"
tooltip={ showTooltip ? CommandService.instance().label('newNote') : '' }
iconName={noteIcon}
title={_('%s', noteButtonText)}
tooltip={CommandService.instance().label('newNote')}
iconName="fas fa-plus"
title={_('%s', 'New note')}
level={ButtonLevel.Primary}
size={ButtonSize.Small}
onClick={onNewNoteButtonClick}
/>
<StyledButton ref={newTodoRef}
<StyledButton
className="new-todo-button"
tooltip={ showTooltip ? CommandService.instance().label('newTodo') : '' }
iconName={todoIcon}
title={_('%s', todoButtonText)}
tooltip={CommandService.instance().label('newTodo')}
iconName="fas fa-plus"
title={_('%s', 'New to-do')}
level={ButtonLevel.Secondary}
size={ButtonSize.Small}
onClick={onNewTodoButtonClick}
/>
</TopRow>
</RowContainer>
);
}
return (
<StyledRoot ref={noteControlsRef}>
<StyledRoot>
{renderNewNoteButtons()}
<BottomRow ref={searchAndSortRef} className="search-and-sort">
<RowContainer>
<SearchBar inputRef={searchBarRef}/>
{showsSortOrderButtons() &&
<SortOrderButtonsContainer>
<StyledPairButtonL
className="sort-order-field-button"
tooltip={sortOrderFieldTooltip()}
iconName={sortOrderFieldIcon()}
level={ButtonLevel.Secondary}
size={ButtonSize.Small}
onClick={onSortOrderFieldButtonClick}
/>
<StyledPairButtonR
className="sort-order-reverse-button"
tooltip={CommandService.instance().label('toggleNotesSortOrderReverse')}
iconName={sortOrderReverseIcon()}
level={ButtonLevel.Secondary}
size={ButtonSize.Small}
onClick={onSortOrderReverseButtonClick}
/>
</SortOrderButtonsContainer>
}
</BottomRow>
<SortOrderButtonsContainer>
{showsSortOrderButtons() &&
<StyledPairButtonL
className="sort-order-field-button"
tooltip={sortOrderFieldTooltip()}
iconName={sortOrderFieldIcon()}
level={ButtonLevel.Secondary}
size={ButtonSize.Small}
onClick={onSortOrderFieldButtonClick}
/>
}
{showsSortOrderButtons() &&
<StyledPairButtonR
className="sort-order-reverse-button"
tooltip={CommandService.instance().label('toggleNotesSortOrderReverse')}
iconName={sortOrderReverseIcon()}
level={ButtonLevel.Secondary}
size={ButtonSize.Small}
onClick={onSortOrderReverseButtonClick}
/>
}
</SortOrderButtonsContainer>
</RowContainer>
</StyledRoot>
);
}
const mapStateToProps = (state: AppState) => {
return {
// TODO: showNewNoteButtons and the logic associated is not needed anymore.
showNewNoteButtons: true,
showNewNoteButtons: state.focusedField !== 'globalSearch',
sortOrderButtonsVisible: state.settings['notes.sortOrder.buttonsVisible'],
sortOrderField: state.settings['notes.sortOrder.field'],
sortOrderReverse: state.settings['notes.sortOrder.reverse'],

View File

@@ -1,6 +1,6 @@
import { themeStyle } from '@joplin/lib/theme';
import * as React from 'react';
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import NoteList from '../NoteList/NoteList';
import NoteListControls from '../NoteListControls/NoteListControls';
import { Size } from '../ResizableLayout/utils/types';
@@ -22,15 +22,7 @@ const StyledRoot = styled.div`
export default function NoteListWrapper(props: Props) {
const theme = themeStyle(props.themeId);
const [controlHeight, setControlHeight] = useState(theme.topRowHeight);
const onContentHeightChange = (sameRow: boolean) => {
if (sameRow) {
setControlHeight(theme.topRowHeight);
} else {
setControlHeight(theme.topRowHeight * 2);
}
};
const controlHeight = theme.topRowHeight;
const noteListSize = useMemo(() => {
return {
@@ -41,7 +33,7 @@ export default function NoteListWrapper(props: Props) {
return (
<StyledRoot>
<NoteListControls height={controlHeight} width={noteListSize.width} onContentHeightChange={onContentHeightChange}/>
<NoteListControls height={controlHeight}/>
<NoteList resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
</StyledRoot>
);

View File

@@ -31,7 +31,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
private styleKey_: number;
private styles_: any;
public constructor(props: Props) {
constructor(props: Props) {
super(props);
this.revisionsLink_click = this.revisionsLink_click.bind(this);
@@ -56,17 +56,17 @@ class NotePropertiesDialog extends React.Component<Props, State> {
};
}
public componentDidMount() {
componentDidMount() {
void this.loadNote(this.props.noteId);
}
public componentDidUpdate() {
componentDidUpdate() {
if (this.state.editedKey === null) {
this.okButton.current.focus();
}
}
public async loadNote(noteId: string) {
async loadNote(noteId: string) {
if (!noteId) {
this.setState({ formNote: null });
} else {
@@ -76,7 +76,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
}
}
public latLongFromLocation(location: string) {
latLongFromLocation(location: string) {
const o: any = {};
const l = location.split(',');
if (l.length === 2) {
@@ -89,7 +89,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
return o;
}
public noteToFormNote(note: NoteEntity) {
noteToFormNote(note: NoteEntity) {
const formNote: any = {};
formNote.user_updated_time = time.formatMsToLocal(note.user_updated_time);
@@ -113,7 +113,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
return formNote;
}
public formNoteToNote(formNote: any) {
formNoteToNote(formNote: any) {
const note = Object.assign({ id: formNote.id }, this.latLongFromLocation(formNote.location));
note.user_created_time = time.formatLocalToMs(formNote.user_created_time);
note.user_updated_time = time.formatLocalToMs(formNote.user_updated_time);
@@ -127,7 +127,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
return note;
}
public styles(themeId: number) {
styles(themeId: number) {
const styleKey = themeId;
if (styleKey === this.styleKey_) return this.styles_;
@@ -168,7 +168,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
return this.styles_;
}
public async closeDialog(applyChanges: boolean) {
async closeDialog(applyChanges: boolean) {
if (applyChanges) {
await this.saveProperty();
const note = this.formNoteToNote(this.state.formNote);
@@ -183,16 +183,16 @@ class NotePropertiesDialog extends React.Component<Props, State> {
}
}
private buttonRow_click(event: any) {
buttonRow_click(event: any) {
void this.closeDialog(event.buttonName === 'ok');
}
private revisionsLink_click() {
revisionsLink_click() {
void this.closeDialog(false);
if (this.props.onRevisionLinkClick) this.props.onRevisionLinkClick();
}
public editPropertyButtonClick(key: string, initialValue: any) {
editPropertyButtonClick(key: string, initialValue: any) {
this.setState({
editedKey: key,
editedValue: initialValue,
@@ -207,7 +207,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
}, 100);
}
public async saveProperty() {
async saveProperty() {
if (!this.state.editedKey) return;
return new Promise((resolve: Function) => {
@@ -233,7 +233,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
});
}
public async cancelProperty() {
async cancelProperty() {
return new Promise((resolve: Function) => {
this.okButton.current.focus();
this.setState({
@@ -245,7 +245,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
});
}
public createNoteField(key: string, value: any) {
createNoteField(key: string, value: any) {
const styles = this.styles(this.props.themeId);
const theme = themeStyle(this.props.themeId);
const labelComp = <label style={Object.assign({}, theme.textStyle, theme.controlBoxLabel)}>{this.formatLabel(key)}</label>;
@@ -364,12 +364,12 @@ class NotePropertiesDialog extends React.Component<Props, State> {
);
}
public formatLabel(key: string) {
formatLabel(key: string) {
if (this.keyToLabel_[key]) return this.keyToLabel_[key];
return key;
}
public formatValue(key: string, note: NoteEntity) {
formatValue(key: string, note: NoteEntity) {
if (key === 'location') {
if (!Number(note.latitude) && !Number(note.longitude)) return null;
const dms = formatcoords(Number(note.latitude), Number(note.longitude));
@@ -383,7 +383,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
return (note as any)[key];
}
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const formNote = this.state.formNote;

View File

@@ -38,7 +38,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
private viewerRef_: any;
private helpButton_onClick: Function;
public constructor(props: Props) {
constructor(props: Props) {
super(props);
this.state = {
@@ -57,7 +57,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
}
public style() {
style() {
const theme = themeStyle(this.props.themeId);
const style = {
@@ -74,7 +74,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
return style;
}
private async viewer_domReady() {
async viewer_domReady() {
// this.viewerRef_.current.openDevTools();
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, this.props.noteId);
@@ -90,7 +90,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
);
}
private async importButton_onClick() {
async importButton_onClick() {
if (!this.state.note) return;
this.setState({ restoring: true });
await RevisionService.instance().importRevisionNote(this.state.note);
@@ -98,11 +98,11 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
alert(RevisionService.instance().restoreSuccessMessage(this.state.note));
}
private backButton_click() {
backButton_click() {
if (this.props.onBack) this.props.onBack();
}
private revisionList_onChange(event: any) {
revisionList_onChange(event: any) {
const value = event.target.value;
if (!value) {
@@ -119,7 +119,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
}
}
public async reloadNote() {
async reloadNote() {
let noteBody = '';
let markupLanguage = MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
if (!this.state.revisions.length || !this.state.currentRevId) {
@@ -153,7 +153,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
});
}
private async webview_ipcMessage(event: any) {
async webview_ipcMessage(event: any) {
// For the revision view, we only suppport a minimal subset of the IPC messages.
// For example, we don't need interactive checkboxes or sync between viewer and editor view.
// We try to get most links work though, except for internal (joplin://) links.
@@ -183,7 +183,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
}
}
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const style = this.style();

View File

@@ -20,7 +20,7 @@ class NoteSearchBar extends React.Component<Props> {
private backgroundColor: any;
public constructor(props: Props) {
constructor(props: Props) {
super(props);
this.searchInput_change = this.searchInput_change.bind(this);
@@ -33,7 +33,7 @@ class NoteSearchBar extends React.Component<Props> {
this.backgroundColor = undefined;
}
public style() {
style() {
const theme = themeStyle(this.props.themeId);
const style = {
@@ -46,7 +46,7 @@ class NoteSearchBar extends React.Component<Props> {
return style;
}
public buttonIconComponent(iconName: string, clickHandler: any, isEnabled: boolean) {
buttonIconComponent(iconName: string, clickHandler: any, isEnabled: boolean) {
const theme = themeStyle(this.props.themeId);
const searchButton = {
@@ -74,12 +74,12 @@ class NoteSearchBar extends React.Component<Props> {
);
}
private searchInput_change(event: any) {
searchInput_change(event: any) {
const query = event.currentTarget.value;
this.triggerOnChange(query);
}
private searchInput_keyDown(event: any) {
searchInput_keyDown(event: any) {
if (event.keyCode === 13) {
// ENTER
event.preventDefault();
@@ -106,28 +106,28 @@ class NoteSearchBar extends React.Component<Props> {
}
}
private previousButton_click() {
previousButton_click() {
if (this.props.onPrevious) this.props.onPrevious();
}
private nextButton_click() {
nextButton_click() {
if (this.props.onNext) this.props.onNext();
}
private closeButton_click() {
closeButton_click() {
if (this.props.onClose) this.props.onClose();
}
public triggerOnChange(query: string) {
triggerOnChange(query: string) {
if (this.props.onChange) this.props.onChange(query);
}
public focus() {
focus() {
(this.refs.searchInput as any).focus();
(this.refs.searchInput as any).select();
}
public render() {
render() {
const query = this.props.query ? this.props.query : '';
// backgroundColor needs to cached to a local variable to prevent the

View File

@@ -11,7 +11,7 @@ interface Props {
}
class NoteStatusBarComponent extends React.Component<Props> {
public style() {
style() {
const theme = themeStyle(this.props.themeId);
const style = {
@@ -24,7 +24,7 @@ class NoteStatusBarComponent extends React.Component<Props> {
return style;
}
public render() {
render() {
const note = this.props.note;
return <div style={this.style().root}>{time.formatMsToLocal(note.user_updated_time)}</div>;
}

View File

@@ -17,7 +17,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
private webviewRef_: any;
private webviewListeners_: any = null;
public constructor(props: any) {
constructor(props: any) {
super(props);
this.webviewRef_ = React.createRef();
@@ -41,20 +41,20 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
this.webview_message = this.webview_message.bind(this);
}
private webview_domReady(event: any) {
webview_domReady(event: any) {
this.domReady_ = true;
if (this.props.onDomReady) this.props.onDomReady(event);
}
private webview_ipcMessage(event: any) {
webview_ipcMessage(event: any) {
if (this.props.onIpcMessage) this.props.onIpcMessage(event);
}
private webview_load() {
webview_load() {
this.webview_domReady({});
}
private webview_message(event: any) {
webview_message(event: any) {
if (!event.data || event.data.target !== 'main') return;
const callName = event.data.name;
@@ -68,11 +68,11 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
}
}
public domReady() {
domReady() {
return this.domReady_;
}
public initWebview() {
initWebview() {
const wv = this.webviewRef_.current;
if (!this.webviewListeners_) {
@@ -92,7 +92,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
this.webviewRef_.current.contentWindow.addEventListener('message', this.webview_message);
}
public destroyWebview() {
destroyWebview() {
const wv = this.webviewRef_.current;
if (!wv || !this.initialized_) return;
@@ -115,28 +115,28 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
this.domReady_ = false;
}
public focus() {
focus() {
if (this.webviewRef_.current) {
this.webviewRef_.current.focus();
}
}
public tryInit() {
tryInit() {
if (!this.initialized_ && this.webviewRef_.current) {
this.initWebview();
this.initialized_ = true;
}
}
public componentDidMount() {
componentDidMount() {
this.tryInit();
}
public componentDidUpdate() {
componentDidUpdate() {
this.tryInit();
}
public componentWillUnmount() {
componentWillUnmount() {
this.destroyWebview();
}
@@ -144,7 +144,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
// Wrap WebView functions
// ----------------------------------------------------------------
public send(channel: string, arg0: any = null, arg1: any = null) {
send(channel: string, arg0: any = null, arg1: any = null) {
const win = this.webviewRef_.current.contentWindow;
if (channel === 'focus') {
@@ -172,7 +172,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
// Wrap WebView functions (END)
// ----------------------------------------------------------------
public render() {
render() {
const viewerStyle = Object.assign({}, { border: 'none' }, this.props.viewerStyle);
return <iframe className="noteTextViewer" ref={this.webviewRef_} style={viewerStyle} src="gui/note-viewer/index.html"></iframe>;
}

View File

@@ -14,7 +14,7 @@ interface Props {
}
class OneDriveLoginScreenComponent extends React.Component<any, any> {
public constructor(props: Props) {
constructor(props: Props) {
super(props);
this.state = {
@@ -22,7 +22,7 @@ class OneDriveLoginScreenComponent extends React.Component<any, any> {
};
}
public async componentDidMount() {
async componentDidMount() {
const log = (s: any) => {
this.setState((state: any) => {
const authLog = state.authLog.slice();
@@ -48,15 +48,15 @@ class OneDriveLoginScreenComponent extends React.Component<any, any> {
}
}
public startUrl() {
startUrl() {
return reg.syncTarget().api().authCodeUrl(this.redirectUrl());
}
public redirectUrl() {
redirectUrl() {
return reg.syncTarget().api().nativeClientRedirectUrl();
}
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const logComps = [];

View File

@@ -26,27 +26,22 @@ export default class PromptDialog extends React.Component<Props, any> {
private focusInput_: boolean;
private styles_: any;
private styleKey_: string;
private menuIsOpened_: boolean = false;
public constructor(props: Props) {
constructor(props: Props) {
super(props);
this.answerInput_ = React.createRef();
this.select_menuOpen = this.select_menuOpen.bind(this);
this.select_menuClose = this.select_menuClose.bind(this);
}
public UNSAFE_componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({
visible: false,
answer: this.props.defaultValue ? this.props.defaultValue : '',
});
this.focusInput_ = true;
this.menuIsOpened_ = false;
}
public UNSAFE_componentWillReceiveProps(newProps: Props) {
UNSAFE_componentWillReceiveProps(newProps: Props) {
if ('visible' in newProps && newProps.visible !== this.props.visible) {
this.setState({ visible: newProps.visible });
if (newProps.visible) this.focusInput_ = true;
@@ -57,20 +52,12 @@ export default class PromptDialog extends React.Component<Props, any> {
}
}
private select_menuOpen() {
this.menuIsOpened_ = true;
}
private select_menuClose() {
this.menuIsOpened_ = false;
}
public componentDidUpdate() {
componentDidUpdate() {
if (this.focusInput_ && this.answerInput_.current) this.answerInput_.current.focus();
this.focusInput_ = false;
}
public styles(themeId: number, width: number, height: number, visible: boolean) {
styles(themeId: number, width: number, height: number, visible: boolean) {
const styleKey = `${themeId}_${width}_${height}_${visible}`;
if (styleKey === this.styleKey_) return this.styles_;
@@ -194,7 +181,7 @@ export default class PromptDialog extends React.Component<Props, any> {
return this.styles_;
}
public render() {
render() {
const style = this.props.style;
const theme = themeStyle(this.props.themeId);
const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel'];
@@ -237,14 +224,16 @@ export default class PromptDialog extends React.Component<Props, any> {
const onKeyDown = (event: any) => {
if (event.key === 'Enter') {
// If the dropdown is open, we don't close the dialog - instead
// the currently item will be selcted. If it is closed however
// we confirm the dialog.
if ((this.props.inputType === 'tags' || this.props.inputType === 'dropdown') && this.menuIsOpened_) {
if (this.props.inputType === 'tags' || this.props.inputType === 'dropdown') {
// Do nothing
} else {
onClose(true);
}
// } else if (this.answerInput_.current && !this.answerInput_.current.state.menuIsOpen) {
// // The menu will be open if the user is selecting a new item
// onClose(true);
// }
} else if (event.key === 'Escape') {
onClose(false);
}
@@ -257,9 +246,9 @@ export default class PromptDialog extends React.Component<Props, any> {
if (this.props.inputType === 'datetime') {
inputComp = <Datetime className="datetime-picker" value={this.state.answer} inputProps={{ style: styles.input }} dateFormat={time.dateFormat()} timeFormat={time.timeFormat()} onChange={(momentObject: any) => onDateTimeChange(momentObject)} />;
} else if (this.props.inputType === 'tags') {
inputComp = <CreatableSelect className="tag-selector" onMenuOpen={this.select_menuOpen} onMenuClose={this.select_menuClose} styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} value={this.state.answer} placeholder="" components={makeAnimated()} isMulti={true} isClearable={false} backspaceRemovesValue={true} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
inputComp = <CreatableSelect className="tag-selector" styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} value={this.state.answer} placeholder="" components={makeAnimated()} isMulti={true} isClearable={false} backspaceRemovesValue={true} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
} else if (this.props.inputType === 'dropdown') {
inputComp = <Select className="item-selector" onMenuOpen={this.select_menuOpen} onMenuClose={this.select_menuClose} styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} components={makeAnimated()} value={this.props.answer} defaultValue={this.props.defaultValue} isClearable={false} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
inputComp = <Select className="item-selector" styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} components={makeAnimated()} value={this.props.answer} defaultValue={this.props.defaultValue} isClearable={false} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
} else {
inputComp = <input style={styles.input} ref={this.answerInput_} value={this.state.answer} type="text" onChange={event => onChange(event)} onKeyDown={event => onKeyDown(event)} />;
}

View File

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

View File

@@ -260,6 +260,10 @@ 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',

View File

@@ -135,7 +135,7 @@ const getNextSortingOrderType = (s: SortingType): SortingType => {
const MAX_RESOURCES = 10000;
class ResourceScreenComponent extends React.Component<Props, State> {
public constructor(props: Props) {
constructor(props: Props) {
super(props);
this.state = {
resources: undefined,
@@ -147,7 +147,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
};
}
public async reloadResources(sorting: ActiveSorting) {
async reloadResources(sorting: ActiveSorting) {
this.setState({ isLoading: true });
const resources = await Resource.all({
order: [{
@@ -161,11 +161,11 @@ class ResourceScreenComponent extends React.Component<Props, State> {
this.setState({ resources, isLoading: false });
}
public componentDidMount() {
componentDidMount() {
void this.reloadResources(this.state.sorting);
}
public onResourceDelete(resource: InnerResource) {
onResourceDelete(resource: InnerResource) {
const ok = bridge().showConfirmMessageBox(_('Delete attachment "%s"?', resource.title), {
buttons: [_('Delete'), _('Cancel')],
defaultId: 1,
@@ -184,7 +184,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
});
}
public openResource(resource: InnerResource) {
openResource(resource: InnerResource) {
const resourcePath = Resource.fullPath(resource);
const ok = bridge().openExternal(`file://${resourcePath}`);
if (!ok) {
@@ -192,7 +192,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
}
}
public onToggleSortOrder(sortOrder: SortingOrder) {
onToggleSortOrder(sortOrder: SortingOrder) {
let newSorting = { ...this.state.sorting };
if (sortOrder === this.state.sorting.order) {
newSorting.type = getNextSortingOrderType(newSorting.type);
@@ -206,7 +206,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
void this.reloadResources(newSorting);
}
public render() {
render() {
const style = this.props.style;
const theme = themeStyle(this.props.themeId);

View File

@@ -27,7 +27,7 @@ import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
import ImportScreen from './ImportScreen';
const { ResourceScreen } = require('./ResourceScreen.js');
import Navigator from './Navigator';
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
const bridge = require('@electron/remote').require('./bridge').default;
@@ -141,7 +141,7 @@ class RootComponent extends React.Component<Props, any> {
});
}
await WelcomeUtils.install(Setting.value('locale'), this.props.dispatch);
await WelcomeUtils.install(this.props.dispatch);
}
private renderModalMessage(props: ModalDialogProps) {

View File

@@ -48,15 +48,6 @@ const StyledFoldersHolder = styled.div`
}}
}
`;
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;
@@ -747,9 +738,9 @@ const SidebarComponent = (props: Props) => {
tagItemsOrder_.current = result.order;
items.push(
<TagsHolder className="tags" key="tag_items" style={{ display: props.tagHeaderIsExpanded ? 'block' : 'none' }}>
<div className="tags" key="tag_items" style={{ display: props.tagHeaderIsExpanded ? 'block' : 'none' }}>
{tagItems}
</TagsHolder>
</div>
);
}

View File

@@ -5,7 +5,7 @@ import CommandService from '@joplin/lib/services/CommandService';
import { AppState } from '../app.reducer';
class TagItemComponent extends React.Component {
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const style = Object.assign({}, theme.tagStyle);
const { title, id } = this.props;

View File

@@ -13,7 +13,7 @@ interface Props {
class ToolbarBaseComponent extends React.Component<Props, any> {
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const style: any = Object.assign({

View File

@@ -6,7 +6,7 @@ interface Props {
}
class ToolbarSpace extends React.Component<Props> {
public render() {
render() {
const theme = themeStyle(this.props.themeId);
const style = Object.assign({}, theme.toolbarStyle);
style.minWidth = style.height / 2;

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