You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-02-01 07:49:31 +02:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5808b60a6 | ||
|
|
e548b3ee74 | ||
|
|
8e2b6ca296 | ||
|
|
0172bb0ad8 | ||
|
|
bd0bef121c | ||
|
|
6569dff1ac | ||
|
|
5226a2e630 | ||
|
|
1d38e443ba | ||
|
|
09ccc2f25d | ||
|
|
5ad19b7261 | ||
|
|
70293478a2 | ||
|
|
3aaa20254f | ||
|
|
42c248f7ca | ||
|
|
ac1e94a8df | ||
|
|
daff4496cf | ||
|
|
1e00078228 | ||
|
|
03a1de9370 | ||
|
|
55ef256c65 | ||
|
|
6d115db16f | ||
|
|
5853031fde | ||
|
|
47db2ae962 | ||
|
|
b960a2a8b0 | ||
|
|
fcaa7d2a98 | ||
|
|
99284ae135 | ||
|
|
66ae58c81b | ||
|
|
484d6a866d | ||
|
|
b45fd09e38 | ||
|
|
903a369c13 | ||
|
|
1fb79315e4 | ||
|
|
4dc021b523 | ||
|
|
bbb4b46dd9 | ||
|
|
063dc46f50 | ||
|
|
aa400b52be | ||
|
|
be7de2f08a | ||
|
|
f8a129e4dc | ||
|
|
c5d9646908 | ||
|
|
876ec80911 | ||
|
|
4051f88ce7 | ||
|
|
f194c111e4 | ||
|
|
e386246bc9 | ||
|
|
292b269f1d | ||
|
|
b2fc43da2b | ||
|
|
4a23a1ed3e | ||
|
|
c8878a18bf | ||
|
|
340fba7af5 | ||
|
|
271c4f4a2a | ||
|
|
c9dba20f59 | ||
|
|
b474cc206a | ||
|
|
9d4df8cc6e | ||
|
|
a4ddfe1f58 | ||
|
|
7d15215e66 | ||
|
|
449555c8e9 |
@@ -1061,6 +1061,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.test.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.js
|
||||
packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
@@ -1101,6 +1103,7 @@ packages/editor/CodeMirror/utils/getSearchState.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/htmlNodeInfo.js
|
||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
|
||||
@@ -1841,17 +1844,19 @@ packages/tools/checkIgnoredFiles.js
|
||||
packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionRunner.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/doRandomAction.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/types.js
|
||||
packages/tools/fuzzer/utils/ProgressBar.js
|
||||
packages/tools/fuzzer/utils/SeededRandom.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1035,6 +1035,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.test.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.js
|
||||
packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
@@ -1075,6 +1077,7 @@ packages/editor/CodeMirror/utils/getSearchState.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/htmlNodeInfo.js
|
||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
|
||||
@@ -1815,17 +1818,19 @@ packages/tools/checkIgnoredFiles.js
|
||||
packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionRunner.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/doRandomAction.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/types.js
|
||||
packages/tools/fuzzer/utils/ProgressBar.js
|
||||
packages/tools/fuzzer/utils/SeededRandom.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
|
||||
# permission from being added.
|
||||
# See:
|
||||
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
|
||||
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
|
||||
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index a16b4ad6d1871cf5cf73ef7ebeaf8bd4d662b134..9871afb5fbf8e687370e08f54d884ecd7dde7e7c 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -37,6 +37,10 @@ android {
|
||||
}
|
||||
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 31)
|
||||
+
|
||||
+ defaultConfig {
|
||||
+ minSdkVersion safeExtGet('minSdkVersion', 24)
|
||||
+ }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
|
||||
# permission from being added.
|
||||
# See:
|
||||
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
|
||||
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
|
||||
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index d42bd23123644cc324051e9c7ec4635de286315a..640996df60fe7769f69b30b35f771eb9cf0b75d4 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -37,6 +37,10 @@ android {
|
||||
}
|
||||
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 31)
|
||||
+
|
||||
+ defaultConfig {
|
||||
+ minSdkVersion safeExtGet('minSdkVersion', 24)
|
||||
+ }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
|
||||
# permission from being added.
|
||||
# See:
|
||||
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
|
||||
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
|
||||
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index 170ec0ff9befe0f9155aaf5e1b84133cfd87be99..e6a0ab4a019ee67c5af7761ae8bb35f18b05c590 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -37,6 +37,10 @@ android {
|
||||
}
|
||||
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 31)
|
||||
+
|
||||
+ defaultConfig {
|
||||
+ minSdkVersion safeExtGet('minSdkVersion', 24)
|
||||
+ }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
|
||||
# permission from being added.
|
||||
# See:
|
||||
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
|
||||
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
|
||||
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index 3b22f9de66795ee01dbaa29655727ee7ddba3cc8..325daa88d33f066b3826e5031ce281793710af2d 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -37,6 +37,10 @@ android {
|
||||
}
|
||||
|
||||
compileSdkVersion safeExtGet('compileSdkVersion', 31)
|
||||
+
|
||||
+ defaultConfig {
|
||||
+ minSdkVersion safeExtGet('minSdkVersion', 24)
|
||||
+ }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -86,9 +86,9 @@
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "16.1.6",
|
||||
"lint-staged": "16.2.6",
|
||||
"madge": "8.0.0",
|
||||
"npm-package-json-lint": "8.0.0",
|
||||
"npm-package-json-lint": "9.0.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
BIN
packages/app-cli/tests/support/onenote/test.onepkg
Normal file
BIN
packages/app-cli/tests/support/onenote/test.onepkg
Normal file
Binary file not shown.
@@ -95,6 +95,9 @@ export default class InteropServiceHelper {
|
||||
// Allows users to override the CSS page size.
|
||||
// See https://github.com/laurent22/joplin/issues/13096
|
||||
preferCSSPageSize: true,
|
||||
|
||||
// Include accessibility information in the output:
|
||||
generateTaggedPDF: true,
|
||||
});
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.6.1",
|
||||
"version": "3.6.2",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.bundle.js",
|
||||
"private": true,
|
||||
|
||||
90
packages/app-desktop/tools/notarizeFile.js
Normal file
90
packages/app-desktop/tools/notarizeFile.js
Normal file
@@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.default = notarizeFile;
|
||||
const fs_1 = require('fs');
|
||||
const notarize_1 = require('@electron/notarize');
|
||||
const execCommand = require('./execCommand');
|
||||
const child_process_1 = require('child_process');
|
||||
const util_1 = require('util');
|
||||
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
||||
// Same appId in electron-builder.
|
||||
const appId = 'net.cozic.joplin-desktop';
|
||||
function isDesktopAppTag(tagName) {
|
||||
if (!tagName) { return false; }
|
||||
return tagName[0] === 'v';
|
||||
}
|
||||
async function notarizeFile(filePath) {
|
||||
if (process.platform !== 'darwin') { return; }
|
||||
console.info(`Checking if notarization should be done on: ${filePath}`);
|
||||
if (!process.env.IS_CONTINUOUS_INTEGRATION || !isDesktopAppTag(process.env.GIT_TAG_NAME)) {
|
||||
console.info(`Either not running in CI or not processing a desktop app tag - skipping notarization. process.env.IS_CONTINUOUS_INTEGRATION = ${process.env.IS_CONTINUOUS_INTEGRATION}; process.env.GIT_TAG_NAME = ${process.env.GIT_TAG_NAME}`);
|
||||
return;
|
||||
}
|
||||
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
|
||||
console.warn('Environment variables APPLE_ID and APPLE_ID_PASSWORD not found - notarization will NOT be done.');
|
||||
return;
|
||||
}
|
||||
if (!(0, fs_1.existsSync)(filePath)) {
|
||||
throw new Error(`Cannot find file at: ${filePath}`);
|
||||
}
|
||||
// Every x seconds we print something to stdout, otherwise CI may timeout
|
||||
// the task after 10 minutes, and Apple notarization can take more time.
|
||||
const waitingIntervalId = setInterval(() => {
|
||||
console.info('.');
|
||||
}, 60000);
|
||||
const isPkg = filePath.endsWith('.pkg');
|
||||
console.info(`Notarizing ${filePath}`);
|
||||
try {
|
||||
if (isPkg) {
|
||||
await execAsync(`xcrun notarytool submit "${filePath}" ` +
|
||||
`--apple-id "${process.env.APPLE_ID}" ` +
|
||||
`--password "${process.env.APPLE_ID_PASSWORD}" ` +
|
||||
`--team-id "${process.env.APPLE_ASC_PROVIDER}" ` +
|
||||
'--wait', { maxBuffer: 1024 * 1024 });
|
||||
} else {
|
||||
await (0, notarize_1.notarize)({
|
||||
appBundleId: appId,
|
||||
appPath: filePath,
|
||||
// Apple Developer email address
|
||||
appleId: process.env.APPLE_ID,
|
||||
// App-specific password: https://support.apple.com/en-us/HT204397
|
||||
appleIdPassword: process.env.APPLE_ID_PASSWORD,
|
||||
// When Apple ID is attached to multiple providers (eg if the
|
||||
// account has been used to build multiple apps for different
|
||||
// companies), in that case the provider "Team Short Name" (also
|
||||
// known as "ProviderShortname") must be provided.
|
||||
//
|
||||
// Use this to get it:
|
||||
//
|
||||
// xcrun altool --list-providers -u APPLE_ID -p APPLE_ID_PASSWORD
|
||||
// ascProvider: process.env.APPLE_ASC_PROVIDER,
|
||||
// In our case, the team ID is the same as the legacy ASC_PROVIDER
|
||||
teamId: process.env.APPLE_ASC_PROVIDER,
|
||||
tool: 'notarytool',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
clearInterval(waitingIntervalId);
|
||||
// It appears that electron-notarize doesn't staple the app, but without
|
||||
// this we were still getting the malware warning when launching the app.
|
||||
// Stapling the app means attaching the notarization ticket to it, so that
|
||||
// if the user is offline, macOS can still check if the app was notarized.
|
||||
// So it seems to be more or less optional, but at least in our case it
|
||||
// wasn't.
|
||||
console.info('Stapling notarization ticket to the file...');
|
||||
const staplerCmd = `xcrun stapler staple "${filePath}"`;
|
||||
console.info(`> ${staplerCmd}`);
|
||||
console.info(await execCommand(staplerCmd));
|
||||
console.info(`Validating stapled file: ${filePath}`);
|
||||
try {
|
||||
await execAsync(`spctl -a -vv -t install "${filePath}"`);
|
||||
} catch (error) {
|
||||
console.error(`Failed validating stapled file: ${filePath}:`, error);
|
||||
}
|
||||
console.info(`Done notarizing ${filePath}`);
|
||||
}
|
||||
// # sourceMappingURL=notarizeFile.js.map
|
||||
@@ -132,6 +132,17 @@ android {
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
profileable {
|
||||
// Release-like build that allows profiling with Android Studio Profiler
|
||||
initWith release
|
||||
signingConfig signingConfigs.debug
|
||||
// Required for Android Studio Profiler to attach
|
||||
debuggable false
|
||||
// Keeps symbols for better stack traces in profiler
|
||||
minifyEnabled false
|
||||
// Use release variants of dependencies that don't have profileable
|
||||
matchingFallbacks = ['release']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
android:theme="@style/AppTheme"
|
||||
android:supportsRtl="true">
|
||||
|
||||
<!-- Enable profiling in release builds (Android 10+) -->
|
||||
<profileable android:shell="true" />
|
||||
|
||||
<!--
|
||||
2018-12-16: Changed android:launchMode from "singleInstance" to "singleTop" for Firebase notification
|
||||
Previously singleInstance was necessary to prevent multiple instance of the RN app from running at the same time, but maybe no longer needed.
|
||||
|
||||
@@ -345,6 +345,7 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf",
|
||||
@@ -364,6 +365,7 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
|
||||
|
||||
@@ -1406,7 +1406,7 @@ PODS:
|
||||
- React-jsiexecutor
|
||||
- React-RCTFBReactNativeSpec
|
||||
- ReactCommon/turbomodule/core
|
||||
- react-native-alarm-notification (3.5.0):
|
||||
- react-native-alarm-notification (3.6.0):
|
||||
- React
|
||||
- react-native-document-picker (10.1.7):
|
||||
- DoubleConversion
|
||||
@@ -1514,7 +1514,7 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-rsa-native (2.0.5):
|
||||
- React
|
||||
- react-native-saf-x (3.5.1):
|
||||
- react-native-saf-x (3.6.0):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (5.6.1):
|
||||
- React-Core
|
||||
@@ -1904,7 +1904,7 @@ PODS:
|
||||
- React-Core
|
||||
- RNDateTimePicker (8.4.5):
|
||||
- React-Core
|
||||
- RNDeviceInfo (14.0.4):
|
||||
- RNDeviceInfo (14.1.1):
|
||||
- React-Core
|
||||
- RNExitApp (2.0.0):
|
||||
- React-Core
|
||||
@@ -1912,7 +1912,7 @@ PODS:
|
||||
- React-Core
|
||||
- RNFS (2.20.0):
|
||||
- React-Core
|
||||
- RNLocalize (3.5.2):
|
||||
- RNLocalize (3.5.4):
|
||||
- React-Core
|
||||
- RNQuickAction (0.3.13):
|
||||
- React
|
||||
@@ -2306,7 +2306,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
||||
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
|
||||
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
|
||||
EXAV: ae28256069c4cdde93d185c007d8f68d92902c2e
|
||||
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
||||
Expo: c8f323f74218c45c46e27eed40d8a53ba50667c3
|
||||
@@ -2319,7 +2319,7 @@ SPEC CHECKSUMS:
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 84b955f7b4da8b895faf5946f73748267347c975
|
||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
|
||||
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
|
||||
hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe
|
||||
JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7
|
||||
JoplinRNShareExtension: e158a4b53ee0aa9cd3037a16221dc8adbd6f7860
|
||||
@@ -2355,7 +2355,7 @@ SPEC CHECKSUMS:
|
||||
React-logger: 8edfcedc100544791cd82692ca5a574240a16219
|
||||
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
|
||||
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
|
||||
react-native-alarm-notification: a4326a743df72a94d361a4c3a21515556f650341
|
||||
react-native-alarm-notification: 846df1df72eca38e711409b9c064a5c635ff1c32
|
||||
react-native-document-picker: b6419b766863408dacbdf5e97b2f3a694c611150
|
||||
react-native-geolocation: ec15ffebc53790314885eb9e5f2132132fbc2600
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
@@ -2364,7 +2364,7 @@ SPEC CHECKSUMS:
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-quick-crypto: b475b71e7fa4dbf3446be55e8ad4ef2c58ac4f7f
|
||||
react-native-rsa-native: a7931cdda1f73a8576a46d7f431378c5550f0c38
|
||||
react-native-saf-x: 404f0f9a29cc6bf21d88582e054c45a11b28c22b
|
||||
react-native-saf-x: 50d176763ed692b379c190bf55ae7293a3ee09bb
|
||||
react-native-safe-area-context: 2243039f43d10cb1ea30ec5ac57fc6d1448413f4
|
||||
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
|
||||
react-native-vector-icons: a45ecc326ec090450f152dfc7076ce1173331ce5
|
||||
@@ -2409,11 +2409,11 @@ SPEC CHECKSUMS:
|
||||
RNCClipboard: f6679d470d0da2bce2a37b0af7b9e0bf369ecda5
|
||||
RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440
|
||||
RNDateTimePicker: 8c12d12e8660697c2e176d2f98775764431c141f
|
||||
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
|
||||
RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c
|
||||
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
|
||||
RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251
|
||||
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
|
||||
RNLocalize: 3c4d0abd777a546fa77bdb6caef85a87fb9ea349
|
||||
RNLocalize: d7859f87f1083349c73aa089e360af33ef89efc2
|
||||
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
|
||||
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
|
||||
RNShare: 40ace3f87cd881869e8085aced9dc16b425c74aa
|
||||
|
||||
@@ -35,11 +35,11 @@
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-documents/picker": "10.1.7",
|
||||
"@react-native-vector-icons/fontawesome5": "12.3.0",
|
||||
"@react-native-vector-icons/fontawesome5": "patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch",
|
||||
"@react-native-vector-icons/get-image": "12.3.0",
|
||||
"@react-native-vector-icons/ionicons": "12.3.0",
|
||||
"@react-native-vector-icons/material-design-icons": "12.4.0",
|
||||
"@react-native-vector-icons/material-icons": "12.4.0",
|
||||
"@react-native-vector-icons/ionicons": "patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch",
|
||||
"@react-native-vector-icons/material-design-icons": "patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch",
|
||||
"@react-native-vector-icons/material-icons": "patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch",
|
||||
"assert-browserify": "2.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"color": "3.2.1",
|
||||
@@ -59,7 +59,7 @@
|
||||
"punycode": "2.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-device-info": "14.0.4",
|
||||
"react-native-device-info": "14.1.1",
|
||||
"react-native-dropdownalert": "5.2.0",
|
||||
"react-native-exit-app": "2.0.0",
|
||||
"react-native-file-viewer": "2.1.5",
|
||||
@@ -114,7 +114,7 @@
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "19.0.14",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/serviceworker": "0.0.158",
|
||||
"@types/serviceworker": "0.0.160",
|
||||
"@types/tar-stream": "3.1.4",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "9.1.3",
|
||||
|
||||
@@ -11,7 +11,6 @@ import Alarm from '@joplin/lib/models/Alarm';
|
||||
import time from '@joplin/lib/time';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import NoteScreen from './components/screens/Note/Note';
|
||||
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
|
||||
import Setting, { } from '@joplin/lib/models/Setting';
|
||||
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
||||
import { NotesParent, serializeNotesParent } from '@joplin/lib/reducer';
|
||||
@@ -36,15 +35,6 @@ import reduxSharedMiddleware from '@joplin/lib/components/shared/reduxSharedMidd
|
||||
const { AppNav } = require('./components/app-nav.js');
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import NotesScreen from './components/screens/Notes/Notes';
|
||||
import TagsScreen from './components/screens/tags';
|
||||
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
|
||||
const { FolderScreen } = require('./components/screens/folder.js');
|
||||
import LogScreen from './components/screens/LogScreen';
|
||||
import StatusScreen from './components/screens/status';
|
||||
import SearchScreen from './components/screens/SearchScreen';
|
||||
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
|
||||
import EncryptionConfigScreen from './components/screens/encryption-config';
|
||||
import DropboxLoginScreen from './components/screens/dropbox-login.js';
|
||||
import { MenuProvider } from 'react-native-popup-menu';
|
||||
import SideMenu, { SideMenuPosition } from './components/SideMenu';
|
||||
import SideMenuContent from './components/side-menu-content';
|
||||
@@ -63,11 +53,47 @@ const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
|
||||
import SyncTargetJoplinServerSAML from '@joplin/lib/SyncTargetJoplinServerSAML';
|
||||
import BiometricPopup from './components/biometrics/BiometricPopup';
|
||||
import { isCallbackUrl, parseCallbackUrl, CallbackUrlCommand } from '@joplin/lib/callbackUrlUtils';
|
||||
import JoplinCloudLoginScreen from './components/screens/JoplinCloudLoginScreen';
|
||||
|
||||
import SyncTargetNone from '@joplin/lib/SyncTargetNone';
|
||||
|
||||
// Lazy-loaded screens for faster startup
|
||||
const TagsScreen = React.lazy(() => import('./components/screens/tags'));
|
||||
const ConfigScreen = React.lazy(() => import('./components/screens/ConfigScreen/ConfigScreen'));
|
||||
const FolderScreen = React.lazy(async () => {
|
||||
// @ts-expect-error JS file without type declarations
|
||||
const m: { FolderScreen: React.ComponentType } = await import('./components/screens/folder.js');
|
||||
return { default: m.FolderScreen };
|
||||
});
|
||||
const LogScreen = React.lazy(() => import('./components/screens/LogScreen'));
|
||||
const StatusScreen = React.lazy(() => import('./components/screens/status'));
|
||||
const SearchScreen = React.lazy(() => import('./components/screens/SearchScreen'));
|
||||
const OneDriveLoginScreen = React.lazy(async () => {
|
||||
// @ts-expect-error JS file without type declarations
|
||||
const m: { OneDriveLoginScreen: React.ComponentType } = await import('./components/screens/onedrive-login.js');
|
||||
return { default: m.OneDriveLoginScreen };
|
||||
});
|
||||
const EncryptionConfigScreen = React.lazy(() => import('./components/screens/encryption-config'));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JS file without type declarations
|
||||
const DropboxLoginScreen = React.lazy(async (): Promise<{ default: any }> => {
|
||||
return await import('./components/screens/dropbox-login.js');
|
||||
});
|
||||
const JoplinCloudLoginScreen = React.lazy(() => import('./components/screens/JoplinCloudLoginScreen'));
|
||||
const UpgradeSyncTargetScreen = React.lazy(() => import('./components/screens/UpgradeSyncTargetScreen'));
|
||||
const ShareManager = React.lazy(() => import('./components/screens/ShareManager'));
|
||||
const ProfileSwitcher = React.lazy(() => import('./components/ProfileSwitcher/ProfileSwitcher'));
|
||||
const ProfileEditor = React.lazy(() => import('./components/ProfileSwitcher/ProfileEditor'));
|
||||
const NoteRevisionViewer = React.lazy(() => import('./components/screens/NoteRevisionViewer'));
|
||||
const DocumentScanner = React.lazy(() => import('./components/screens/DocumentScanner/DocumentScanner'));
|
||||
const SyncWizard = React.lazy(() => import('./components/SyncWizard/SyncWizard'));
|
||||
|
||||
// SsoLoginScreen needs special handling due to its factory pattern
|
||||
const SsoLoginScreen = React.lazy(async () => {
|
||||
const [{ default: SsoLoginScreenFactory }, { default: SamlShared }] = await Promise.all([
|
||||
import('./components/screens/SsoLoginScreen'),
|
||||
import('@joplin/lib/components/shared/SamlShared'),
|
||||
]);
|
||||
return { default: SsoLoginScreenFactory(new SamlShared()) };
|
||||
});
|
||||
|
||||
SyncTargetRegistry.addClass(SyncTargetNone);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
@@ -85,29 +111,21 @@ import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import setupNotifications from './utils/setupNotifications';
|
||||
import { loadMasterKeysFromSettings } from '@joplin/lib/services/e2ee/utils';
|
||||
import { Theme, ThemeAppearance } from '@joplin/lib/themes/type';
|
||||
import ProfileSwitcher from './components/ProfileSwitcher/ProfileSwitcher';
|
||||
import ProfileEditor from './components/ProfileSwitcher/ProfileEditor';
|
||||
import sensorInfo, { SensorInfo } from './components/biometrics/sensorInfo';
|
||||
import { setDispatch } from './services/profiles';
|
||||
import { ReactNode } from 'react';
|
||||
import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTheme';
|
||||
import PluginRunnerWebView from './components/plugins/PluginRunnerWebView';
|
||||
import { refreshFolders, scheduleRefreshFolders } from '@joplin/lib/folders-screen-utils';
|
||||
import ShareManager from './components/screens/ShareManager';
|
||||
import { setDateFormat, setTimeFormat, setTimeLocale } from '@joplin/utils/time';
|
||||
import DialogManager from './components/DialogManager';
|
||||
import { AppState } from './utils/types';
|
||||
import { getDisplayParentId } from '@joplin/lib/services/trash';
|
||||
import PluginNotification from './components/plugins/PluginNotification';
|
||||
import FocusControl from './components/accessibility/FocusControl/FocusControl';
|
||||
import SsoLoginScreen from './components/screens/SsoLoginScreen';
|
||||
import SamlShared from '@joplin/lib/components/shared/SamlShared';
|
||||
import NoteRevisionViewer from './components/screens/NoteRevisionViewer';
|
||||
import DocumentScanner from './components/screens/DocumentScanner/DocumentScanner';
|
||||
import buildStartupTasks from './utils/buildStartupTasks';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import appReducer from './utils/appReducer';
|
||||
import SyncWizard from './components/SyncWizard/SyncWizard';
|
||||
|
||||
const logger = Logger.create('root');
|
||||
const perfLogger = PerformanceLogger.create();
|
||||
@@ -711,7 +729,7 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
OneDriveLogin: { screen: OneDriveLoginScreen },
|
||||
DropboxLogin: { screen: DropboxLoginScreen },
|
||||
JoplinCloudLogin: { screen: JoplinCloudLoginScreen },
|
||||
JoplinServerSamlLogin: { screen: SsoLoginScreen(new SamlShared()) },
|
||||
JoplinServerSamlLogin: { screen: SsoLoginScreen },
|
||||
EncryptionConfig: { screen: EncryptionConfigScreen },
|
||||
UpgradeSyncTarget: { screen: UpgradeSyncTargetScreen },
|
||||
ShareManager: { screen: ShareManager },
|
||||
@@ -759,11 +777,15 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: '100%' }}>
|
||||
<SafeAreaView style={{ flex: 1 }} titleBarUnderlayColor={theme.backgroundColor2}>
|
||||
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
|
||||
{ shouldShowMainContent && <AppNav screens={appNavInit} dispatch={this.props.dispatch} /> }
|
||||
<React.Suspense fallback={<View/>}>
|
||||
{ shouldShowMainContent && <AppNav screens={appNavInit} dispatch={this.props.dispatch} /> }
|
||||
</React.Suspense>
|
||||
</View>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied */}
|
||||
<DropdownAlert alert={(func: any) => (this.dropdownAlert_ = func)} />
|
||||
<SyncWizard/>
|
||||
<React.Suspense fallback={null}>
|
||||
<SyncWizard/>
|
||||
</React.Suspense>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</SideMenu>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PluginAssetsLoader from '../PluginAssetsLoader';
|
||||
import AlarmService from '@joplin/lib/services/AlarmService';
|
||||
import Logger, { TargetType } from '@joplin/utils/Logger';
|
||||
import Logger, { LogLevel, TargetType } from '@joplin/utils/Logger';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import BaseService from '@joplin/lib/services/BaseService';
|
||||
import ResourceService from '@joplin/lib/services/ResourceService';
|
||||
@@ -200,11 +200,8 @@ const buildStartupTasks = (
|
||||
const mainLogger = new Logger();
|
||||
mainLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
|
||||
mainLogger.setLevel(Logger.LEVEL_INFO);
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
mainLogger.addTarget(TargetType.Console);
|
||||
mainLogger.setLevel(Logger.LEVEL_DEBUG);
|
||||
}
|
||||
mainLogger.addTarget(TargetType.Console);
|
||||
mainLogger.setLevel(Setting.value('env') === 'dev' ? LogLevel.Debug : LogLevel.Info);
|
||||
|
||||
Logger.initializeGlobalLogger(mainLogger);
|
||||
initLib(mainLogger);
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"url": "git+https://github.com/laurent22/joplin.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/yargs": "17.0.33",
|
||||
"joplin-plugin-freehand-drawing": "4.2.0",
|
||||
"@types/yargs": "17.0.34",
|
||||
"joplin-plugin-freehand-drawing": "4.3.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
|
||||
@@ -3,39 +3,17 @@ import { EditorSelection } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import uslug from '@joplin/fork-uslug/lib/uslug';
|
||||
import { SyntaxNodeRef } from '@lezer/common';
|
||||
import htmlNodeInfo from '../utils/htmlNodeInfo';
|
||||
|
||||
const jumpToHash = (view: EditorView, hash: string) => {
|
||||
const state = view.state;
|
||||
const timeout = 1_000; // Maximum time to spend parsing the syntax tree
|
||||
let targetLocation: number|undefined = undefined;
|
||||
|
||||
const removeQuotes = (quoted: string) => quoted.replace(/^["'](.*)["']$/, '$1');
|
||||
|
||||
const makeEnterNode = (offset: number) => (node: SyntaxNodeRef) => {
|
||||
const nodeToText = (node: SyntaxNodeRef) => {
|
||||
return state.sliceDoc(node.from + offset, node.to + offset);
|
||||
};
|
||||
// Returns the attribute with the given name for [node]
|
||||
const getHtmlNodeAttr = (node: SyntaxNodeRef, attrName: string) => {
|
||||
if (node.from === node.to) return null; // Empty
|
||||
const content = node.node.resolveInner(node.from + 1);
|
||||
|
||||
// Search for the "id" attribute
|
||||
const attributes = content.getChildren('Attribute');
|
||||
for (const attribute of attributes) {
|
||||
const nameNode = attribute.getChild('AttributeName');
|
||||
const valueNode = attribute.getChild('AttributeValue');
|
||||
|
||||
if (nameNode && valueNode) {
|
||||
const name = nodeToText(nameNode).toLowerCase().replace(/^"(.*)"$/, '$1');
|
||||
if (name === attrName) {
|
||||
return removeQuotes(nodeToText(valueNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const found = targetLocation !== undefined;
|
||||
if (found) return false; // Skip this node
|
||||
@@ -46,13 +24,14 @@ const jumpToHash = (view: EditorView, hash: string) => {
|
||||
.replace(/^#+\s/, '') // Leading #s in headers
|
||||
.replace(/\n-+$/, ''); // Trailing --s in headers
|
||||
matches = hash === uslug(nodeText);
|
||||
} else if (node.name === 'HTMLTag' || node.name === 'HTMLBlock') {
|
||||
} else if (node.name === 'HTMLBlock') {
|
||||
// CodeMirror adds HTML information to Markdown documents using overlays attached
|
||||
// to HTMLTag and HTMLBlock nodes.
|
||||
// Use .enter to enter the overlay and visit the HTML nodes:
|
||||
node.node.enter(node.from, 1).toTree().iterate({ enter: makeEnterNode(node.from) });
|
||||
} else if (node.name === 'OpenTag') {
|
||||
matches = getHtmlNodeAttr(node, 'id') === hash || getHtmlNodeAttr(node, 'name') === hash;
|
||||
} else if (node.name === 'OpenTag' || node.name === 'HTMLTag') {
|
||||
const htmlNodeDetails = htmlNodeInfo(node, state);
|
||||
matches = htmlNodeDetails.getAttr('id') === hash || htmlNodeDetails.getAttr('name') === hash;
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import replaceBulletLists from './replaceBulletLists';
|
||||
import replaceCheckboxes from './replaceCheckboxes';
|
||||
import replaceDividers from './replaceDividers';
|
||||
import replaceFormatCharacters from './replaceFormatCharacters';
|
||||
import replaceInlineHtml from './replaceInlineHtml';
|
||||
|
||||
export default () => {
|
||||
return [
|
||||
@@ -13,5 +14,6 @@ export default () => {
|
||||
replaceBackslashEscapes,
|
||||
replaceDividers,
|
||||
addFormattingClasses,
|
||||
replaceInlineHtml,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import createTestEditor from '../../testing/createTestEditor';
|
||||
import replaceInlineHtml from './replaceInlineHtml';
|
||||
|
||||
const createEditor = async (initialMarkdown: string, expectedTags: string[] = ['HTMLTag']) => {
|
||||
const editor = await createTestEditor(
|
||||
initialMarkdown,
|
||||
EditorSelection.cursor(0),
|
||||
expectedTags,
|
||||
[replaceInlineHtml],
|
||||
);
|
||||
return editor;
|
||||
};
|
||||
|
||||
describe('replaceInlineHtml', () => {
|
||||
test.each([
|
||||
{ markdown: '<sup>Test</sup>', expectedTagsQuery: 'sup' },
|
||||
{ markdown: '<strike>Test</strike>', expectedTagsQuery: 'strike' },
|
||||
{ markdown: 'Test: <span style="color: red;">Test</span>', expectedTagsQuery: 'span[style]' },
|
||||
{ markdown: 'Test: <span style="color: rgb(123, 0, 0);">Test</span>', expectedTagsQuery: 'span[style]' },
|
||||
])('should render inline HTML (case %#)', async ({ markdown, expectedTagsQuery }) => {
|
||||
// Add additional newlines: Ensure that the cursor isn't initially on the same line as the content to be rendered:
|
||||
const editor = await createEditor(`\n\n${markdown}\n\n`);
|
||||
|
||||
expect(editor.contentDOM.querySelector(expectedTagsQuery)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import makeInlineReplaceExtension from './utils/makeInlineReplaceExtension';
|
||||
import { Decoration } from '@codemirror/view';
|
||||
import htmlNodeInfo, { HtmlNodeInfo } from '../../utils/htmlNodeInfo';
|
||||
import { SyntaxNodeRef } from '@lezer/common';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
|
||||
const hideDecoration = Decoration.replace({});
|
||||
|
||||
type OnRenderTagContent = (openingTag: HtmlNodeInfo)=> Decoration;
|
||||
const createHtmlReplacementExtension = (tagName: string, onRenderContent: OnRenderTagContent) => {
|
||||
const isMatchingTag = (info: HtmlNodeInfo) => {
|
||||
return info.tagName().toLowerCase() === tagName;
|
||||
};
|
||||
const isMatchingOpeningTag = (info: HtmlNodeInfo) => {
|
||||
return isMatchingTag(info) && info.opening;
|
||||
};
|
||||
const isMatchingClosingTag = (info: HtmlNodeInfo) => {
|
||||
return isMatchingTag(info) && info.closing;
|
||||
};
|
||||
|
||||
const findClosingTag = (openingTag: SyntaxNodeRef, state: EditorState) => {
|
||||
const openingTagInfo = htmlNodeInfo(openingTag, state);
|
||||
// Self-closing?
|
||||
if (openingTagInfo.closing) {
|
||||
return openingTag;
|
||||
}
|
||||
|
||||
let cursor = openingTag.node.nextSibling;
|
||||
let nestedTagCounter = 1;
|
||||
|
||||
// Find the matching closing tag
|
||||
for (; !!cursor && nestedTagCounter > 0; cursor = cursor.nextSibling) {
|
||||
const info = htmlNodeInfo(cursor, state);
|
||||
if (isMatchingOpeningTag(info)) {
|
||||
nestedTagCounter ++;
|
||||
} else if (isMatchingClosingTag(info)) {
|
||||
nestedTagCounter --;
|
||||
}
|
||||
|
||||
if (nestedTagCounter === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return cursor;
|
||||
};
|
||||
|
||||
const hideTags = makeInlineReplaceExtension({
|
||||
createDecoration: (node, state) => {
|
||||
const info = htmlNodeInfo(node, state);
|
||||
return info && isMatchingTag(info) ? hideDecoration : null;
|
||||
},
|
||||
});
|
||||
|
||||
const styleContent = makeInlineReplaceExtension({
|
||||
createDecoration: (node, state) => {
|
||||
const info = htmlNodeInfo(node, state);
|
||||
if (!info || !isMatchingOpeningTag(info)) return null;
|
||||
return onRenderContent(info);
|
||||
},
|
||||
getDecorationRange(node, state) {
|
||||
const closingTag = findClosingTag(node, state);
|
||||
|
||||
if (closingTag) {
|
||||
return [node.to, closingTag.from];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return [hideTags, styleContent];
|
||||
};
|
||||
|
||||
|
||||
export default [
|
||||
createHtmlReplacementExtension('sub', () => Decoration.mark({ tagName: 'sub' })),
|
||||
createHtmlReplacementExtension('sup', () => Decoration.mark({ tagName: 'sup' })),
|
||||
createHtmlReplacementExtension('strike', () => Decoration.mark({ tagName: 'strike' })),
|
||||
createHtmlReplacementExtension('span', (info) => {
|
||||
const styles = info.getAttr('style') ?? '';
|
||||
const colorMatch = styles.match(/color:\s*(#?[a-z0-9A-Z]+|rgba?\([0-9, ]+\))(;|$)/);
|
||||
|
||||
return Decoration.mark({
|
||||
attributes: {
|
||||
style: colorMatch ? `color: ${colorMatch[1]};` : '',
|
||||
},
|
||||
});
|
||||
}),
|
||||
].flat();
|
||||
88
packages/editor/CodeMirror/utils/htmlNodeInfo.ts
Normal file
88
packages/editor/CodeMirror/utils/htmlNodeInfo.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { SyntaxNodeRef } from '@lezer/common';
|
||||
|
||||
export interface HtmlNodeInfo {
|
||||
node: SyntaxNodeRef;
|
||||
opening: boolean;
|
||||
closing: boolean;
|
||||
from: number;
|
||||
to: number;
|
||||
tagName: ()=> string;
|
||||
getAttr: (attributeName: string)=> string;
|
||||
}
|
||||
|
||||
type OnGetNodeContent = (node: SyntaxNodeRef)=> string;
|
||||
|
||||
const removeQuotes = (quoted: string) => quoted.replace(/^["'](.*)["']$/, '$1');
|
||||
|
||||
const getHtmlNodeAttr = (node: SyntaxNodeRef, attrName: string, getText: OnGetNodeContent) => {
|
||||
if (node.from === node.to) return null; // Empty
|
||||
const content = node.node.resolveInner(node.from + 1);
|
||||
|
||||
// Search for the "id" attribute
|
||||
const attributes = content.getChildren('Attribute');
|
||||
for (const attribute of attributes) {
|
||||
const nameNode = attribute.getChild('AttributeName');
|
||||
const valueNode = attribute.getChild('AttributeValue');
|
||||
|
||||
if (nameNode && valueNode) {
|
||||
const name = getText(nameNode).toLowerCase().replace(/^"(.*)"$/, '$1');
|
||||
if (name === attrName) {
|
||||
return removeQuotes(getText(valueNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Utility function to access CodeMirror HTML node information, based on
|
||||
// the corresponding Markdown node.
|
||||
const htmlNodeInfo = (node: SyntaxNodeRef, state: EditorState, offset = 0): HtmlNodeInfo|null => {
|
||||
// Already an HTML node?
|
||||
if (node.name === 'OpenTag' || node.name === 'CloseTag' || node.name === 'SelfClosingTag') {
|
||||
const getNodeText = (childNode: SyntaxNodeRef) => state.sliceDoc(childNode.from + offset, childNode.to + offset);
|
||||
const selfClosing = node.name === 'SelfClosingTag';
|
||||
|
||||
return {
|
||||
node,
|
||||
opening: node.name === 'OpenTag' || selfClosing,
|
||||
closing: node.name === 'CloseTag' || selfClosing,
|
||||
from: node.from + offset,
|
||||
to: node.to + offset,
|
||||
tagName: () => {
|
||||
const nodeText = getNodeText(node).trim();
|
||||
const tagNameMatch = nodeText.match(/^<\/?([^>\s]+)/);
|
||||
if (tagNameMatch) {
|
||||
return tagNameMatch[1];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getAttr: (name: string) => {
|
||||
return getHtmlNodeAttr(node, name, getNodeText);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Convert Markdown HTML nodes to HTML nodes
|
||||
if (node.name === 'HTMLTag' || node.name === 'HTMLBlock') {
|
||||
const globalOffset = node.from + offset;
|
||||
let resolved: HtmlNodeInfo|null = null;
|
||||
|
||||
// CodeMirror adds HTML information to Markdown documents using overlays attached
|
||||
// to HTMLTag and HTMLBlock nodes.
|
||||
// Use .enter to enter the overlay and visit the HTML nodes:
|
||||
node.node.enter(node.from, 1).toTree().iterate({
|
||||
enter: (subNode) => {
|
||||
resolved ??= htmlNodeInfo(subNode, state, globalOffset);
|
||||
return !resolved;
|
||||
},
|
||||
});
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default htmlNodeInfo;
|
||||
@@ -314,7 +314,11 @@ export default class JoplinDatabase extends Database {
|
||||
throw new Error(`\`notes_fts\` (${countFieldsNotesFts} fields) must have the same number of fields as \`items_fts\` (${countFieldsItemsFts} fields) for the search engine BM25 algorithm to work`);
|
||||
}
|
||||
|
||||
const tableRows = await this.selectAll('SELECT name FROM sqlite_master WHERE type=\'table\'');
|
||||
interface TableRow {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const tableRows: TableRow[] = await this.selectAll('SELECT name FROM sqlite_master WHERE type=\'table\'');
|
||||
|
||||
for (let i = 0; i < tableRows.length; i++) {
|
||||
let pragmas: Row[] = [];
|
||||
@@ -322,7 +326,7 @@ export default class JoplinDatabase extends Database {
|
||||
try {
|
||||
if (tableName === 'android_metadata') continue;
|
||||
if (tableName === 'table_fields') continue;
|
||||
if (tableName === 'sqlite_sequence') continue;
|
||||
if (tableName.startsWith('sqlite_')) continue;
|
||||
if (tableName.indexOf('notes_fts') === 0) continue;
|
||||
if (tableName.indexOf('items_fts') === 0) continue;
|
||||
if (tableName === 'notes_spellfix') continue;
|
||||
|
||||
@@ -130,7 +130,7 @@ export default class PerformanceLogger {
|
||||
|
||||
const startTime = performance.now();
|
||||
this.lastLogTime_ = startTime;
|
||||
PerformanceLogger.logDebug_(`${name}: Start at ${formatAbsoluteTime(startTime)}`);
|
||||
PerformanceLogger.log_(`${name}: Start at ${formatAbsoluteTime(startTime)}`);
|
||||
|
||||
const onEnd = () => {
|
||||
const now = performance.now();
|
||||
@@ -140,12 +140,7 @@ export default class PerformanceLogger {
|
||||
performance.measure(name, `${uniqueTaskId}-start`, `${uniqueTaskId}-end`);
|
||||
}
|
||||
|
||||
const duration = now - startTime;
|
||||
// Increase the log level for long-running tasks
|
||||
const isLong = duration >= Second / 10;
|
||||
const log = isLong ? PerformanceLogger.log_ : PerformanceLogger.logDebug_;
|
||||
|
||||
log(`${name}: End at ${formatAbsoluteTime(now)} (took ${formatTaskDuration(now - startTime)})`);
|
||||
PerformanceLogger.log_(`${name}: End at ${formatAbsoluteTime(now)} (took ${formatTaskDuration(now - startTime)})`);
|
||||
};
|
||||
return {
|
||||
onEnd,
|
||||
|
||||
@@ -26,11 +26,6 @@ export interface ArchiveExtractOptions {
|
||||
extractTo: string;
|
||||
}
|
||||
|
||||
export interface CabExtractOptions extends ArchiveExtractOptions {
|
||||
// Only files matching the pattern will be extracted
|
||||
fileNamePattern: string;
|
||||
}
|
||||
|
||||
export interface ZipEntry {
|
||||
entryName: string;
|
||||
name: string;
|
||||
@@ -276,8 +271,4 @@ export default class FsDriverBase {
|
||||
public async zipExtract(_options: ArchiveExtractOptions): Promise<ZipEntry[]> {
|
||||
throw new Error('Not implemented: zipExtract');
|
||||
}
|
||||
|
||||
public async cabExtract(_options: CabExtractOptions) {
|
||||
throw new Error('Not implemented: cabExtract.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import AdmZip = require('adm-zip');
|
||||
import FsDriverBase, { Stat, ZipEntry, ArchiveExtractOptions, CabExtractOptions } from './fs-driver-base';
|
||||
import FsDriverBase, { Stat, ZipEntry, ArchiveExtractOptions } from './fs-driver-base';
|
||||
import time from './time';
|
||||
import { execCommand } from '@joplin/utils';
|
||||
import { extname } from 'path';
|
||||
const md5File = require('md5-file');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
@@ -218,25 +216,4 @@ export default class FsDriverNode extends FsDriverBase {
|
||||
zip.extractAllTo(options.extractTo, false);
|
||||
return zip.getEntries();
|
||||
}
|
||||
|
||||
public async cabExtract(options: CabExtractOptions) {
|
||||
if (process.platform !== 'win32') {
|
||||
throw new Error('Extracting CAB archives is only supported on Windows.');
|
||||
}
|
||||
|
||||
const source = this.resolve(options.source);
|
||||
const extractTo = this.resolve(options.extractTo);
|
||||
|
||||
if (extname(source).toLowerCase() !== '.cab') {
|
||||
throw new Error(`Invalid file extension. Expected .CAB. Was ${extname(source)}`);
|
||||
}
|
||||
|
||||
// See https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/expand
|
||||
await execCommand([
|
||||
'expand.exe',
|
||||
source,
|
||||
`-f:${options.fileNamePattern}`,
|
||||
extractTo,
|
||||
], { quiet: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +145,7 @@ export default class InteropService {
|
||||
fileExtensions: [
|
||||
'zip',
|
||||
'one',
|
||||
// .onepkg is a CAB archive, which Joplin can currently only extract on Windows
|
||||
...(shim.isWindows() ? ['onepkg'] : []),
|
||||
'onepkg',
|
||||
],
|
||||
sources: [FileSystemItem.File],
|
||||
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
|
||||
|
||||
@@ -42,6 +42,19 @@ const normalizeNoteForSnapshot = (body: string) => {
|
||||
return removeItemIds(removeDefaultCss(body));
|
||||
};
|
||||
|
||||
// A single Markdown string is much easier to visually compare during snapshot testing.
|
||||
// Prefer notesToMarkdownString to normalizeNoteForSnapshot when the exact output HTML
|
||||
// doesn't matter.
|
||||
const notesToMarkdownString = (notes: NoteEntity[]) => {
|
||||
const converter = new HtmlToMd();
|
||||
return notes.map(note => {
|
||||
return [
|
||||
`# Note: ${note.title}`,
|
||||
converter.parse(normalizeNoteForSnapshot(note.body)),
|
||||
].join('\n\n');
|
||||
}).sort().join('\n\n\n');
|
||||
};
|
||||
|
||||
// This file is ignored if not running in CI. Look at onenote-converter/README.md and jest.config.js for more information
|
||||
describe('InteropService_Importer_OneNote', () => {
|
||||
let tempDir: string;
|
||||
@@ -329,4 +342,10 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
|
||||
expect(normalizeNoteForSnapshot(importedNote.body)).toMatchSnapshot('EmbeddedFiles');
|
||||
});
|
||||
|
||||
it('should correctly import .onepkg notebooks', async () => {
|
||||
const notes = await importNote(`${supportDir}/onenote/test.onepkg`);
|
||||
|
||||
expect(notesToMarkdownString(notes)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,30 +47,13 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
|
||||
if (fileExtension === '.zip') {
|
||||
logger.info('Unzipping files...');
|
||||
await shim.fsDriver().zipExtract({ source: sourcePath, extractTo: targetPath });
|
||||
} else if (fileExtension === '.one') {
|
||||
} else if (fileExtension === '.one' || fileExtension === '.onepkg') {
|
||||
logger.info('Copying file...');
|
||||
|
||||
const outputDirectory = join(targetPath, fileNameNoExtension);
|
||||
await shim.fsDriver().mkdir(outputDirectory);
|
||||
|
||||
await shim.fsDriver().copy(sourcePath, join(outputDirectory, basename(sourcePath)));
|
||||
} else if (fileExtension === '.onepkg') {
|
||||
// Change the file extension so that the archive can be extracted
|
||||
const archivePath = join(targetPath, `${fileNameNoExtension}.cab`);
|
||||
await shim.fsDriver().copy(sourcePath, archivePath);
|
||||
|
||||
const extractPath = join(targetPath, fileNameNoExtension);
|
||||
await shim.fsDriver().mkdir(extractPath);
|
||||
|
||||
await shim.fsDriver().cabExtract({
|
||||
source: archivePath,
|
||||
extractTo: extractPath,
|
||||
// Only the .one files are used--there's no need to extract
|
||||
// other files.
|
||||
fileNamePattern: '*.one',
|
||||
});
|
||||
|
||||
await this.fixIncorrectLatin1Decoding_(extractPath);
|
||||
} else {
|
||||
throw new Error(`Unknown file extension: ${fileExtension}`);
|
||||
}
|
||||
@@ -101,7 +84,7 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
|
||||
const notebookFilePath = join(unzipTempDirectory, notebookFile.path);
|
||||
// In some cases, the OneNote zip file can include folders and other files
|
||||
// that shouldn't be imported directly. Skip these:
|
||||
if (!['.one', '.onetoc2'].includes(extname(notebookFilePath).toLowerCase())) {
|
||||
if (!['.one', '.onepkg', '.onetoc2'].includes(extname(notebookFilePath).toLowerCase())) {
|
||||
logger.info('Skipping non-OneNote file:', notebookFile.path);
|
||||
skippedFiles.push(notebookFile.path);
|
||||
continue;
|
||||
@@ -323,47 +306,4 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
|
||||
changed: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Works around a decoding issue in which file names are extracted as latin1 strings,
|
||||
// rather than UTF-8 strings. For example, OneNote seems to encode filenames as UTF-8 in .onepkg files.
|
||||
// However, EXPAND.EXE reads the filenames as latin1. As a result, "é.one" becomes
|
||||
// "é.one" when extracted from the archive.
|
||||
// This workaround re-encodes filenames as UTF-8.
|
||||
private async fixIncorrectLatin1Decoding_(parentDir: string) {
|
||||
// Only seems to be necessary on Windows.
|
||||
if (!shim.isWindows()) return;
|
||||
|
||||
const fixEncoding = async (basePath: string, fileName: string) => {
|
||||
const originalPath = join(basePath, fileName);
|
||||
let newPath;
|
||||
|
||||
let fixedFileName = Buffer.from(fileName, 'latin1').toString('utf8');
|
||||
if (fixedFileName !== fileName) {
|
||||
// In general, the path shouldn't start with "."s or contain path separators.
|
||||
// However, if it does, these characters might cause import errors, so remove them:
|
||||
fixedFileName = fixedFileName.replace(/^\.+/, '');
|
||||
fixedFileName = fixedFileName.replace(/[/\\]/g, ' ');
|
||||
|
||||
// Avoid path traversal: Ensure that the file path is contained within the base directory
|
||||
const newFullPathSafe = shim.fsDriver().resolveRelativePathWithinDir(basePath, fixedFileName);
|
||||
await shim.fsDriver().move(originalPath, newFullPathSafe);
|
||||
|
||||
newPath = newFullPathSafe;
|
||||
} else {
|
||||
newPath = originalPath;
|
||||
}
|
||||
|
||||
if (await shim.fsDriver().isDirectory(originalPath)) {
|
||||
const children = await shim.fsDriver().readDirStats(newPath, { recursive: false });
|
||||
for (const child of children) {
|
||||
await fixEncoding(originalPath, child.path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stats = await shim.fsDriver().readDirStats(parentDir, { recursive: false });
|
||||
for (const stat of stats) {
|
||||
await fixEncoding(parentDir, stat.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,133 @@ jeudi 23 octobre 2025
|
||||
- [x] Documenter configuration synchro JBS saml pour un utilisateur (case cochée)"
|
||||
`;
|
||||
|
||||
exports[`InteropService_Importer_OneNote should correctly import .onepkg notebooks 1`] = `
|
||||
"# Note: A
|
||||
|
||||
A
|
||||
|
||||
- [Test](:/id-here "Test")
|
||||
|
||||
|
||||
# Note: A test
|
||||
|
||||
A test
|
||||
|
||||
A test
|
||||
|
||||
Tuesday, January 13, 2026
|
||||
|
||||
1:44 PM
|
||||
|
||||
…test…
|
||||
|
||||
|
||||
# Note: Another section
|
||||
|
||||
Another section
|
||||
|
||||
- [Page 1](:/id-here "Page 1")
|
||||
- [Page 2](:/id-here "Page 2")
|
||||
|
||||
|
||||
# Note: B
|
||||
|
||||
B
|
||||
|
||||
- [Test page](:/id-here "Test page")
|
||||
|
||||
|
||||
# Note: Page 1
|
||||
|
||||
Page 1
|
||||
|
||||
Page 1
|
||||
|
||||
Tuesday, January 13, 2026
|
||||
|
||||
1:42 PM
|
||||
|
||||
Test
|
||||
|
||||
|
||||
# Note: Page 2
|
||||
|
||||
Page 2
|
||||
|
||||
Page 2
|
||||
|
||||
Tuesday, January 13, 2026
|
||||
|
||||
1:42 PM
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
# Note: Test
|
||||
|
||||
Test
|
||||
|
||||
Test
|
||||
|
||||
Tuesday, January 13, 2026
|
||||
|
||||
1:44 PM
|
||||
|
||||
|
||||
# Note: Test page
|
||||
|
||||
Test page
|
||||
|
||||
Test page
|
||||
|
||||
Tuesday, January 13, 2026
|
||||
|
||||
1:45 PM
|
||||
|
||||
|
||||
# Note: Testing…
|
||||
|
||||
Testing…
|
||||
|
||||
Testing…
|
||||
|
||||
Friday, November 28, 2025
|
||||
|
||||
2:47 PM
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Link to page: [Page 2](onenote:https://d.docs.live.net/4c230b31b0dfb50f/Documents/OneNote%20Notebooks/test/Another%20section.one#Page%202§ion-id={C271F3B1-5F22-457F-9DEA-F2B938D9B3D7}&page-id={62800B88-EC08-4170-BDB6-885CBB47FF99}&end)
|
||||
|
||||
|
||||
# Note: Tést!
|
||||
|
||||
Tést!
|
||||
|
||||
- [Testing…](:/id-here "Testing…")
|
||||
- [A test](:/id-here "A test")
|
||||
|
||||
|
||||
# Note: Untitled Page 1
|
||||
|
||||
Untitled Page
|
||||
|
||||
Tuesday, January 13, 2026
|
||||
|
||||
1:45 PM
|
||||
|
||||
|
||||
# Note: ⅀⸨ Unicode ⸩
|
||||
|
||||
⅀⸨ Unicode ⸩
|
||||
|
||||
- [Untitled Page 1](:/id-here "Untitled Page 1")"
|
||||
`;
|
||||
|
||||
exports[`InteropService_Importer_OneNote should correctly import math formulas: Math 1`] = `
|
||||
" Math
|
||||
|
||||
|
||||
@@ -253,16 +253,16 @@ describe('Synchronizer.revisions', () => {
|
||||
const getNoteRevisions = () => {
|
||||
return Revision.allByType(BaseModel.TYPE_NOTE, note.id);
|
||||
};
|
||||
jest.advanceTimersByTime(200);
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
await Note.save({ id: note.id, title: 'note REV0' });
|
||||
jest.advanceTimersByTime(200);
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
await revisionService().collectRevisions(); // REV0
|
||||
expect(await getNoteRevisions()).toHaveLength(1);
|
||||
|
||||
const interimTime = Date.now();
|
||||
jest.advanceTimersByTime(200);
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
await Note.save({ id: note.id, title: 'note REV1' });
|
||||
await revisionService().collectRevisions(); // REV1
|
||||
@@ -273,6 +273,10 @@ describe('Synchronizer.revisions', () => {
|
||||
await switchClient(2);
|
||||
await synchronizerStart();
|
||||
|
||||
// Prevent a race condition whereby a revision is downloaded via the sync, then one of the same revisions is updated within the same millisecond via
|
||||
// deleteOldRevisions, and therefore is not uploaded via the sync because the sync_time matches
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
const revisions = await getNoteRevisions();
|
||||
expect(revisions).toHaveLength(2);
|
||||
expect(revisions[0].title_diff).toBe('[{"diffs":[[1,"note REV0"]],"start1":0,"start2":0,"length1":0,"length2":9}]');
|
||||
|
||||
@@ -459,7 +459,7 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<PlanName, Pla
|
||||
featureLabelsOn: getFeatureLabelsByPlan(PlanName.JoplinServerBusiness, true),
|
||||
featureLabelsOff: getFeatureLabelsByPlan(PlanName.JoplinServerBusiness, false),
|
||||
cfaLabel: _('Get a quote'),
|
||||
cfaUrl: 'mailto:jsb-inquiry@joplin.cloud?subject=Joplin%20Server%20Business%20inquiry',
|
||||
cfaUrl: 'https://tally.so/r/D4BlOE',
|
||||
footnote: '',
|
||||
learnMoreUrl: 'https://joplinapp.org/help/apps/joplin_server_business',
|
||||
hostingType: PlanHostingType.Self,
|
||||
|
||||
108
packages/onenote-converter/Cargo.lock
generated
108
packages/onenote-converter/Cargo.lock
generated
@@ -17,6 +17,12 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.15"
|
||||
@@ -93,7 +99,7 @@ dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.4.4",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
@@ -113,12 +119,30 @@ version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cab"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"flate2",
|
||||
"lzxd",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.96"
|
||||
@@ -168,6 +192,24 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.11.0"
|
||||
@@ -204,6 +246,16 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
@@ -270,6 +322,12 @@ version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "lzxd"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
@@ -302,6 +360,22 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -455,6 +529,12 @@ version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
@@ -553,6 +633,7 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"bytes",
|
||||
"cab",
|
||||
"color-eyre",
|
||||
"console_error_panic_hook",
|
||||
"encoding_rs",
|
||||
@@ -652,6 +733,12 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
@@ -710,6 +797,25 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.40"
|
||||
|
||||
@@ -28,10 +28,31 @@ function normalizeAndWriteFile(filePath, data) {
|
||||
fs.writeFileSync(filePath, data);
|
||||
}
|
||||
|
||||
function fileReader(path) {
|
||||
const fd = fs.openSync(path);
|
||||
const size = fs.fstatSync(fd).size;
|
||||
return {
|
||||
read: (position, length) => {
|
||||
const data = Buffer.alloc(length);
|
||||
const sizeRead = fs.readSync(fd, data, { length, position });
|
||||
|
||||
// Make data.size match the number of bytes read:
|
||||
return data.subarray(0, sizeRead);
|
||||
},
|
||||
size: () => {
|
||||
return size;
|
||||
},
|
||||
close: () => {
|
||||
fs.closeSync(fd);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mkdirSyncRecursive,
|
||||
isDirectory,
|
||||
readDir,
|
||||
removePrefix,
|
||||
normalizeAndWriteFile,
|
||||
fileReader,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
pub type ApiResult<T> = std::result::Result<T, std::io::Error>;
|
||||
pub trait FileHandle: Read + Seek {}
|
||||
|
||||
pub trait FileApiDriver: Send + Sync {
|
||||
fn is_directory(&self, path: &str) -> ApiResult<bool>;
|
||||
@@ -7,6 +10,7 @@ pub trait FileApiDriver: Send + Sync {
|
||||
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()>;
|
||||
fn make_dir(&self, path: &str) -> ApiResult<()>;
|
||||
fn exists(&self, path: &str) -> ApiResult<bool>;
|
||||
fn open_file(&self, path: &str) -> ApiResult<Box<dyn FileHandle>>;
|
||||
|
||||
// These functions correspond to the similarly-named
|
||||
// NodeJS path functions and should behave like the NodeJS
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod api;
|
||||
pub use api::ApiResult;
|
||||
pub use api::FileApiDriver;
|
||||
pub use api::FileHandle;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::ApiResult;
|
||||
use super::FileApiDriver;
|
||||
use super::FileHandle;
|
||||
use std::fs;
|
||||
use std::path;
|
||||
use std::path::Path;
|
||||
@@ -26,6 +27,10 @@ impl FileApiDriver for FileApiDriverImpl {
|
||||
fs::read(path)
|
||||
}
|
||||
|
||||
fn open_file(&self, path: &str) -> ApiResult<Box<dyn FileHandle>> {
|
||||
Ok(Box::new(fs::File::open(path)?))
|
||||
}
|
||||
|
||||
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()> {
|
||||
fs::write(path, data)
|
||||
}
|
||||
@@ -72,6 +77,8 @@ impl FileApiDriver for FileApiDriverImpl {
|
||||
}
|
||||
}
|
||||
|
||||
impl FileHandle for fs::File {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::file_api::FileApiDriver;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::ApiResult;
|
||||
use super::FileApiDriver;
|
||||
use super::FileHandle;
|
||||
use std::io::{BufReader, Read, Seek, SeekFrom};
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use web_sys::js_sys;
|
||||
@@ -31,6 +33,27 @@ extern "C" {
|
||||
|
||||
#[wasm_bindgen(js_name = readDir, catch)]
|
||||
fn read_dir_js(path: &str) -> std::result::Result<JsValue, JsValue>;
|
||||
|
||||
#[wasm_bindgen(js_name = fileReader, catch)]
|
||||
fn open_file_handle(path: &str) -> std::result::Result<JsFileHandle, JsValue>;
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
type JsFileHandle;
|
||||
|
||||
#[wasm_bindgen(structural, method, catch)]
|
||||
fn read(
|
||||
this: &JsFileHandle,
|
||||
offset: usize,
|
||||
size: usize,
|
||||
) -> std::result::Result<Uint8Array, JsValue>;
|
||||
|
||||
#[wasm_bindgen(structural, method)]
|
||||
fn size(this: &JsFileHandle) -> usize;
|
||||
|
||||
#[wasm_bindgen(structural, method, catch)]
|
||||
fn close(this: &JsFileHandle) -> std::result::Result<(), JsValue>;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(module = "fs")]
|
||||
@@ -97,6 +120,16 @@ impl FileApiDriver for FileApiDriverImpl {
|
||||
}
|
||||
}
|
||||
|
||||
fn open_file(&self, path: &str) -> ApiResult<Box<dyn FileHandle>> {
|
||||
match open_file_handle(path) {
|
||||
Ok(handle) => {
|
||||
let file = BufReader::new(SeekableFileHandle { handle, offset: 0 });
|
||||
Ok(Box::new(file))
|
||||
}
|
||||
Err(e) => Err(handle_error(e, &format!("opening file {}", path))),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()> {
|
||||
if let Err(error) = write_file(path, data) {
|
||||
Err(handle_error(error, &format!("writing file {}", path)))
|
||||
@@ -138,3 +171,87 @@ impl FileApiDriver for FileApiDriverImpl {
|
||||
join_path(path_1, path_2).unwrap().as_string().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
struct SeekableFileHandle {
|
||||
handle: JsFileHandle,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl Read for SeekableFileHandle {
|
||||
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
|
||||
let file_size = self.handle.size();
|
||||
let bytes_remaining = if self.offset < file_size {
|
||||
file_size - self.offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let maximum_read_size = bytes_remaining.min(out.len());
|
||||
match self.handle.read(self.offset, maximum_read_size) {
|
||||
Ok(data) => {
|
||||
let data = data.to_vec();
|
||||
let size = data.len();
|
||||
self.offset += size;
|
||||
|
||||
// Verify that handle.read respected the maximum length:
|
||||
if size > out.len() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Invariant violation: Size read must be less than or equal to the maximum_read_size.",
|
||||
));
|
||||
}
|
||||
|
||||
let (target_mem, padding) = out.split_at_mut(size);
|
||||
target_mem.copy_from_slice(&data);
|
||||
padding.fill(0);
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Read failed: {:?}.", error),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for SeekableFileHandle {
|
||||
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
|
||||
match pos {
|
||||
SeekFrom::Start(pos) => {
|
||||
self.offset = pos as usize;
|
||||
}
|
||||
SeekFrom::Current(offset) => {
|
||||
// Disallow seeking to a negative position
|
||||
if offset < 0 && (-offset) as usize > self.offset {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Attempted to seek before the beginning of the file.",
|
||||
));
|
||||
}
|
||||
|
||||
self.offset = (self.offset as i64 + offset) as usize;
|
||||
}
|
||||
SeekFrom::End(offset) => {
|
||||
self.offset = self.handle.size();
|
||||
self.seek(SeekFrom::Current(offset))?;
|
||||
}
|
||||
}
|
||||
Ok(self.offset as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SeekableFileHandle {
|
||||
fn drop(&mut self) {
|
||||
if let Err(error) = self.handle.close() {
|
||||
// Use web_sys directly -- log_warn! can't be used from within the parser-utils package:
|
||||
let message: JsValue =
|
||||
format!("OneNote converter: Failed to close file: Error: {error:?}").into();
|
||||
web_sys::console::warn_1(&message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileHandle for BufReader<SeekableFileHandle> {}
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod parse;
|
||||
pub mod reader;
|
||||
|
||||
pub use errors::Result;
|
||||
pub use file_api::FileHandle;
|
||||
pub use file_api::fs_driver;
|
||||
|
||||
pub type Reader<'a, 'b> = &'b mut crate::reader::Reader<'a>;
|
||||
|
||||
@@ -76,10 +76,17 @@ impl Parser {
|
||||
pub fn parse_section(&mut self, path: String) -> Result<Section> {
|
||||
log!("Parsing section: {:?}", path);
|
||||
let data = fs_driver().read_file(path.as_str())?;
|
||||
self.parse_section_from_data(&data, &path)
|
||||
}
|
||||
|
||||
/// Parse a OneNote section file from a byte array.
|
||||
/// The [path] is used to provide debugging information and determine
|
||||
/// the name of the section file.
|
||||
pub fn parse_section_from_data(&mut self, data: &[u8], path: &str) -> Result<Section> {
|
||||
let store = parse_onestore(&mut Reader::new(&data))?;
|
||||
|
||||
if store.get_type() != OneStoreType::Section {
|
||||
return Err(ErrorKind::NotASectionFile { file: path }.into());
|
||||
return Err(ErrorKind::NotASectionFile { file: String::from(path) }.into());
|
||||
}
|
||||
|
||||
let filename = fs_driver()
|
||||
|
||||
@@ -30,6 +30,7 @@ uuid = "1.1.2"
|
||||
widestring = "1.0.2"
|
||||
wasm-bindgen = "0.2"
|
||||
lazy_static = "1.4"
|
||||
cab = "0.6.0"
|
||||
parser = { path = "../parser" }
|
||||
parser-utils = { path = "../parser-utils" }
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use color_eyre::eyre::{Result, eyre};
|
||||
pub use parser::Parser;
|
||||
use std::panic;
|
||||
use sanitize_filename::sanitize;
|
||||
use std::{io::Read, panic};
|
||||
use wasm_bindgen::{JsError, prelude::wasm_bindgen};
|
||||
|
||||
use parser_utils::{fs_driver, log};
|
||||
use parser_utils::{FileHandle, fs_driver, log};
|
||||
|
||||
mod errors;
|
||||
mod notebook;
|
||||
@@ -34,8 +35,6 @@ fn _main(input_path: &str, output_dir: &str, base_path: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
|
||||
let mut parser = Parser::new();
|
||||
|
||||
let extension: String = fs_driver().get_file_extension(path);
|
||||
|
||||
match extension.as_str() {
|
||||
@@ -47,7 +46,7 @@ pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let section = parser.parse_section(path.to_owned())?;
|
||||
let section = Parser::new().parse_section(path.to_owned())?;
|
||||
|
||||
let section_output_dir = fs_driver().get_output_path(base_path, output_dir, path);
|
||||
section::Renderer::new().render(§ion, section_output_dir.to_owned())?;
|
||||
@@ -56,7 +55,7 @@ pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
|
||||
let _name: String = fs_driver().get_file_name(path).expect("Missing file name");
|
||||
log!("Parsing .onetoc2 file: {}", _name);
|
||||
|
||||
let notebook = parser.parse_notebook(path.to_owned())?;
|
||||
let notebook = Parser::new().parse_notebook(path.to_owned())?;
|
||||
|
||||
let notebook_name = fs_driver()
|
||||
.get_parent_dir(path)
|
||||
@@ -71,8 +70,66 @@ pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
|
||||
|
||||
notebook::Renderer::new().render(¬ebook, ¬ebook_name, ¬ebook_output_dir)?;
|
||||
}
|
||||
".onepkg" => {
|
||||
let file_data = fs_driver().open_file(path)?;
|
||||
convert_onepkg(file_data, output_dir)?;
|
||||
}
|
||||
ext => return Err(eyre!("Invalid file extension: {}, file: {}", ext, path)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_onepkg(file_data: Box<dyn FileHandle>, output_dir: &str) -> Result<()> {
|
||||
// .onepkg files are cabinet files
|
||||
let mut cabinet = cab::Cabinet::new(file_data)?;
|
||||
|
||||
let file_paths: Vec<String> = cabinet
|
||||
.folder_entries()
|
||||
.flat_map(|folder| folder.file_entries())
|
||||
.map(|entry| String::from(entry.name()))
|
||||
.collect();
|
||||
|
||||
let build_output_dir = |file_path_in_archive: &str| -> Result<(String, String)> {
|
||||
let mut output_path = String::from(output_dir);
|
||||
|
||||
// Split on both "\"s and "/"s since CAB archives seem to use Windows-style paths,
|
||||
// where both / and \ are valid path separators.
|
||||
let is_path_separator = |c| c == '\\' || c == '/';
|
||||
let path_segments: Vec<&str> = file_path_in_archive.split(is_path_separator).collect();
|
||||
|
||||
let path_segments_without_filename = &path_segments[0..path_segments.len() - 1];
|
||||
for part in path_segments_without_filename {
|
||||
output_path = fs_driver().join(&output_path, &sanitize(part));
|
||||
fs_driver().make_dir(&output_path)?;
|
||||
}
|
||||
|
||||
let file_name = path_segments.last().unwrap_or(&"");
|
||||
Ok((output_path, sanitize(file_name)))
|
||||
};
|
||||
|
||||
let mut parser = Parser::new();
|
||||
for file_path in file_paths {
|
||||
log!("File path {file_path}");
|
||||
|
||||
if !file_path.ends_with(".one") {
|
||||
log!("Skipping non-section file {file_path}");
|
||||
continue;
|
||||
}
|
||||
|
||||
log!("Rendering {file_path}");
|
||||
|
||||
let data = {
|
||||
let mut file_data = cabinet.read_file(&file_path)?;
|
||||
let mut data = Vec::new();
|
||||
file_data.read_to_end(&mut data)?;
|
||||
data
|
||||
};
|
||||
|
||||
let (output_path, file_name) = build_output_dir(&file_path)?;
|
||||
let section = parser.parse_section_from_data(&data, &file_name)?;
|
||||
section::Renderer::new().render(§ion, output_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"prettycron": "0.10.0",
|
||||
"qrcode": "1.5.4",
|
||||
"query-string": "7.1.3",
|
||||
"rate-limiter-flexible": "7.3.2",
|
||||
"rate-limiter-flexible": "7.4.0",
|
||||
"raw-body": "3.0.1",
|
||||
"samlify": "2.10.1",
|
||||
"sqlite3": "5.1.6",
|
||||
@@ -77,15 +77,15 @@
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/node-os-utils": "1.3.4",
|
||||
"@types/nodemailer": "6.4.20",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@types/nodemailer": "6.4.21",
|
||||
"@types/yargs": "17.0.34",
|
||||
"@types/zxcvbn": "4.4.5",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"jest-expect-message": "1.1.3",
|
||||
"jsdom": "26.1.0",
|
||||
"node-mocks-http": "1.17.2",
|
||||
"short-uuid": "4.2.0",
|
||||
"short-uuid": "5.2.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
|
||||
@@ -123,7 +123,12 @@ export default class ItemModel extends BaseModel<Item> {
|
||||
const share = await this.models().share().load(resource.jop_share_id, { fields: ['id', 'owner_id'] });
|
||||
|
||||
if (!share) {
|
||||
modelLogger.warn('cannot find the share associated with this item. Action:', action, 'User:', user, 'Resource:', resource);
|
||||
// Don't warn in the case where the share doesn't exist. This can happen, for example, when
|
||||
// unsharing a folder.
|
||||
// See https://github.com/laurent22/joplin/issues/14107.
|
||||
if (resource.owner_id !== user.id) {
|
||||
modelLogger.warn('cannot find the share associated with this item. Action:', action, 'User:', user.email, 'Resource:', resource);
|
||||
}
|
||||
} else {
|
||||
if (share.owner_id !== user.id) {
|
||||
const shareUser = await this.models().shareUser().byShareAndUserId(share.id, user.id);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ErrorForbidden } from '../../utils/errors';
|
||||
import { execRequest, execRequestC } from '../../utils/testing/apiUtils';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml, checkContextError, expectHttpError, expectThrow } from '../../utils/testing/testUtils';
|
||||
import { uuidgen } from '@joplin/lib/uuid';
|
||||
import config from '../../config';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
async function postUser(sessionId: string, email: string, password: string = null, props: any = null): Promise<User> {
|
||||
@@ -349,4 +350,29 @@ describe('index/users', () => {
|
||||
expect(await models().application().count()).toBe(0);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ isExternal: true, expectedDisabled: true },
|
||||
{ isExternal: false, expectedDisabled: false },
|
||||
])('should disable password fields for external users, enable for internal users (case: %j)', async ({
|
||||
isExternal, expectedDisabled,
|
||||
}) => {
|
||||
const { user, session } = await createUserAndSession();
|
||||
|
||||
config().SAML_ENABLED = true;
|
||||
try {
|
||||
await models().user().save({
|
||||
id: user.id,
|
||||
is_external: isExternal ? 1 : 0,
|
||||
}, { skipValidation: true });
|
||||
|
||||
const userHtml = await getUserHtml(session.id, user.id);
|
||||
const doc = parseHtml(userHtml);
|
||||
|
||||
expect(
|
||||
doc.querySelector<HTMLInputElement>('input[name=password]').disabled,
|
||||
).toBe(expectedDisabled);
|
||||
} finally {
|
||||
config().SAML_ENABLED = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,7 +119,9 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, formUser: User =
|
||||
view.content.hasFlags = !!userFlagViews.length;
|
||||
view.content.userFlagViews = userFlagViews;
|
||||
view.content.stripePortalUrl = stripePortalUrl();
|
||||
view.content.disabledIfExternalAuth = isUsingExternalAuth(config()) ? 'disabled' : '';
|
||||
|
||||
const isExternalAuth = isUsingExternalAuth(config());
|
||||
view.content.disabledIfExternalAuth = isExternalAuth && user.is_external ? 'disabled' : '';
|
||||
|
||||
view.jsFiles.push('zxcvbn');
|
||||
view.cssFiles.push('index/user');
|
||||
|
||||
484
packages/tools/fuzzer/ActionRunner.ts
Normal file
484
packages/tools/fuzzer/ActionRunner.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import Client from './Client';
|
||||
import ClientPool from './ClientPool';
|
||||
import { assertIsFolder, assertIsNote, FuzzContext, ItemId, RandomFolderOptions } from './types';
|
||||
import { strict as assert } from 'assert';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import retryWithCount from './utils/retryWithCount';
|
||||
import { Second } from '@joplin/utils/time';
|
||||
|
||||
const logger = Logger.create('ActionRunner');
|
||||
|
||||
export interface ActionSpec {
|
||||
key: string;
|
||||
options: Record<string, string|number>;
|
||||
}
|
||||
|
||||
export default class ActionRunner {
|
||||
public constructor(private context_: FuzzContext, private clientPool_: ClientPool, private activeClient_: Client) {}
|
||||
|
||||
public switchClient(client: Client) {
|
||||
this.activeClient_ = client;
|
||||
}
|
||||
|
||||
public async syncAndCheckState() {
|
||||
await this.activeClient_.sync();
|
||||
|
||||
// .checkState can fail occasionally due to incomplete
|
||||
// syncs (perhaps because the server is still processing
|
||||
// share-related changes?). Allow this to be retried:
|
||||
await retryWithCount(async () => {
|
||||
await this.clientPool_.checkState();
|
||||
}, {
|
||||
count: 4,
|
||||
delayOnFailure: count => count * Second * 2,
|
||||
onFail: async () => {
|
||||
logger.info('.checkState failed. Syncing all clients...');
|
||||
await this.clientPool_.syncAll();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private buildActions_() {
|
||||
const { actions, schema, addAction } = getActions(this.context_, this.clientPool_, this.activeClient_);
|
||||
|
||||
addAction('switchClient', async ({ id }) => {
|
||||
if (typeof id !== 'number') throw new Error(`clientId must be a number. Was ${id}`);
|
||||
this.switchClient(this.clientPool_.clientById(id));
|
||||
return true;
|
||||
}, { id: () => this.clientPool_.getClientId(this.clientPool_.randomClient()) });
|
||||
|
||||
addAction('syncAndCheckState', async () => {
|
||||
await this.syncAndCheckState();
|
||||
return true;
|
||||
}, {});
|
||||
|
||||
return { actions, schema };
|
||||
}
|
||||
|
||||
private validateActions_(specs: ActionSpec[]) {
|
||||
const { schema } = this.buildActions_();
|
||||
const errors = [];
|
||||
for (const spec of specs) {
|
||||
const currentActionLabel = JSON.stringify([spec.key, spec.options]);
|
||||
|
||||
const supportedOptions = schema.get(spec.key);
|
||||
if (!supportedOptions) {
|
||||
errors.push(
|
||||
`In ${currentActionLabel}: Unknown action: ${spec.key}. Available action keys: ${JSON.stringify([...schema.keys()])}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const providedOptions = Object.keys(spec.options);
|
||||
for (const option of providedOptions) {
|
||||
if (!supportedOptions.includes(option)) {
|
||||
errors.push(`In ${currentActionLabel}: Unknown option: ${option}. Supported options: ${JSON.stringify(supportedOptions)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
throw new Error(`Validation failed:\n- ${errors.join('\n- ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async doActions(specs: ActionSpec[]) {
|
||||
this.validateActions_(specs);
|
||||
|
||||
for (const spec of specs) {
|
||||
const { actions } = this.buildActions_();
|
||||
|
||||
const action = actions.get(spec.key);
|
||||
if (!action) throw new Error(`Not found: ${spec.key}`);
|
||||
|
||||
await action(spec.options);
|
||||
}
|
||||
}
|
||||
|
||||
public async doRandomAction() {
|
||||
// Avoid running special actions (e.g. "comment")
|
||||
const { actions } = this.buildActions_();
|
||||
|
||||
const actionKeys = [...actions.keys()].filter(key => {
|
||||
// Avoid choosing certain actions:
|
||||
return key !== 'syncAndCheckState' && key !== 'switchClient' && key !== 'comment';
|
||||
});
|
||||
|
||||
let result = false;
|
||||
while (!result) { // Loop until an action was done
|
||||
const randomAction = this.context_.randomFrom(actionKeys);
|
||||
logger.info(`Action: ${randomAction} in ${this.activeClient_.email}`);
|
||||
result = await actions.get(randomAction)({ });
|
||||
if (!result) {
|
||||
logger.info(` ${randomAction} was skipped (preconditions not met).`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ActionDefaults = { [key: string]: ()=> unknown|Promise<unknown> };
|
||||
type ActionOptions<Defaults extends ActionDefaults> = {
|
||||
[t in keyof Defaults]: Awaited<ReturnType<Defaults[t]>>
|
||||
};
|
||||
type UnknownActionOptions = Record<string, unknown>;
|
||||
type ActionFunction<Options extends UnknownActionOptions>
|
||||
= (options: Options)=> Promise<boolean>;
|
||||
|
||||
// Creates an action function, with defaults applied and logging
|
||||
const createActionFunction = <Defaults extends ActionDefaults> (
|
||||
key: string, action: ActionFunction<ActionOptions<Defaults>>, defaults: Defaults,
|
||||
): ActionFunction<Partial<ActionOptions<Defaults>>> => {
|
||||
return async (options) => {
|
||||
const builtOptions: Record<string, unknown> = {};
|
||||
for (const key in defaults) {
|
||||
const defaultValue = await defaults[key]();
|
||||
builtOptions[key] = options[key] ?? defaultValue;
|
||||
}
|
||||
|
||||
logger.info('Run action:', JSON.stringify([key, builtOptions]));
|
||||
return action(builtOptions as ActionOptions<Defaults>);
|
||||
};
|
||||
};
|
||||
|
||||
const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client) => {
|
||||
const selectOrCreateWriteableFolder = async () => {
|
||||
let parentId = (await client.randomFolder({ includeReadOnly: false }))?.id;
|
||||
|
||||
// Create a toplevel folder to serve as this
|
||||
// folder's parent if none exist yet
|
||||
if (!parentId) {
|
||||
parentId = uuid.create();
|
||||
await client.createFolder({
|
||||
parentId: '',
|
||||
id: parentId,
|
||||
title: 'Parent folder',
|
||||
});
|
||||
}
|
||||
|
||||
return parentId;
|
||||
};
|
||||
|
||||
const defaultNoteProperties = {
|
||||
published: false,
|
||||
};
|
||||
|
||||
const selectOrCreateWriteableNote = async () => {
|
||||
const options = { includeReadOnly: false };
|
||||
let note = await client.randomNote(options);
|
||||
|
||||
if (!note) {
|
||||
await client.createNote({
|
||||
...defaultNoteProperties,
|
||||
parentId: await selectOrCreateWriteableFolder(),
|
||||
id: uuid.create(),
|
||||
title: 'Test note',
|
||||
body: 'Body',
|
||||
});
|
||||
|
||||
note = await client.randomNote(options);
|
||||
assert.ok(note, 'should have selected a random note');
|
||||
}
|
||||
|
||||
return note.id;
|
||||
};
|
||||
|
||||
const noteById = (id: ItemId) => {
|
||||
const note = client.itemById(id);
|
||||
assertIsNote(note);
|
||||
return note;
|
||||
};
|
||||
|
||||
const folderById = (id: ItemId) => {
|
||||
const folder = client.itemById(id);
|
||||
assertIsFolder(folder);
|
||||
return folder;
|
||||
};
|
||||
|
||||
const folderByIdOrRandom = (id: ItemId|undefined, randomOptions: RandomFolderOptions) => {
|
||||
if (id !== undefined) {
|
||||
return folderById(id);
|
||||
} else {
|
||||
return client.randomFolder(randomOptions);
|
||||
}
|
||||
};
|
||||
|
||||
const schema = new Map<string, string[]>; // Maps from keys to supported options
|
||||
const actions = new Map<string, ActionFunction<UnknownActionOptions>>();
|
||||
const addAction = <T extends ActionDefaults> (key: string, action: ActionFunction<ActionOptions<T>>, defaults: T) => {
|
||||
actions.set(key, createActionFunction(key, action, defaults) as ActionFunction<UnknownActionOptions>);
|
||||
schema.set(key, Object.keys(defaults));
|
||||
};
|
||||
|
||||
const undefinedId = (): ItemId|undefined => undefined;
|
||||
|
||||
addAction('newSubfolder', async ({ parentId, id }) => {
|
||||
await client.createRandomFolder({ parentId, id, quiet: false });
|
||||
return true;
|
||||
}, {
|
||||
parentId: selectOrCreateWriteableFolder,
|
||||
id: undefinedId,
|
||||
});
|
||||
|
||||
addAction('newToplevelFolder', async ({ id }) => {
|
||||
await client.createRandomFolder({ parentId: '', id, quiet: false });
|
||||
return true;
|
||||
}, { id: undefinedId });
|
||||
|
||||
addAction('newNote', async ({ parentId, id }) => {
|
||||
await client.createRandomNote({ parentId, id });
|
||||
return true;
|
||||
}, {
|
||||
parentId: selectOrCreateWriteableFolder,
|
||||
id: undefinedId,
|
||||
});
|
||||
|
||||
addAction('renameNote', async ({ id }) => {
|
||||
await client.updateNote({
|
||||
...noteById(id),
|
||||
title: `Renamed (${context.randInt(0, 1000)})`,
|
||||
});
|
||||
|
||||
return true;
|
||||
}, { id: selectOrCreateWriteableNote });
|
||||
|
||||
addAction('updateNoteBody', async ({ id }) => {
|
||||
const note = noteById(id);
|
||||
await client.updateNote({
|
||||
...note,
|
||||
body: `${note.body}\n\nUpdated.\n`,
|
||||
});
|
||||
|
||||
return true;
|
||||
}, { id: selectOrCreateWriteableNote });
|
||||
|
||||
addAction('moveNote', async ({ noteId, targetFolderId }) => {
|
||||
const note = noteById(noteId);
|
||||
const newParent = await folderByIdOrRandom(targetFolderId, {
|
||||
filter: folder => folder.id !== note.parentId,
|
||||
includeReadOnly: false,
|
||||
});
|
||||
if (!newParent) return false;
|
||||
|
||||
await client.moveItem(note.id, newParent.id);
|
||||
return true;
|
||||
}, {
|
||||
noteId: selectOrCreateWriteableNote,
|
||||
targetFolderId: undefinedId,
|
||||
});
|
||||
|
||||
addAction('deleteNote', async ({ id }) => {
|
||||
const validatedNote = noteById(id); // Ensure, e.g., that the note exists
|
||||
|
||||
await client.deleteNote(validatedNote.id);
|
||||
return true;
|
||||
}, { id: selectOrCreateWriteableNote });
|
||||
|
||||
const randomClientOnDifferentAccount = () => {
|
||||
const other = clientPool.randomClient(c => !c.hasSameAccount(client));
|
||||
if (!other) return undefined;
|
||||
return clientPool.getClientId(other);
|
||||
};
|
||||
|
||||
addAction('shareFolder', async ({ otherClientId, folderId, readOnly }) => {
|
||||
// No suitable client?
|
||||
if (otherClientId === undefined) return false;
|
||||
|
||||
const other = clientPool.clientById(otherClientId);
|
||||
const target = await folderByIdOrRandom(folderId, {
|
||||
filter: candidate => {
|
||||
const isToplevel = !candidate.parentId;
|
||||
const ownedByCurrent = candidate.ownedByEmail === client.email;
|
||||
const alreadyShared = isToplevel && candidate.isSharedWith(other.email);
|
||||
return isToplevel && ownedByCurrent && !alreadyShared;
|
||||
},
|
||||
includeReadOnly: true,
|
||||
});
|
||||
if (!target) return false;
|
||||
|
||||
await client.shareFolder(target.id, other, { readOnly });
|
||||
return true;
|
||||
}, {
|
||||
otherClientId: randomClientOnDifferentAccount,
|
||||
folderId: undefinedId,
|
||||
readOnly: () => context.randInt(0, 2) === 1 && context.isJoplinCloud,
|
||||
});
|
||||
|
||||
addAction('unshareFolder', async ({ folderId, clientId }) => {
|
||||
const target = await folderByIdOrRandom(folderId, {
|
||||
filter: candidate => {
|
||||
return candidate.isRootSharedItem && candidate.ownedByEmail === client.email;
|
||||
},
|
||||
includeReadOnly: true,
|
||||
});
|
||||
if (!target) return false;
|
||||
|
||||
const recipientEmail = () => {
|
||||
if (clientId !== undefined) {
|
||||
if (clientId === 'all') {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
const email = clientPool.clientById(clientId).email;
|
||||
assert.ok(target.shareRecipients.includes(email), `Not shared with ${email}.`);
|
||||
return email;
|
||||
}
|
||||
|
||||
const recipientIndex = context.randInt(-1, target.shareRecipients.length);
|
||||
if (recipientIndex === -1) return 'all';
|
||||
|
||||
const recipientEmail = target.shareRecipients[recipientIndex];
|
||||
return recipientEmail;
|
||||
};
|
||||
|
||||
|
||||
const email = recipientEmail();
|
||||
if (email === 'all') { // Completely remove the share
|
||||
await client.deleteAssociatedShare(target.id);
|
||||
} else {
|
||||
const recipient = clientPool.clientsByEmail(email)[0];
|
||||
assert.ok(recipient, `invalid state -- recipient ${recipientEmail} should exist`);
|
||||
await client.removeFromShare(target.id, recipient);
|
||||
}
|
||||
return true;
|
||||
}, {
|
||||
folderId: undefinedId,
|
||||
clientId: (): number|'all'|undefined => undefined,
|
||||
});
|
||||
|
||||
addAction('deleteFolder', async ({ folderId }) => {
|
||||
await client.deleteFolder(folderId);
|
||||
return true;
|
||||
}, {
|
||||
folderId: selectOrCreateWriteableFolder,
|
||||
});
|
||||
|
||||
addAction('moveFolderToToplevel', async ({ folderId }) => {
|
||||
if (!folderId) return false;
|
||||
|
||||
await client.deleteFolder(folderId);
|
||||
return true;
|
||||
}, {
|
||||
folderId: async () => (await client.randomFolder({
|
||||
// Don't choose items that are already toplevel
|
||||
filter: item => !!item.parentId,
|
||||
includeReadOnly: false,
|
||||
}))?.id,
|
||||
});
|
||||
|
||||
addAction('moveFolderTo', async ({ sourceFolderId, newParentId }) => {
|
||||
const target = await folderByIdOrRandom(sourceFolderId, {
|
||||
// Don't move shared folders (should not be allowed by the GUI in the main apps).
|
||||
filter: item => !item.isRootSharedItem,
|
||||
includeReadOnly: false,
|
||||
});
|
||||
if (!target) return false;
|
||||
|
||||
const targetDescendants = new Set(await client.allFolderDescendants(target.id));
|
||||
|
||||
const newParent = await folderByIdOrRandom(newParentId, {
|
||||
filter: (item) => {
|
||||
// Avoid making the folder a child of itself
|
||||
return !targetDescendants.has(item.id);
|
||||
},
|
||||
includeReadOnly: false,
|
||||
});
|
||||
if (!newParent) return false;
|
||||
|
||||
await client.moveItem(target.id, newParent.id);
|
||||
return true;
|
||||
}, {
|
||||
sourceFolderId: undefinedId,
|
||||
newParentId: undefinedId,
|
||||
});
|
||||
|
||||
addAction('newClientOnSameAccount', async ({ welcomeNoteCount }) => {
|
||||
logger.info(`Syncing a new client on the same account ${welcomeNoteCount > 0 ? `(with ${welcomeNoteCount} initial notes)` : ''}`);
|
||||
const createClientInitialNotes = async (client: Client) => {
|
||||
if (welcomeNoteCount === 0) return;
|
||||
|
||||
// Create a new folder. Usually, new clients have a default set of
|
||||
// welcome notes when first syncing.
|
||||
const parentFolder = await client.createRandomFolder({ parentId: '', quiet: false });
|
||||
|
||||
for (let i = 0; i < welcomeNoteCount; i++) {
|
||||
await client.createRandomNote({ parentId: parentFolder.id });
|
||||
}
|
||||
};
|
||||
|
||||
await client.sync();
|
||||
|
||||
const other = await clientPool.newWithSameAccount(client);
|
||||
await createClientInitialNotes(other);
|
||||
|
||||
// Sometimes, a delay is needed between client creation
|
||||
// and initial sync. Retry the initial sync and the checkState
|
||||
// on failure:
|
||||
await retryWithCount(async () => {
|
||||
await other.sync();
|
||||
await other.checkState();
|
||||
}, {
|
||||
delayOnFailure: (count) => Second * count,
|
||||
count: 3,
|
||||
onFail: async (error) => {
|
||||
logger.warn('other.sync/other.checkState failed with', error, 'retrying...');
|
||||
},
|
||||
});
|
||||
|
||||
await client.sync();
|
||||
return true;
|
||||
}, {
|
||||
welcomeNoteCount: () => context.randInt(0, 30),
|
||||
});
|
||||
|
||||
addAction('removeClientsOnSameAccount', async () => {
|
||||
const others = clientPool.othersWithSameAccount(client);
|
||||
if (others.length === 0) return false;
|
||||
|
||||
for (const otherClient of others) {
|
||||
assert.notEqual(otherClient, client);
|
||||
await otherClient.close();
|
||||
}
|
||||
return true;
|
||||
}, {});
|
||||
|
||||
addAction('createOrUpdateMany', async ({ count }) => {
|
||||
await client.createOrUpdateMany(count);
|
||||
return true;
|
||||
}, {
|
||||
count: () => context.randInt(1, 512),
|
||||
});
|
||||
|
||||
addAction('publishNote', async ({ id }) => {
|
||||
const note = id ? noteById(id) : await client.randomNote({
|
||||
includeReadOnly: true,
|
||||
});
|
||||
if (!note || note.published) return false;
|
||||
|
||||
await client.publishNote(note.id);
|
||||
return true;
|
||||
}, {
|
||||
id: undefinedId,
|
||||
});
|
||||
|
||||
addAction('unpublishNote', async ({ id }) => {
|
||||
const note = id ? noteById(id) : await client.randomNote({ includeReadOnly: true });
|
||||
if (!note || !note.published) return false;
|
||||
|
||||
await client.unpublishNote(note.id);
|
||||
return true;
|
||||
}, { id: undefinedId });
|
||||
|
||||
addAction('sync', async () => {
|
||||
await client.sync();
|
||||
return true;
|
||||
}, {});
|
||||
|
||||
addAction('comment', async ({ message }) => {
|
||||
logger.info(`Action: Comment: ${JSON.stringify(message)}`);
|
||||
return true;
|
||||
}, { message: () => '' });
|
||||
|
||||
return { actions, schema, addAction };
|
||||
};
|
||||
|
||||
@@ -10,11 +10,43 @@ interface ClientInfo {
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ActionLogEntry {
|
||||
action: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
class ActionTracker {
|
||||
private idToActionLog_: Map<ItemId, ActionLogEntry[]> = new Map();
|
||||
private idToItem_: Map<ItemId, TreeItem> = new Map();
|
||||
private tree_: Map<string, ClientData> = new Map();
|
||||
public constructor(private readonly context_: FuzzContext) {}
|
||||
|
||||
public getActionLog(id: ItemId) {
|
||||
return [...(this.idToActionLog_.get(id) ?? [])];
|
||||
}
|
||||
|
||||
public printActionLog(id: ItemId) {
|
||||
const logEntries = this.getActionLog(id);
|
||||
if (logEntries.length === 0) {
|
||||
process.stdout.write('N/A\n');
|
||||
return;
|
||||
}
|
||||
|
||||
const log = logEntries
|
||||
.map(item => `in:${item.source}: ${item.action}`)
|
||||
.join('\n');
|
||||
process.stdout.write(`${log}\n`);
|
||||
}
|
||||
|
||||
private logAction_(item: ItemId|TreeItem, action: string, source: string) {
|
||||
const itemId = typeof item === 'string' ? item : item.id;
|
||||
|
||||
const log = this.idToActionLog_.get(itemId) ?? [];
|
||||
this.idToActionLog_.set(itemId, log);
|
||||
|
||||
log.push({ action, source });
|
||||
}
|
||||
|
||||
private getToplevelParent_(item: ItemId|TreeItem) {
|
||||
let itemId = typeof item === 'string' ? item : item.id;
|
||||
const originalItemId = itemId;
|
||||
@@ -99,6 +131,14 @@ class ActionTracker {
|
||||
});
|
||||
}
|
||||
|
||||
const logAction = (item: ItemId|TreeItem, action: string) => {
|
||||
this.logAction_(item, action, clientId);
|
||||
};
|
||||
const updateItem = (id: ItemId, newValue: TreeItem, changeLabel: string) => {
|
||||
logAction(id, changeLabel);
|
||||
this.idToItem_.set(id, newValue);
|
||||
};
|
||||
|
||||
const getChildIds = (itemId: ItemId) => {
|
||||
const item = this.idToItem_.get(itemId);
|
||||
if (!item || !isFolder(item)) return [];
|
||||
@@ -207,10 +247,12 @@ class ActionTracker {
|
||||
}
|
||||
|
||||
this.idToItem_.delete(id);
|
||||
logAction(id, 'removed');
|
||||
} else {
|
||||
const idIsUnused = removeRootItem(item.id);
|
||||
if (idIsUnused) {
|
||||
this.idToItem_.delete(id);
|
||||
logAction(id, 'removed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,8 +265,9 @@ class ActionTracker {
|
||||
}
|
||||
}
|
||||
};
|
||||
const mapItems = <T> (map: (item: TreeItem)=> T) => {
|
||||
const workList: ItemId[] = [...this.tree_.get(clientId).childIds];
|
||||
const mapItems = <T> (map: (item: TreeItem)=> T, startFolder?: FolderRecord) => {
|
||||
const startIds: readonly ItemId[] = (startFolder ?? this.tree_.get(clientId)).childIds;
|
||||
const workList = [...startIds];
|
||||
const result: T[] = [];
|
||||
|
||||
while (workList.length > 0) {
|
||||
@@ -244,6 +287,8 @@ class ActionTracker {
|
||||
return result;
|
||||
};
|
||||
|
||||
const descendants = (folder: FolderRecord) => mapItems(item => item, folder);
|
||||
|
||||
const getAllFolders = () => {
|
||||
return mapItems((item): FolderRecord => {
|
||||
return isFolder(item) ? item : null;
|
||||
@@ -286,7 +331,13 @@ class ActionTracker {
|
||||
childIds: otherSubTree.childIds.filter(childId => childId !== id),
|
||||
});
|
||||
|
||||
this.idToItem_.set(id, targetItem.withRemovedFromShare(shareWith.email));
|
||||
const updateLabel = `remove ${shareWith.email} from share`;
|
||||
updateItem(
|
||||
id, targetItem.withRemovedFromShare(shareWith.email), updateLabel,
|
||||
);
|
||||
for (const item of descendants(targetItem)) {
|
||||
logAction(item, updateLabel);
|
||||
}
|
||||
|
||||
this.checkRep_();
|
||||
};
|
||||
@@ -297,9 +348,9 @@ class ActionTracker {
|
||||
|
||||
assert.ok(!!data.parentId, `note ${data.id} should have a parentId`);
|
||||
assert.ok(!this.idToItem_.has(data.id), `note ${data.id} should not yet exist`);
|
||||
this.idToItem_.set(data.id, {
|
||||
updateItem(data.id, {
|
||||
...data,
|
||||
});
|
||||
}, 'created');
|
||||
addChild(data.parentId, data.id);
|
||||
|
||||
this.checkRep_();
|
||||
@@ -308,14 +359,26 @@ class ActionTracker {
|
||||
updateNote: (data: NoteData) => {
|
||||
assertWriteable(data.parentId);
|
||||
|
||||
const oldItem = this.idToItem_.get(data.id);
|
||||
const oldItem = this.idToItem_.get(data.id) as NoteData;
|
||||
assert.ok(oldItem, `note ${data.id} should exist`);
|
||||
assert.ok(!!data.parentId, `note ${data.id} should have a parentId`);
|
||||
|
||||
// Additional debugging information about what changed:
|
||||
const changedFieldsInfo = Object.entries(data)
|
||||
.filter(([key, newValue]) => {
|
||||
const itemKey = key as keyof NoteData;
|
||||
// isShared is a virtual property
|
||||
return key !== 'isShared'
|
||||
&& oldItem[itemKey] !== newValue;
|
||||
})
|
||||
.map(([key]) => {
|
||||
return key;
|
||||
});
|
||||
|
||||
removeChild(oldItem.parentId, data.id);
|
||||
this.idToItem_.set(data.id, {
|
||||
updateItem(data.id, {
|
||||
...data,
|
||||
});
|
||||
}, `updated (changed fields: ${JSON.stringify(changedFieldsInfo)})`);
|
||||
addChild(data.parentId, data.id);
|
||||
|
||||
this.checkRep_();
|
||||
@@ -325,14 +388,14 @@ class ActionTracker {
|
||||
const parentId = data.parentId ?? '';
|
||||
assertWriteable(parentId);
|
||||
|
||||
this.idToItem_.set(data.id, new FolderRecord({
|
||||
updateItem(data.id, new FolderRecord({
|
||||
...data,
|
||||
parentId: parentId ?? '',
|
||||
childIds: getChildIds(data.id),
|
||||
sharedWith: [],
|
||||
ownedByEmail: clientId,
|
||||
isShared: false,
|
||||
}));
|
||||
}), 'created');
|
||||
addChild(data.parentId, data.id);
|
||||
|
||||
this.checkRep_();
|
||||
@@ -365,6 +428,8 @@ class ActionTracker {
|
||||
return Promise.resolve();
|
||||
},
|
||||
shareFolder: (id: ItemId, shareWith: ClientInfo, options: ShareOptions) => {
|
||||
assert.notEqual(client.email, shareWith.email, 'Cannot share a folder with the same client');
|
||||
|
||||
const itemToShare = this.idToItem_.get(id);
|
||||
assertIsFolder(itemToShare);
|
||||
|
||||
@@ -381,10 +446,17 @@ class ActionTracker {
|
||||
childIds: [...shareWithChildIds, id],
|
||||
});
|
||||
|
||||
this.idToItem_.set(
|
||||
id, itemToShare.withShared(shareWith.email, options.readOnly),
|
||||
updateItem(
|
||||
id,
|
||||
itemToShare.withShared(shareWith.email, options.readOnly),
|
||||
`shared with ${shareWith.email}`,
|
||||
);
|
||||
|
||||
// Additional logging
|
||||
for (const item of descendants(itemToShare)) {
|
||||
logAction(item, `ancestor shared with ${shareWith.email}`);
|
||||
}
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
},
|
||||
@@ -397,7 +469,11 @@ class ActionTracker {
|
||||
removeFromShare(id, { email: recipient });
|
||||
}
|
||||
|
||||
this.idToItem_.set(id, targetItem.withUnshared());
|
||||
updateItem(id, targetItem.withUnshared(), 'unshared');
|
||||
|
||||
for (const item of descendants(targetItem)) {
|
||||
logAction(item, 'parent share removed');
|
||||
}
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
@@ -426,9 +502,10 @@ class ActionTracker {
|
||||
|
||||
removeChild(item.parentId, itemId);
|
||||
addChild(newParentId, itemId);
|
||||
this.idToItem_.set(
|
||||
updateItem(
|
||||
itemId,
|
||||
isFolder(item) ? item.withParent(newParentId) : { ...item, parentId: newParentId },
|
||||
`moved to id:${newParentId}`,
|
||||
);
|
||||
|
||||
this.checkRep_();
|
||||
@@ -441,10 +518,10 @@ class ActionTracker {
|
||||
assert.ok(!oldItem.published, 'should not be published');
|
||||
|
||||
|
||||
this.idToItem_.set(id, {
|
||||
updateItem(id, {
|
||||
...oldItem,
|
||||
published: true,
|
||||
});
|
||||
}, 'published');
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
@@ -455,10 +532,10 @@ class ActionTracker {
|
||||
assert.ok(!isFolder(oldItem), 'folders cannot be unpublished');
|
||||
assert.ok(oldItem.published, 'should be published');
|
||||
|
||||
this.idToItem_.set(id, {
|
||||
updateItem(id, {
|
||||
...oldItem,
|
||||
published: false,
|
||||
});
|
||||
}, 'unpublished');
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
@@ -525,6 +602,13 @@ class ActionTracker {
|
||||
const noteIndex = this.context_.randInt(0, notes.length);
|
||||
return notes.length ? notes[noteIndex] : null;
|
||||
},
|
||||
// Note: Does not verify that the current client has access to the item
|
||||
itemById: (id: ItemId) => {
|
||||
const item = this.idToItem_.get(id);
|
||||
|
||||
if (!item) throw new Error(`No item found with ID ${id}`);
|
||||
return item;
|
||||
},
|
||||
};
|
||||
return tracker;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import Stream = require('stream');
|
||||
import ProgressBar from './utils/ProgressBar';
|
||||
import logDiffDebug from './utils/logDiffDebug';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import diffSortedStringArrays from './utils/diffSortedStringArrays';
|
||||
|
||||
const logger = Logger.create('Client');
|
||||
|
||||
@@ -98,6 +99,12 @@ interface CreateOrUpdateOptions {
|
||||
quiet?: boolean;
|
||||
}
|
||||
|
||||
interface CreateRandomItemOptions extends CreateOrUpdateOptions {
|
||||
parentId: ItemId;
|
||||
id?: ItemId;
|
||||
quiet?: boolean;
|
||||
}
|
||||
|
||||
class Client implements ActionableClient {
|
||||
public readonly email: string;
|
||||
|
||||
@@ -251,6 +258,7 @@ class Client implements ActionableClient {
|
||||
|
||||
this.childProcess_.close();
|
||||
this.closed_ = true;
|
||||
logger.info('Closed client ', this.email);
|
||||
}
|
||||
|
||||
public onClose(listener: OnCloseListener) {
|
||||
@@ -313,7 +321,8 @@ class Client implements ActionableClient {
|
||||
this.bufferedChildProcessStderr_ = [];
|
||||
process.stdout.write('CLI debug session. Enter a blank line or "exit" to exit.\n');
|
||||
process.stdout.write('To review a transcript of all interactions with this client,\n');
|
||||
process.stdout.write('enter "[transcript]".\n\n');
|
||||
process.stdout.write('enter "[transcript]". To log information about a particular item\n');
|
||||
process.stdout.write('enter "[item:...id here...]".\n\n');
|
||||
process.stdout.write(cliProcessPromptString);
|
||||
|
||||
const isExitRequest = (input: string) => {
|
||||
@@ -328,6 +337,10 @@ class Client implements ActionableClient {
|
||||
lastInput = await readline.question('');
|
||||
if (lastInput === '[transcript]') {
|
||||
process.stdout.write(`\n\n# Transcript\n\n${this.getTranscript()}\n\n# End transcript\n\n`);
|
||||
} else if (lastInput.startsWith('[item:') && lastInput.endsWith(']')) {
|
||||
let id = lastInput.substring('[item:'.length);
|
||||
id = id.substring(0, id.length - 1);
|
||||
this.globalActionTracker_.printActionLog(id);
|
||||
} else if (!isExitRequest(lastInput)) {
|
||||
this.childProcess_.writeStdin(`${lastInput}\n`);
|
||||
}
|
||||
@@ -471,11 +484,11 @@ class Client implements ActionableClient {
|
||||
let parentId = (await this.randomFolder({ includeReadOnly: false }))?.id;
|
||||
const createSubfolder = this.context_.randInt(0, 100) < 10;
|
||||
if (!parentId || createSubfolder) {
|
||||
const folder = await this.createRandomFolder(parentId, { quiet: true });
|
||||
const folder = await this.createRandomFolder({ parentId, quiet: true });
|
||||
parentId = folder.id;
|
||||
}
|
||||
|
||||
await this.createRandomNote(parentId, { quiet: true });
|
||||
await this.createRandomNote({ parentId, quiet: true });
|
||||
},
|
||||
update: async (targetNote: NoteData) => {
|
||||
const keep = targetNote.body.substring(
|
||||
@@ -513,16 +526,15 @@ class Client implements ActionableClient {
|
||||
bar.complete();
|
||||
}
|
||||
|
||||
public async createRandomFolder(parentId: ItemId, options: CreateOrUpdateOptions) {
|
||||
public async createRandomFolder({ quiet, parentId, id }: CreateRandomItemOptions) {
|
||||
const titleLength = this.context_.randInt(1, 128);
|
||||
const folderId = uuid.create();
|
||||
const folder = {
|
||||
parentId: parentId,
|
||||
id: folderId,
|
||||
id: id ?? uuid.create(),
|
||||
title: this.context_.randomString(titleLength).replace(/\n/g, ' '),
|
||||
};
|
||||
|
||||
await this.createFolder(folder, options);
|
||||
await this.createFolder(folder, { quiet });
|
||||
|
||||
return folder;
|
||||
}
|
||||
@@ -582,11 +594,14 @@ class Client implements ActionableClient {
|
||||
logDiffDebug(lastActualNote.title, expected.title);
|
||||
logDiffDebug(lastActualNote.body, expected.body);
|
||||
}
|
||||
// Log all transactions associated with the item
|
||||
this.globalActionTracker_.printActionLog(expected.id);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async createRandomNote(parentId: string, { quiet = false }: CreateOrUpdateOptions = { }) {
|
||||
public async createRandomNote({ parentId, id, quiet = false }: CreateRandomItemOptions) {
|
||||
const titleLength = this.context_.randInt(0, 256);
|
||||
const bodyLength = this.context_.randInt(0, 2000);
|
||||
await this.createNote({
|
||||
@@ -594,7 +609,7 @@ class Client implements ActionableClient {
|
||||
parentId,
|
||||
title: this.context_.randomString(titleLength),
|
||||
body: this.context_.randomString(bodyLength),
|
||||
id: uuid.create(),
|
||||
id: id ?? uuid.create(),
|
||||
}, { quiet });
|
||||
}
|
||||
|
||||
@@ -793,6 +808,10 @@ class Client implements ActionableClient {
|
||||
return this.tracker_.randomNote(options);
|
||||
}
|
||||
|
||||
public itemById(itemId: ItemId) {
|
||||
return this.tracker_.itemById(itemId);
|
||||
}
|
||||
|
||||
public async checkState() {
|
||||
logger.info('Check state', this.label);
|
||||
|
||||
@@ -814,6 +833,35 @@ class Client implements ActionableClient {
|
||||
}
|
||||
};
|
||||
|
||||
const assertSameIds = (actualSorted: ItemSlice[], expectedSorted: ItemSlice[], testLabel: string) => {
|
||||
const actualIds = actualSorted.map(i => i.id);
|
||||
const expectedIds = expectedSorted.map(i => i.id);
|
||||
const { missing, unexpected } = diffSortedStringArrays(actualIds, expectedIds);
|
||||
|
||||
|
||||
if (missing.length || unexpected.length) {
|
||||
const idLogs = (ids: string[]) => {
|
||||
const output = [];
|
||||
for (const id of ids) {
|
||||
const log = this.globalActionTracker_.getActionLog(id);
|
||||
output.push(`\nid:${id}`);
|
||||
output.push(log.map(item => `\t${item.source}: ${item.action}`).join('\n'));
|
||||
}
|
||||
return output.join('\n');
|
||||
};
|
||||
|
||||
throw new Error([
|
||||
`IDs were different (${testLabel}):`,
|
||||
missing.length && `- Expected ${JSON.stringify(missing)} to be present, but were missing.`,
|
||||
unexpected.length && `- Present but should not have been: ${JSON.stringify(unexpected)}`,
|
||||
'\n',
|
||||
'Logs:',
|
||||
idLogs(missing),
|
||||
idLogs(unexpected),
|
||||
].filter(line => !!line).join('\n'));
|
||||
}
|
||||
};
|
||||
|
||||
const checkNoteState = async () => {
|
||||
const notes = [...await this.listNotes()];
|
||||
const expectedNotes = [...await this.tracker_.listNotes()];
|
||||
@@ -823,6 +871,7 @@ class Client implements ActionableClient {
|
||||
|
||||
assertNoAdjacentEqualIds(notes, 'notes');
|
||||
assertNoAdjacentEqualIds(expectedNotes, 'expectedNotes');
|
||||
assertSameIds(notes, expectedNotes, 'should have the same note IDs');
|
||||
assert.deepEqual(notes, expectedNotes, 'should have the same notes as the expected state');
|
||||
};
|
||||
|
||||
@@ -835,6 +884,7 @@ class Client implements ActionableClient {
|
||||
|
||||
assertNoAdjacentEqualIds(folders, 'folders');
|
||||
assertNoAdjacentEqualIds(expectedFolders, 'expectedFolders');
|
||||
assertSameIds(folders, expectedFolders, 'should have the same folder IDs');
|
||||
assert.deepEqual(folders, expectedFolders, 'should have the same folders as the expected state');
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export default class ClientPool {
|
||||
});
|
||||
}
|
||||
|
||||
public async createInitialItemsAndSync() {
|
||||
public async createRandomInitialItemsAndSync() {
|
||||
for (const client of this.clients) {
|
||||
logger.info('Creating items for ', client.email);
|
||||
const actionCount = this.context_.randomFrom([0, 10, 100]);
|
||||
@@ -55,6 +55,18 @@ export default class ClientPool {
|
||||
return this.clients.filter(client => client.email === email);
|
||||
}
|
||||
|
||||
public clientById(id: number) {
|
||||
const client = this.clients[id];
|
||||
if (!client) throw new Error(`Not found: ${id}`);
|
||||
return client;
|
||||
}
|
||||
|
||||
public getClientId(client: Client): number {
|
||||
const index = this.clients.indexOf(client);
|
||||
if (index === -1) throw new Error(`Not found: ${client}`);
|
||||
return index;
|
||||
}
|
||||
|
||||
public randomClient(filter: ClientFilter = ()=>true) {
|
||||
const clients = this.clients_.filter(filter);
|
||||
return clients[
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import Client from './Client';
|
||||
import ClientPool from './ClientPool';
|
||||
import { FuzzContext } from './types';
|
||||
import { strict as assert } from 'assert';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import retryWithCount from './utils/retryWithCount';
|
||||
import { Second } from '@joplin/utils/time';
|
||||
|
||||
const logger = Logger.create('doRandomAction');
|
||||
|
||||
const doRandomAction = async (context: FuzzContext, client: Client, clientPool: ClientPool) => {
|
||||
const selectOrCreateParentFolder = async () => {
|
||||
let parentId = (await client.randomFolder({ includeReadOnly: false }))?.id;
|
||||
|
||||
// Create a toplevel folder to serve as this
|
||||
// folder's parent if none exist yet
|
||||
if (!parentId) {
|
||||
parentId = uuid.create();
|
||||
await client.createFolder({
|
||||
parentId: '',
|
||||
id: parentId,
|
||||
title: 'Parent folder',
|
||||
});
|
||||
}
|
||||
|
||||
return parentId;
|
||||
};
|
||||
|
||||
const defaultNoteProperties = {
|
||||
published: false,
|
||||
};
|
||||
|
||||
const selectOrCreateWriteableNote = async () => {
|
||||
const options = { includeReadOnly: false };
|
||||
let note = await client.randomNote(options);
|
||||
|
||||
if (!note) {
|
||||
await client.createNote({
|
||||
...defaultNoteProperties,
|
||||
parentId: await selectOrCreateParentFolder(),
|
||||
id: uuid.create(),
|
||||
title: 'Test note',
|
||||
body: 'Body',
|
||||
});
|
||||
|
||||
note = await client.randomNote(options);
|
||||
assert.ok(note, 'should have selected a random note');
|
||||
}
|
||||
|
||||
return note;
|
||||
};
|
||||
|
||||
const actions = {
|
||||
newSubfolder: async () => {
|
||||
const parentId = await selectOrCreateParentFolder();
|
||||
await client.createRandomFolder(parentId, { quiet: false });
|
||||
|
||||
return true;
|
||||
},
|
||||
newToplevelFolder: async () => {
|
||||
await client.createRandomFolder('', { quiet: false });
|
||||
return true;
|
||||
},
|
||||
newNote: async () => {
|
||||
const parentId = await selectOrCreateParentFolder();
|
||||
await client.createRandomNote(parentId);
|
||||
|
||||
return true;
|
||||
},
|
||||
renameNote: async () => {
|
||||
const note = await selectOrCreateWriteableNote();
|
||||
|
||||
await client.updateNote({
|
||||
...note,
|
||||
title: `Renamed (${context.randInt(0, 1000)})`,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
updateNoteBody: async () => {
|
||||
const note = await selectOrCreateWriteableNote();
|
||||
|
||||
await client.updateNote({
|
||||
...note,
|
||||
body: `${note.body}\n\nUpdated.\n`,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
moveNote: async () => {
|
||||
const note = await selectOrCreateWriteableNote();
|
||||
const targetParent = await client.randomFolder({
|
||||
filter: folder => folder.id !== note.parentId,
|
||||
includeReadOnly: false,
|
||||
});
|
||||
if (!targetParent) return false;
|
||||
|
||||
await client.moveItem(note.id, targetParent.id);
|
||||
|
||||
return true;
|
||||
},
|
||||
deleteNote: async () => {
|
||||
const target = await client.randomNote({ includeReadOnly: false });
|
||||
if (!target) return false;
|
||||
|
||||
await client.deleteNote(target.id);
|
||||
return true;
|
||||
},
|
||||
shareFolder: async () => {
|
||||
const other = clientPool.randomClient(c => !c.hasSameAccount(client));
|
||||
if (!other) return false;
|
||||
|
||||
const target = await client.randomFolder({
|
||||
filter: candidate => {
|
||||
const isToplevel = !candidate.parentId;
|
||||
const ownedByCurrent = candidate.ownedByEmail === client.email;
|
||||
const alreadyShared = isToplevel && candidate.isSharedWith(other.email);
|
||||
return isToplevel && ownedByCurrent && !alreadyShared;
|
||||
},
|
||||
includeReadOnly: true,
|
||||
});
|
||||
if (!target) return false;
|
||||
|
||||
const readOnly = context.randInt(0, 2) === 1 && context.isJoplinCloud;
|
||||
await client.shareFolder(target.id, other, { readOnly });
|
||||
return true;
|
||||
},
|
||||
unshareFolder: async () => {
|
||||
const target = await client.randomFolder({
|
||||
filter: candidate => {
|
||||
return candidate.isRootSharedItem && candidate.ownedByEmail === client.email;
|
||||
},
|
||||
includeReadOnly: true,
|
||||
});
|
||||
if (!target) return false;
|
||||
|
||||
const recipientIndex = context.randInt(-1, target.shareRecipients.length);
|
||||
if (recipientIndex === -1) { // Completely remove the share
|
||||
await client.deleteAssociatedShare(target.id);
|
||||
} else {
|
||||
const recipientEmail = target.shareRecipients[recipientIndex];
|
||||
const recipient = clientPool.clientsByEmail(recipientEmail)[0];
|
||||
assert.ok(recipient, `invalid state -- recipient ${recipientEmail} should exist`);
|
||||
await client.removeFromShare(target.id, recipient);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
deleteFolder: async () => {
|
||||
const target = await client.randomFolder({ includeReadOnly: false });
|
||||
if (!target) return false;
|
||||
|
||||
await client.deleteFolder(target.id);
|
||||
return true;
|
||||
},
|
||||
moveFolderToToplevel: async () => {
|
||||
const target = await client.randomFolder({
|
||||
// Don't choose items that are already toplevel
|
||||
filter: item => !!item.parentId,
|
||||
includeReadOnly: false,
|
||||
});
|
||||
if (!target) return false;
|
||||
|
||||
await client.moveItem(target.id, '');
|
||||
return true;
|
||||
},
|
||||
moveFolderTo: async () => {
|
||||
const target = await client.randomFolder({
|
||||
// Don't move shared folders (should not be allowed by the GUI in the main apps).
|
||||
filter: item => !item.isRootSharedItem,
|
||||
includeReadOnly: false,
|
||||
});
|
||||
if (!target) return false;
|
||||
|
||||
const targetDescendants = new Set(await client.allFolderDescendants(target.id));
|
||||
|
||||
const newParent = await client.randomFolder({
|
||||
filter: (item) => {
|
||||
// Avoid making the folder a child of itself
|
||||
return !targetDescendants.has(item.id);
|
||||
},
|
||||
includeReadOnly: false,
|
||||
});
|
||||
if (!newParent) return false;
|
||||
|
||||
await client.moveItem(target.id, newParent.id);
|
||||
return true;
|
||||
},
|
||||
newClientOnSameAccount: async () => {
|
||||
const welcomeNoteCount = context.randInt(0, 30);
|
||||
logger.info(`Syncing a new client on the same account ${welcomeNoteCount > 0 ? `(with ${welcomeNoteCount} initial notes)` : ''}`);
|
||||
const createClientInitialNotes = async (client: Client) => {
|
||||
if (welcomeNoteCount === 0) return;
|
||||
|
||||
// Create a new folder. Usually, new clients have a default set of
|
||||
// welcome notes when first syncing.
|
||||
const parentFolder = await client.createRandomFolder('', { quiet: false });
|
||||
|
||||
for (let i = 0; i < welcomeNoteCount; i++) {
|
||||
await client.createRandomNote(parentFolder.id);
|
||||
}
|
||||
};
|
||||
|
||||
await client.sync();
|
||||
|
||||
const other = await clientPool.newWithSameAccount(client);
|
||||
await createClientInitialNotes(other);
|
||||
|
||||
// Sometimes, a delay is needed between client creation
|
||||
// and initial sync. Retry the initial sync and the checkState
|
||||
// on failure:
|
||||
await retryWithCount(async () => {
|
||||
await other.sync();
|
||||
await other.checkState();
|
||||
}, {
|
||||
delayOnFailure: (count) => Second * count,
|
||||
count: 3,
|
||||
onFail: async (error) => {
|
||||
logger.warn('other.sync/other.checkState failed with', error, 'retrying...');
|
||||
},
|
||||
});
|
||||
|
||||
await client.sync();
|
||||
return true;
|
||||
},
|
||||
removeClientsOnSameAccount: async () => {
|
||||
const others = clientPool.othersWithSameAccount(client);
|
||||
if (others.length === 0) return false;
|
||||
|
||||
for (const otherClient of others) {
|
||||
assert.notEqual(otherClient, client);
|
||||
await otherClient.close();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
createOrUpdateMany: async () => {
|
||||
await client.createOrUpdateMany(context.randInt(1, 512));
|
||||
return true;
|
||||
},
|
||||
publishNote: async () => {
|
||||
const note = await client.randomNote({
|
||||
includeReadOnly: true,
|
||||
});
|
||||
if (!note || note.published) return false;
|
||||
|
||||
await client.publishNote(note.id);
|
||||
return true;
|
||||
},
|
||||
unpublishNote: async () => {
|
||||
const note = await client.randomNote({ includeReadOnly: true });
|
||||
if (!note || !note.published) return false;
|
||||
|
||||
await client.unpublishNote(note.id);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const actionKeys = [...Object.keys(actions)] as (keyof typeof actions)[];
|
||||
|
||||
let result = false;
|
||||
while (!result) { // Loop until an action was done
|
||||
const randomAction = context.randomFrom(actionKeys);
|
||||
logger.info(`Action: ${randomAction} in ${client.email}`);
|
||||
result = await actions[randomAction]();
|
||||
if (!result) {
|
||||
logger.info(` ${randomAction} was skipped (preconditions not met).`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default doRandomAction;
|
||||
26
packages/tools/fuzzer/sample-fuzzer-setup.json
Normal file
26
packages/tools/fuzzer/sample-fuzzer-setup.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"clientCount": 2,
|
||||
"actions": [
|
||||
["switchClient", { "id": 0 }],
|
||||
"sync",
|
||||
|
||||
"// Switch to the second client & share a folder",
|
||||
["switchClient", { "id": 1 }],
|
||||
["newToplevelFolder", { "id": "11111111111111111111111111111112" }],
|
||||
["newNote", { "id": "11111111111111111111111111111113", "parentId": "11111111111111111111111111111112" }],
|
||||
["shareFolder", { "folderId": "11111111111111111111111111111112", "otherClientId": 0, "readOnly": false }],
|
||||
"syncAndCheckState",
|
||||
|
||||
"// Switch to the first client & remove a note from the folder",
|
||||
["switchClient", { "id": 0 }],
|
||||
["newToplevelFolder", { "id": "11111111111111111111111111111111" }],
|
||||
"syncAndCheckState",
|
||||
|
||||
["moveNote", { "noteId": "11111111111111111111111111111113", "targetFolderId": "11111111111111111111111111111111" }],
|
||||
"syncAndCheckState",
|
||||
|
||||
"// Switch back to the second client and add a new client to the same account",
|
||||
["switchClient", { "id": 0 }],
|
||||
"newClientOnSameAccount"
|
||||
]
|
||||
}
|
||||
@@ -5,15 +5,14 @@ import Logger, { TargetType } from '@joplin/utils/Logger';
|
||||
import Server from './Server';
|
||||
import { CleanupTask, FuzzContext } from './types';
|
||||
import ClientPool from './ClientPool';
|
||||
import retryWithCount from './utils/retryWithCount';
|
||||
import SeededRandom from './utils/SeededRandom';
|
||||
import { env } from 'process';
|
||||
import yargs = require('yargs');
|
||||
import openDebugSession from './utils/openDebugSession';
|
||||
import { Second } from '@joplin/utils/time';
|
||||
import { packagesDir } from './constants';
|
||||
import doRandomAction from './doRandomAction';
|
||||
import ActionRunner, { ActionSpec } from './ActionRunner';
|
||||
import randomString from './utils/randomString';
|
||||
import { readFile } from 'fs/promises';
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node');
|
||||
|
||||
const globalLogger = new Logger();
|
||||
@@ -48,6 +47,7 @@ interface Options {
|
||||
randomStrings: boolean;
|
||||
clientCount: number;
|
||||
keepAccountsOnClose: boolean;
|
||||
setupActions: ActionSpec[];
|
||||
|
||||
serverPath: string;
|
||||
isJoplinCloud: boolean;
|
||||
@@ -137,11 +137,16 @@ const main = async (options: Options) => {
|
||||
options.clientCount,
|
||||
task => { cleanupTasks.push(task); },
|
||||
);
|
||||
await clientPool.createInitialItemsAndSync();
|
||||
|
||||
const actionRunner = new ActionRunner(fuzzContext, clientPool, clientPool.randomClient());
|
||||
logger.info('Starting setup:');
|
||||
await actionRunner.doActions(options.setupActions);
|
||||
|
||||
logger.info('Starting randomized actions:');
|
||||
const maxSteps = options.maximumSteps;
|
||||
for (let stepIndex = 1; maxSteps <= 0 || stepIndex <= maxSteps; stepIndex++) {
|
||||
const client = clientPool.randomClient();
|
||||
actionRunner.switchClient(client);
|
||||
|
||||
// Ensure that the client starts up-to-date with the other synced clients.
|
||||
await client.sync();
|
||||
@@ -152,23 +157,10 @@ const main = async (options: Options) => {
|
||||
if (actionsBeforeFullSync > 1) {
|
||||
logger.info('Sub-step', subStepIndex, '/', actionsBeforeFullSync, '(in step', stepIndex, ')');
|
||||
}
|
||||
await doRandomAction(fuzzContext, client, clientPool);
|
||||
await actionRunner.doRandomAction();
|
||||
}
|
||||
await client.sync();
|
||||
|
||||
// .checkState can fail occasionally due to incomplete
|
||||
// syncs (perhaps because the server is still processing
|
||||
// share-related changes?). Allow this to be retried:
|
||||
await retryWithCount(async () => {
|
||||
await clientPool.checkState();
|
||||
}, {
|
||||
count: 4,
|
||||
delayOnFailure: count => count * Second * 2,
|
||||
onFail: async () => {
|
||||
logger.info('.checkState failed. Syncing all clients...');
|
||||
await clientPool.syncAll();
|
||||
},
|
||||
});
|
||||
await actionRunner.syncAndCheckState();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('ERROR', error);
|
||||
@@ -185,6 +177,64 @@ const main = async (options: Options) => {
|
||||
}
|
||||
};
|
||||
|
||||
const readSetupFile = async (path: string) => {
|
||||
const setupActionFile = await readFile(path, 'utf-8');
|
||||
const setupData = JSON.parse(setupActionFile);
|
||||
|
||||
const errorLabel = `Reading ${path}.`;
|
||||
|
||||
const readNumber = <T extends object> (key: keyof T, parent: T) => {
|
||||
if (typeof parent[key] !== 'number') {
|
||||
throw new Error(`${errorLabel} Expected key ${String(key)} to be a number. Was ${typeof parent[key]}.`);
|
||||
}
|
||||
|
||||
return parent[key];
|
||||
};
|
||||
const readArray = <T extends object> (key: keyof T, parent: T) => {
|
||||
if (!Array.isArray(parent[key])) {
|
||||
throw new Error(`${errorLabel} Expected key ${String(key)} to be an array. Was ${typeof parent[key]}.`);
|
||||
}
|
||||
|
||||
return parent[key];
|
||||
};
|
||||
|
||||
const clientCount = readNumber('clientCount', setupData);
|
||||
|
||||
const initialActions: unknown[] = readArray('actions', setupData);
|
||||
const actions: ActionSpec[] = initialActions
|
||||
.map((action: unknown, index: number) => {
|
||||
if (typeof action === 'string') {
|
||||
const isComment = action.startsWith('//');
|
||||
if (isComment) {
|
||||
return { key: 'comment', options: { message: action.substring(2).trimStart() } };
|
||||
}
|
||||
return { key: action, options: {} };
|
||||
}
|
||||
|
||||
if (!Array.isArray(action) || action.length < 1 || action.length > 2) {
|
||||
throw new Error(`${errorLabel} In 'actions'. Expected an array of length 1 or 2. (Reading item ${JSON.stringify(action)} at index: ${index})`);
|
||||
}
|
||||
|
||||
const key = action[0];
|
||||
const options = action[1] ?? {};
|
||||
return { key, options } as ActionSpec;
|
||||
});
|
||||
|
||||
return { clientCount, setupActions: actions };
|
||||
};
|
||||
|
||||
const defaultSetupActions = (clientCount: number) => {
|
||||
const actions = [];
|
||||
for (let i = 0; i < clientCount; i++) {
|
||||
actions.push(
|
||||
{ key: 'switchClient', options: { id: i } },
|
||||
{ key: 'createOrUpdateMany', options: {} },
|
||||
{ key: 'sync', options: {} },
|
||||
);
|
||||
}
|
||||
return actions;
|
||||
};
|
||||
|
||||
|
||||
void yargs
|
||||
.usage('$0 <cmd>')
|
||||
@@ -239,22 +289,39 @@ void yargs
|
||||
'This also enables testing for some Joplin Cloud-specific features (e.g. read-only shares).',
|
||||
].join(''),
|
||||
},
|
||||
'setup': {
|
||||
type: 'string',
|
||||
default: '',
|
||||
defaultDescription: [
|
||||
'A path: If provided, this should point to a JSON file containing actions to run during startup. ',
|
||||
'The JSON file should contain an object similar to { "actions": [ ["newNote", {}] ], "clientCount": 1 }.',
|
||||
].join(''),
|
||||
},
|
||||
});
|
||||
},
|
||||
async (argv) => {
|
||||
const serverPath = argv.joplinCloud ? argv.joplinCloud : join(packagesDir, 'server');
|
||||
|
||||
let setupData = undefined;
|
||||
if (argv.setup) {
|
||||
setupData = await readSetupFile(argv.setup);
|
||||
}
|
||||
|
||||
const clientCount = setupData?.clientCount ?? argv.clients;
|
||||
await main({
|
||||
seed: argv.seed,
|
||||
maximumSteps: argv.steps,
|
||||
clientCount: argv.clients,
|
||||
clientCount,
|
||||
serverPath: serverPath,
|
||||
isJoplinCloud: !!argv.joplinCloud,
|
||||
maximumStepsBetweenSyncs: argv['steps-between-syncs'],
|
||||
keepAccountsOnClose: argv.keepAccounts,
|
||||
enableE2ee: argv.enableE2ee,
|
||||
randomStrings: argv.randomStrings,
|
||||
setupActions: setupData?.setupActions ?? defaultSetupActions(clientCount),
|
||||
});
|
||||
},
|
||||
)
|
||||
.demandCommand()
|
||||
.help()
|
||||
.argv;
|
||||
|
||||
@@ -42,6 +42,10 @@ export const assertIsFolder: (item: TreeItem)=> asserts item is FolderRecord = i
|
||||
throw new Error(`Expected item with ID ${item?.id} to be a folder.`);
|
||||
}
|
||||
};
|
||||
export const assertIsNote: (item: TreeItem)=> asserts item is NoteData = item => {
|
||||
if (!item) throw new Error(`Item ${item} is not a note`);
|
||||
if (isFolder(item)) throw new Error(`Expected item with ID ${item?.id} to be a note.`);
|
||||
};
|
||||
|
||||
export interface FuzzContext {
|
||||
serverUrl: string;
|
||||
@@ -88,6 +92,7 @@ export interface ActionableClient {
|
||||
allFolderDescendants(parentId: ItemId): Promise<ItemId[]>;
|
||||
randomFolder(options: RandomFolderOptions): Promise<FolderRecord>;
|
||||
randomNote(options: RandomNoteOptions): Promise<NoteData>;
|
||||
itemById(id: ItemId): TreeItem;
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
|
||||
34
packages/tools/fuzzer/utils/diffSortedStringArrays.test.ts
Normal file
34
packages/tools/fuzzer/utils/diffSortedStringArrays.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import diffSortedStringArrays from './diffSortedStringArrays';
|
||||
|
||||
describe('diffSortedStringArrays', () => {
|
||||
test.each([
|
||||
[
|
||||
['a'],
|
||||
[],
|
||||
{ unexpected: ['a'], missing: [] },
|
||||
],
|
||||
[
|
||||
['a', 'b'],
|
||||
['a', 'b'],
|
||||
{ unexpected: [], missing: [] },
|
||||
],
|
||||
[
|
||||
[],
|
||||
['a'],
|
||||
{ unexpected: [], missing: ['a'] },
|
||||
],
|
||||
[
|
||||
['a'],
|
||||
['b', 'c'],
|
||||
{ unexpected: ['a'], missing: ['b', 'c'] },
|
||||
],
|
||||
[
|
||||
['a', 'b', 'c', 'd', 'e', 'f'],
|
||||
['a', 'e'],
|
||||
{ unexpected: ['b', 'c', 'd', 'f'], missing: [] },
|
||||
],
|
||||
])('should diff string arrays: %j, %j', (actual, expected, diff) => {
|
||||
expect(diffSortedStringArrays(actual, expected)).toEqual(diff);
|
||||
});
|
||||
});
|
||||
|
||||
58
packages/tools/fuzzer/utils/diffSortedStringArrays.ts
Normal file
58
packages/tools/fuzzer/utils/diffSortedStringArrays.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
|
||||
// Input arrays must be sorted
|
||||
const diffSortedStringArrays = (actual: string[], expected: string[]) => {
|
||||
const missing = [];
|
||||
const unexpected = [];
|
||||
|
||||
let indexActual = 0;
|
||||
let indexExpected = 0;
|
||||
for (;
|
||||
indexActual < actual.length && indexExpected < expected.length;
|
||||
indexActual++, indexExpected++
|
||||
) {
|
||||
|
||||
const itemActual = actual[indexActual];
|
||||
const itemExpected = expected[indexExpected];
|
||||
|
||||
if (itemActual !== itemExpected) {
|
||||
let found = false;
|
||||
// Case 1: The expected item is present eventually, after a few unexpected item:
|
||||
for (let i = indexActual + 1; i < actual.length; i++) {
|
||||
if (actual[i] === itemExpected) {
|
||||
// Everything before the found item shouldn't have been present:
|
||||
unexpected.push(actual.slice(indexActual, i));
|
||||
|
||||
indexActual = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: The expected item wasn't present at all:
|
||||
if (!found) {
|
||||
missing.push(itemExpected);
|
||||
|
||||
// Revisit the current item in "actual" on the next loop:
|
||||
indexActual --;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle any unexpected items at the end
|
||||
if (indexActual < actual.length) {
|
||||
unexpected.push(actual.slice(indexActual));
|
||||
}
|
||||
if (indexExpected < expected.length) {
|
||||
missing.push(expected.slice(indexExpected));
|
||||
}
|
||||
|
||||
return {
|
||||
// Items that were present in the actual state, but are missing from the expected state
|
||||
unexpected: unexpected.flat(),
|
||||
// Items that were present in the expected state, but missing from the actual state.
|
||||
missing: missing.flat(),
|
||||
};
|
||||
};
|
||||
|
||||
export default diffSortedStringArrays;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Türkçe\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Arda Kılıçdağı <arda@kilicdagi.com>\n"
|
||||
"Language-Team: Turkish (Turkey)\n"
|
||||
"Language: tr_TR\n"
|
||||
@@ -328,7 +330,7 @@ msgstr "A5"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1121
|
||||
msgid "ABC musical notation: Options"
|
||||
msgstr ""
|
||||
msgstr "ABC müzik notasyonu: Seçenekler"
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx:62
|
||||
#: packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx:240
|
||||
@@ -424,7 +426,7 @@ msgstr "Gövde Ekle"
|
||||
|
||||
#: packages/editor/ProseMirror/plugins/tablePlugin.ts:31
|
||||
msgid "Add column"
|
||||
msgstr ""
|
||||
msgstr "Sütun ekle"
|
||||
|
||||
#: packages/app-mobile/components/buttons/FloatingActionButton.tsx:66
|
||||
#: packages/app-mobile/components/ComboBox.tsx:103
|
||||
@@ -441,9 +443,8 @@ msgid "Add recipient:"
|
||||
msgstr "Alıcı ekle:"
|
||||
|
||||
#: packages/editor/ProseMirror/plugins/tablePlugin.ts:26
|
||||
#, fuzzy
|
||||
msgid "Add row"
|
||||
msgstr "Yeni bir şey ekle"
|
||||
msgstr "Satır ekle"
|
||||
|
||||
#: packages/app-mobile/components/TagEditor.tsx:281
|
||||
msgid "Add tags:"
|
||||
@@ -801,7 +802,7 @@ msgstr "Beta"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:123
|
||||
msgid "Block code"
|
||||
msgstr ""
|
||||
msgstr "Blok kod"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:55
|
||||
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:80
|
||||
@@ -919,9 +920,8 @@ msgid "Cannot change encrypted item"
|
||||
msgstr "Şifrelenmiş öğe değiştirilemiyor"
|
||||
|
||||
#: packages/lib/commands/convertNoteToMarkdown.ts:42
|
||||
#, fuzzy
|
||||
msgid "Cannot convert read-only item: \"%s\""
|
||||
msgstr "Yeni bir not oluşturulamadı: %s"
|
||||
msgstr "Salt okunur öğe dönüştürülemiyor: “%s”"
|
||||
|
||||
#: packages/lib/models/Note.ts:622
|
||||
msgid "Cannot copy note to \"%s\" notebook"
|
||||
@@ -1077,7 +1077,7 @@ msgstr "Kontrol ediliyor... Lütfen bekleyin."
|
||||
|
||||
#: packages/app-desktop/gui/NoteContentPropertiesDialog.tsx:114
|
||||
msgid "Chinese/Japanese/Korean characters"
|
||||
msgstr ""
|
||||
msgstr "Çince/Japonca/Korece karakterler"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note/commands/attachFile.ts:98
|
||||
msgid "Choose an option"
|
||||
@@ -1244,7 +1244,7 @@ msgstr "Komut"
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:30
|
||||
msgid "COMMAND"
|
||||
msgstr ""
|
||||
msgstr "KOMUT"
|
||||
|
||||
#: packages/app-desktop/plugins/GotoAnything.tsx:783
|
||||
msgid "Command palette"
|
||||
@@ -1309,9 +1309,8 @@ msgid "Configuration"
|
||||
msgstr "Yapılandırma"
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:24
|
||||
#, fuzzy
|
||||
msgid "Configured keyboard shortcuts:"
|
||||
msgstr "Klavye Kısayolları"
|
||||
msgstr "Ayarlanmış klavye Kısayolları:"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1296
|
||||
msgid "Configures the size of scrollbars used in the app."
|
||||
@@ -1386,9 +1385,8 @@ msgid "Convert it"
|
||||
msgstr "Dönüştür"
|
||||
|
||||
#: packages/lib/commands/convertNoteToMarkdown.ts:18
|
||||
#, fuzzy
|
||||
msgid "Convert to Markdown"
|
||||
msgstr "Notu Markdown'a dönüştür"
|
||||
msgstr "Markdown'a dönüştür"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note/Note.tsx:1350
|
||||
msgid "Convert to note"
|
||||
@@ -1494,9 +1492,8 @@ msgid "Could not connect to plugin repository."
|
||||
msgstr "Eklenti sunucusuna bağlanılamadı."
|
||||
|
||||
#: packages/lib/commands/convertNoteToMarkdown.ts:70
|
||||
#, fuzzy
|
||||
msgid "Could not convert notes to Markdown: %s"
|
||||
msgstr "Not Markdown'a dönüştürülemedi: %s"
|
||||
msgstr "Notlar Markdown'a dönüştürülemedi: %s"
|
||||
|
||||
#: packages/app-desktop/InteropServiceHelper.ts:235
|
||||
msgid "Could not export notes: %s"
|
||||
@@ -1767,9 +1764,8 @@ msgid "Delete attachment \"%s\"?"
|
||||
msgstr "\"%s\" eki silinsin mi?"
|
||||
|
||||
#: packages/editor/ProseMirror/plugins/tablePlugin.ts:41
|
||||
#, fuzzy
|
||||
msgid "Delete column"
|
||||
msgstr "Satırı sil"
|
||||
msgstr "Sütunu sil"
|
||||
|
||||
#: packages/server/src/services/TaskService.ts:27
|
||||
msgid "Delete expired sessions"
|
||||
@@ -1810,19 +1806,19 @@ msgid "Delete profile \"%s\""
|
||||
msgstr "\"%s\" profilini sil"
|
||||
|
||||
#: packages/app-desktop/gui/ProfileEditor.tsx:147
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Delete profile \"%s\"?\n"
|
||||
"\n"
|
||||
"All data, including notes, notebooks and tags will be permanently deleted."
|
||||
msgstr ""
|
||||
"Not defterleri, etiketler ve notlar da dahil olmak üzere tüm veriler kalıcı "
|
||||
"olarak silinecek."
|
||||
"“%s” profili silinsin mi?\n"
|
||||
"\n"
|
||||
"Notlar, not defterleri ve etiketler dâhil tüm veriler kalıcı olarak "
|
||||
"silinecektir."
|
||||
|
||||
#: packages/editor/ProseMirror/plugins/tablePlugin.ts:36
|
||||
#, fuzzy
|
||||
msgid "Delete row"
|
||||
msgstr "Notu sil"
|
||||
msgstr "Satırı sil"
|
||||
|
||||
#: packages/app-mobile/components/ScreenHeader/index.tsx:487
|
||||
msgid "Delete selected notes"
|
||||
@@ -1835,6 +1831,8 @@ msgid ""
|
||||
"All notes associated with this tag will remain, but the tag will be removed "
|
||||
"from all notes."
|
||||
msgstr ""
|
||||
"\"%s\" etiketini silmek ister misiniz? Bu etiketle ilişkili tüm notlar "
|
||||
"kalacak, ancak etiket tüm notlardan kaldırılacaktır."
|
||||
|
||||
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.ts:38
|
||||
#: packages/app-mobile/components/side-menu-content.tsx:414
|
||||
@@ -2017,7 +2015,7 @@ msgstr "Notla ilgili tüm bilgileri görüntüler."
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:14
|
||||
msgid "Displays the configured keyboard shortcuts."
|
||||
msgstr ""
|
||||
msgstr "Yapılandırılmış klavye kısayollarını görüntüler."
|
||||
|
||||
#: packages/app-cli/app/command-cat.ts:14
|
||||
msgid "Displays the given note."
|
||||
@@ -2237,9 +2235,8 @@ msgid "Edit profile configuration..."
|
||||
msgstr "Profil ayarlarını düzenle…"
|
||||
|
||||
#: packages/app-mobile/components/screens/tags.tsx:64
|
||||
#, fuzzy
|
||||
msgid "Edit tag"
|
||||
msgstr "Notu düzenle."
|
||||
msgstr "Etiketi düzenle"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen.tsx:129
|
||||
#: packages/app-desktop/gui/NoteContentPropertiesDialog.tsx:151
|
||||
@@ -2349,9 +2346,8 @@ msgid "Enable abbreviation syntax"
|
||||
msgstr "Kısaltma söz dizimini etkinleştir"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1093
|
||||
#, fuzzy
|
||||
msgid "Enable ABC musical notation support"
|
||||
msgstr "Fountain söz dizimi desteğini etkinleştir"
|
||||
msgstr "ABC müzik notasyon desteğini etkinleştir"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1095
|
||||
msgid "Enable audio player"
|
||||
@@ -3133,9 +3129,8 @@ msgid "Import or export your data"
|
||||
msgstr "Verini içeri veya dışarı aktar"
|
||||
|
||||
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.ts:20
|
||||
#, fuzzy
|
||||
msgid "Import..."
|
||||
msgstr "Dışa aktarılıyor..."
|
||||
msgstr "İçe aktar…"
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteImportButton.tsx:76
|
||||
msgid "Imported successfully!"
|
||||
@@ -3515,7 +3510,7 @@ msgstr "Not tarihçesini şu kadar süre tut"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:2037
|
||||
msgid "Keep notes in the trash for"
|
||||
msgstr "Çöp kutusunda notları şu kadar süre tut: "
|
||||
msgstr "Çöp kutusunda notları şu kadar süre tut:"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1470
|
||||
msgid "Keyboard Mode"
|
||||
@@ -3535,7 +3530,7 @@ msgstr "Keychain Desteği: %s"
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:30
|
||||
msgid "KEYS"
|
||||
msgstr ""
|
||||
msgstr "TUŞLAR"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:74
|
||||
msgid "Keys that need upgrading"
|
||||
@@ -3763,6 +3758,8 @@ msgid ""
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile "
|
||||
"cannot be deleted."
|
||||
msgstr ""
|
||||
"Profillerini yönet. Profilleri yeniden adlandırabilir veya silebilirsin. O "
|
||||
"an kullanılan aktif profil silinemez."
|
||||
|
||||
#. `generate-ppk`
|
||||
#: packages/app-cli/app/command-e2ee.ts:19
|
||||
@@ -3923,7 +3920,6 @@ msgstr[0] "%d not \"%s\" not defterine taşınsın mı?"
|
||||
msgstr[1] "%d not \"%s\" not defterine taşınsın mı?"
|
||||
|
||||
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.ts:34
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Move %d notebooks to the trash?\n"
|
||||
"\n"
|
||||
@@ -4174,7 +4170,7 @@ msgstr "Güncelleme bulunamadı"
|
||||
|
||||
#: packages/lib/components/shared/SamlShared.ts:12
|
||||
msgid "No URL for SAML authentication set."
|
||||
msgstr ""
|
||||
msgstr "URL veya SAML doğrulaması ayarlanmamış."
|
||||
|
||||
#: packages/app-cli/app/command-share.ts:188
|
||||
#: packages/app-cli/app/command-share.ts:208
|
||||
@@ -4516,7 +4512,7 @@ msgstr "Senkronizasyon Sihirbazını Aç…"
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx:635
|
||||
msgid "Open-source licences"
|
||||
msgstr ""
|
||||
msgstr "Açık kaynaklı lisanslar"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:87
|
||||
msgid "Open..."
|
||||
@@ -4555,6 +4551,8 @@ msgid ""
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 "
|
||||
"object. The full list of options is available at: %s"
|
||||
msgstr ""
|
||||
"ABC kodu her işlendiğinde kullanılacak seçenekler. Bir JSON5 nesnesi "
|
||||
"olmalıdır. Seçeneklerin tam listesi şu adreste mevcuttur: %s"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:103
|
||||
msgid "Ordered list"
|
||||
@@ -4920,9 +4918,8 @@ msgid "Profile name"
|
||||
msgstr "Profil adı"
|
||||
|
||||
#: packages/app-desktop/gui/ProfileEditor.tsx:120
|
||||
#, fuzzy
|
||||
msgid "Profile name cannot be empty"
|
||||
msgstr "Şifre boş olamaz"
|
||||
msgstr "Profil adı boş olamaz"
|
||||
|
||||
#: packages/app-desktop/gui/ProfileEditor.tsx:116
|
||||
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.ts:18
|
||||
@@ -5115,11 +5112,8 @@ msgid "Remove"
|
||||
msgstr "Sil"
|
||||
|
||||
#: packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx:134
|
||||
#, fuzzy
|
||||
msgid "Remove %d tags from all notes? This cannot be undone."
|
||||
msgstr ""
|
||||
"Model silinip yeniden indirilsin mi?\n"
|
||||
"Bu işlem geri alınamaz."
|
||||
msgstr "Tüm notlardan %d etiket kaldırılsın mı? Bu işlem geri alınamaz."
|
||||
|
||||
#: packages/app-mobile/components/TagEditor.tsx:136
|
||||
msgid "Remove %s"
|
||||
@@ -5536,7 +5530,7 @@ msgstr "Ana not defterini seç"
|
||||
|
||||
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.ts:42
|
||||
msgid "Select the type of file to be imported:"
|
||||
msgstr ""
|
||||
msgstr "İçe aktarılacak dosya türünü seçin:"
|
||||
|
||||
#: packages/app-mobile/components/ComboBox.tsx:378
|
||||
msgid "Selected: %s"
|
||||
@@ -5861,7 +5855,7 @@ msgstr "Kaynak: "
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:35
|
||||
msgid "SPACE"
|
||||
msgstr ""
|
||||
msgstr "ALAN"
|
||||
|
||||
#: packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx:456
|
||||
msgid "Spacer"
|
||||
@@ -6172,18 +6166,16 @@ msgid "Tab moves focus"
|
||||
msgstr "Tab odak hareket ettirir"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:118
|
||||
#, fuzzy
|
||||
msgid "Table"
|
||||
msgstr "Etkinleştir"
|
||||
msgstr "Tablo"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1440
|
||||
msgid "Tabloid"
|
||||
msgstr "Tablo"
|
||||
|
||||
#: packages/app-mobile/components/screens/tags.tsx:206
|
||||
#, fuzzy
|
||||
msgid "Tag: %s"
|
||||
msgstr "Kullanım: %s"
|
||||
msgstr "Etiket: %s"
|
||||
|
||||
#: packages/app-cli/app/command-import.ts:58
|
||||
#: packages/app-desktop/gui/ImportScreen.tsx:94
|
||||
@@ -6404,7 +6396,6 @@ msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
|
||||
msgstr "“%s” notu başarılı bir şekilde “%s” not defterine geri yüklendi."
|
||||
|
||||
#: packages/lib/commands/convertNoteToMarkdown.ts:64
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"The note has been converted to Markdown and the original note has been moved "
|
||||
"to the trash"
|
||||
@@ -6412,7 +6403,8 @@ msgid_plural ""
|
||||
"The notes have been converted to Markdown and the original notes have been "
|
||||
"moved to the trash"
|
||||
msgstr[0] "Not Markdown'a dönüştürüldü ve orijinal not çöp kutusuna taşındı"
|
||||
msgstr[1] "Not Markdown'a dönüştürüldü ve orijinal not çöp kutusuna taşındı"
|
||||
msgstr[1] ""
|
||||
"Notlar Markdown'a dönüştürüldü ve orijinal notlar çöp kutusuna taşındı"
|
||||
|
||||
#: packages/app-desktop/gui/TrashNotification/TrashNotification.tsx:45
|
||||
msgid "The note was successfully moved to the trash."
|
||||
@@ -6991,7 +6983,7 @@ msgstr "Şimdi dene"
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:30
|
||||
msgid "TYPE"
|
||||
msgstr ""
|
||||
msgstr "TİP"
|
||||
|
||||
#: packages/app-cli/app/command-help.ts:72
|
||||
msgid ""
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/node-fetch": "2.6.13",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@types/yargs": "17.0.34",
|
||||
"gettext-extractor": "3.8.0",
|
||||
"gulp": "4.0.2",
|
||||
"html-entities": "1.4.0",
|
||||
|
||||
@@ -197,6 +197,10 @@
|
||||
"ios-v13.5.2": true,
|
||||
"android-v3.5.8": true,
|
||||
"ios-v13.5.3": true,
|
||||
"v3.5.11": true
|
||||
"v3.5.11": true,
|
||||
"v3.5.12": true,
|
||||
"v3.6.1": true,
|
||||
"v3.6.2": true,
|
||||
"android-v3.5.9": true
|
||||
}
|
||||
}
|
||||
@@ -186,6 +186,12 @@ async function createRelease(projectName: string, releaseConfig: ReleaseConfig,
|
||||
}
|
||||
|
||||
const uploadToGitHubRelease = async (projectName: string, tagName: string, isPreRelease: boolean, releaseFiles: Record<string, Release>) => {
|
||||
const allPublishDisabled = Object.values(releaseFiles).every(r => !r.publish);
|
||||
if (allPublishDisabled) {
|
||||
console.info('All release files have publishing disabled - skipping GitHub release creation');
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(`Creating GitHub release ${tagName}...`);
|
||||
|
||||
const releaseOptions = { isPreRelease: isPreRelease };
|
||||
@@ -323,10 +329,15 @@ async function main() {
|
||||
|
||||
await uploadToGitHubRelease(mainProjectName, tagName, isPreRelease, releaseFiles);
|
||||
|
||||
console.info(`Main download URL: ${releaseFiles['main'].downloadUrl}`);
|
||||
if (releaseFiles['main']) console.info(`Main download URL: ${releaseFiles['main'].downloadUrl}`);
|
||||
|
||||
const changelogPath = `${rootDir}/readme/about/changelog/android.md`;
|
||||
await completeReleaseWithChangelog(changelogPath, version, tagName, 'Android', isPreRelease);
|
||||
|
||||
// When creating the changelog, we always set `isPrerelease` to `false` - this is because we
|
||||
// only ever publish pre-releases, and it's only later that we manually promote some of them to
|
||||
// stable releases. So having "(Pre-release)" for each Android version in the changelog is
|
||||
// meaningless and would be incorrect for the versions that are stable ones.
|
||||
await completeReleaseWithChangelog(changelogPath, version, tagName, 'Android', false);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"dependencies": {
|
||||
"@joplin/utils": "~3.6",
|
||||
"@koa/cors": "3.4.3",
|
||||
"dotenv": "16.6.1",
|
||||
"dotenv": "17.2.3",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "11.3.2",
|
||||
"knex": "3.1.0",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"browserify": "14.5.0",
|
||||
"rollup": "0.50.1",
|
||||
"standard": "17.1.2",
|
||||
"turndown": "7.2.1",
|
||||
"turndown": "7.2.2",
|
||||
"turndown-attendant": "0.0.3"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"jsdom": "26.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "28.0.8",
|
||||
"@rollup/plugin-commonjs": "28.0.9",
|
||||
"@rollup/plugin-node-resolve": "16.0.3",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"browserify": "14.5.0",
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# Joplin Android Changelog
|
||||
|
||||
## [android-v3.5.8](https://github.com/laurent22/joplin/releases/tag/android-v3.5.8) (Pre-release) - 2026-01-10T10:08:46Z
|
||||
## [android-v3.5.9](https://github.com/laurent22/joplin/releases/tag/android-v3.5.9) - 2026-01-19T16:24:19Z
|
||||
|
||||
- Improved: Remove unnecessary READ_PHONE_STATE permission (#14157 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.5.8](https://github.com/laurent22/joplin/releases/tag/android-v3.5.8) - 2026-01-10T10:08:46Z
|
||||
|
||||
- Fixed: Fixed keyboard input issue in note title (#14070) (#13544 by [@mrjo118](https://github.com/mrjo118))
|
||||
|
||||
## [android-v3.5.7](https://github.com/laurent22/joplin/releases/tag/android-v3.5.7) (Pre-release) - 2026-01-08T19:30:28Z
|
||||
## [android-v3.5.7](https://github.com/laurent22/joplin/releases/tag/android-v3.5.7) - 2026-01-08T19:30:28Z
|
||||
|
||||
- New: Rich Text Editor: Add shortcuts for inserting code blocks (#14055 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Accessibility: In-editor rendering: Fix rendered checkboxes are very small on mobile (#14056 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -16,18 +20,18 @@
|
||||
- Fixed: Images sometimes don't render until you click somewhere in the note (#14019) (#13963 by [@bwat47](https://github.com/bwat47))
|
||||
- Fixed: Insert time command not respecting locale settings (#13994) (#13229 by [@HIJOdelIDANII](https://github.com/HIJOdelIDANII))
|
||||
|
||||
## [android-v3.5.6](https://github.com/laurent22/joplin/releases/tag/android-v3.5.6) (Pre-release) - 2025-12-27T20:34:44Z
|
||||
## [android-v3.5.6](https://github.com/laurent22/joplin/releases/tag/android-v3.5.6) - 2025-12-27T20:34:44Z
|
||||
|
||||
- Revert "All: Apache Tomcat WebDAV compatibility for sync (#13614)"
|
||||
|
||||
## [android-v3.5.5](https://github.com/laurent22/joplin/releases/tag/android-v3.5.5) (Pre-release) - 2025-12-26T10:53:20Z
|
||||
## [android-v3.5.5](https://github.com/laurent22/joplin/releases/tag/android-v3.5.5) - 2025-12-26T10:53:20Z
|
||||
|
||||
- Improved: Update js-draw to v1.33.0 (#13990 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Updated packages react-native-webview (v13.16.0)
|
||||
- Fixed: Editor: Fix search/replace UI is partially off-screen on small-screen devices (#13978 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Feature flags: Fix "voice typing" feature flag (#13981 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.5.4](https://github.com/laurent22/joplin/releases/tag/android-v3.5.4) (Pre-release) - 2025-12-23T20:00:18Z
|
||||
## [android-v3.5.4](https://github.com/laurent22/joplin/releases/tag/android-v3.5.4) - 2025-12-23T20:00:18Z
|
||||
|
||||
- Improved: Accessibility: Dark mode: Improve contrast of conflicts notebook title, error messages in "Logs" (#13925 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Attempt to fix application hang when opening the camera (#13974 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -40,7 +44,7 @@
|
||||
- Fixed: Rich Text Editor: Fix indent/de-indent buttons do nothing when not in a list (#13961 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Toolbar editor: Fix toolbar editor dismiss button is rendered outside the dialog on small screens (#13976 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.5.3](https://github.com/laurent22/joplin/releases/tag/android-v3.5.3) (Pre-release) - 2025-12-14T13:46:02Z
|
||||
## [android-v3.5.3](https://github.com/laurent22/joplin/releases/tag/android-v3.5.3) - 2025-12-14T13:46:02Z
|
||||
|
||||
- New: Add a link to the list of open-source licenses (5caec16)
|
||||
- New: Add the ability to rename and delete tags (#13731 by [@mrjo118](https://github.com/mrjo118))
|
||||
@@ -54,7 +58,7 @@
|
||||
- Fixed: Markdown import incorrectly parses a link as a file path (#12172)
|
||||
- Fixed: Rich Text Editor: Fix table delete row/delete column buttons can't remove the last row/column from a table (#13877 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.5.1](https://github.com/laurent22/joplin/releases/tag/android-v3.5.1) (Pre-release) - 2025-11-29T12:33:28Z
|
||||
## [android-v3.5.1](https://github.com/laurent22/joplin/releases/tag/android-v3.5.1) - 2025-11-29T12:33:28Z
|
||||
|
||||
- New: Add support for mixed case tags (#12931 by [@mrjo118](https://github.com/mrjo118))
|
||||
- New: Add the ability to search on the tag list screen (#13733 by [@mrjo118](https://github.com/mrjo118))
|
||||
@@ -130,13 +134,13 @@
|
||||
- Fixed: Treat unclosed quotes as fully quoted search terms, to prevent malformed match expression error (#13564) (#13319 by [@mrjo118](https://github.com/mrjo118))
|
||||
- Fixed: When creating a conflict, ensure the latest note contents are used to create the conflict (#13552) (#13531 by [@mrjo118](https://github.com/mrjo118))
|
||||
|
||||
## [android-v3.4.7](https://github.com/laurent22/joplin/releases/tag/android-v3.4.7) (Pre-release) - 2025-09-09T08:09:54Z
|
||||
## [android-v3.4.7](https://github.com/laurent22/joplin/releases/tag/android-v3.4.7) - 2025-09-09T08:09:54Z
|
||||
|
||||
- Fixed: Fix error when saving in-editor rendering-related settings (#13105) (#13103 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Fix light bar shown above header in dark mode (#13132 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Plugins: Fix renderer plugins that use the `settingValue` API (#13131 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.4.6](https://github.com/laurent22/joplin/releases/tag/android-v3.4.6) (Pre-release) - 2025-09-01T12:02:41Z
|
||||
## [android-v3.4.6](https://github.com/laurent22/joplin/releases/tag/android-v3.4.6) - 2025-09-01T12:02:41Z
|
||||
|
||||
- Fixed: Fix "edit profile" button is partially off-screen (#13084) (#13015 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Fix shadow shown above the screen header (#13074 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -144,7 +148,7 @@
|
||||
- Fixed: Plugin API: Fix compatibility with certain plugins targetting the desktop app (#13077 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Plugins: Fix plugin panel buttons are off-screen on recent versions of Android (#13080 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.4.5](https://github.com/laurent22/joplin/releases/tag/android-v3.4.5) (Pre-release) - 2025-08-27T06:27:53Z
|
||||
## [android-v3.4.5](https://github.com/laurent22/joplin/releases/tag/android-v3.4.5) - 2025-08-27T06:27:53Z
|
||||
|
||||
- New: Add a "highlight active line" setting (#12967 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- New: Rich Text Editor: Add basic support for collapsible <details> blocks (#12946 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -167,7 +171,7 @@
|
||||
- Fixed: Rich Text Editor: Fix additional blank lines added around list items on save (#12935 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Shared folders: Fix moving shared subfolder to top-level briefly marks it as a top-level share (#12964 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.4.4](https://github.com/laurent22/joplin/releases/tag/android-v3.4.4) (Pre-release) - 2025-08-10T09:31:45Z
|
||||
## [android-v3.4.4](https://github.com/laurent22/joplin/releases/tag/android-v3.4.4) - 2025-08-10T09:31:45Z
|
||||
|
||||
- Improved: Allow editing code blocks from the Rich Text Editor (#12906) (#12841 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Fixed missing filename when a file is shared with the app (#12895) (#12858 by [@klaas0](https://github.com/klaas0))
|
||||
@@ -180,7 +184,7 @@
|
||||
- Fixed: Fix switching between note and todo on mobile (#12849) (#12822 by [@mrjo118](https://github.com/mrjo118))
|
||||
- Fixed: Rich Text Editor: Make initial search behavior match the Markdown editor (#12878) (#12844 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.4.3](https://github.com/laurent22/joplin/releases/tag/android-v3.4.3) (Pre-release) - 2025-08-04T17:38:13Z
|
||||
## [android-v3.4.3](https://github.com/laurent22/joplin/releases/tag/android-v3.4.3) - 2025-08-04T17:38:13Z
|
||||
|
||||
- New: Add a Rich Text Editor (#12748 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Performance: Improve Rich Text Editor startup performance (#12819 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -191,12 +195,12 @@
|
||||
- Fixed: Improve usability of inline search in notes (#12791) (#12783 by [@mrjo118](https://github.com/mrjo118))
|
||||
- Fixed: Markdown editor: Make list indentation size equivalent to four spaces (#12794) (#12573 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.4.2](https://github.com/laurent22/joplin/releases/tag/android-v3.4.2) (Pre-release) - 2025-07-25T08:30:30Z
|
||||
## [android-v3.4.2](https://github.com/laurent22/joplin/releases/tag/android-v3.4.2) - 2025-07-25T08:30:30Z
|
||||
|
||||
- Improved: Updated packages react-native-paper (v5.13.5)
|
||||
- Fixed: Fix title bar is partially hidden by the screen header (#12785 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.4.1](https://github.com/laurent22/joplin/releases/tag/android-v3.4.1) (Pre-release) - 2025-07-24T10:57:44Z
|
||||
## [android-v3.4.1](https://github.com/laurent22/joplin/releases/tag/android-v3.4.1) - 2025-07-24T10:57:44Z
|
||||
|
||||
- New: Add Joplin Server SAML support (#11865 by [@ttcchhmm](https://github.com/ttcchhmm))
|
||||
- New: Add delete line, duplicate line and sort selected lines buttons to editor toolbar (#12555 by [@mrjo118](https://github.com/mrjo118))
|
||||
@@ -230,32 +234,32 @@
|
||||
- Fixed: Fix voice typing fails to start on certain devices (#12351 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Moving sub-notebook of shared notebook should unshare it (#12647) (#12089)
|
||||
|
||||
## [android-v3.3.11](https://github.com/laurent22/joplin/releases/tag/android-v3.3.11) (Pre-release) - 2025-07-09T22:51:55Z
|
||||
## [android-v3.3.11](https://github.com/laurent22/joplin/releases/tag/android-v3.3.11) - 2025-07-09T22:51:55Z
|
||||
|
||||
- Fixed: Biometrics: Fix notebook list can still be accessed when the app is locked (#12691 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.10](https://github.com/laurent22/joplin/releases/tag/android-v3.3.10) (Pre-release) - 2025-06-10T08:07:25Z
|
||||
## [android-v3.3.10](https://github.com/laurent22/joplin/releases/tag/android-v3.3.10) - 2025-06-10T08:07:25Z
|
||||
|
||||
- New: Add additional checks when updating sidebar state (#12428 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.9](https://github.com/laurent22/joplin/releases/tag/android-v3.3.9) (Pre-release) - 2025-06-09T17:11:04Z
|
||||
## [android-v3.3.9](https://github.com/laurent22/joplin/releases/tag/android-v3.3.9) - 2025-06-09T17:11:04Z
|
||||
|
||||
- Fixed: Voice typing: Fix memory leak (#12402 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.8](https://github.com/laurent22/joplin/releases/tag/android-v3.3.8) (Pre-release) - 2025-05-01T15:45:35Z
|
||||
## [android-v3.3.8](https://github.com/laurent22/joplin/releases/tag/android-v3.3.8) - 2025-05-01T15:45:35Z
|
||||
|
||||
- New: Force quick action shortcuts to have the same size (#12195 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Increase space between new note/to-do buttons (#12194 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Fix new note menu size (#12193) (#12191 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.7](https://github.com/laurent22/joplin/releases/tag/android-v3.3.7) (Pre-release) - 2025-04-29T13:06:06Z
|
||||
## [android-v3.3.7](https://github.com/laurent22/joplin/releases/tag/android-v3.3.7) - 2025-04-29T13:06:06Z
|
||||
|
||||
- New: Add plural forms for notes, users, hours, minutes, days (#12171 by [@SilverGreen93](https://github.com/SilverGreen93))
|
||||
- Improved: Allow new note and new to-do buttons to wrap (#12163 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Update immer to v9.0.21 (#12182 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Editor: Allow syntax highlighting within ==highlight==s (#12167) (#12110 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.6](https://github.com/laurent22/joplin/releases/tag/android-v3.3.6) (Pre-release) - 2025-04-24T08:34:56Z
|
||||
## [android-v3.3.6](https://github.com/laurent22/joplin/releases/tag/android-v3.3.6) - 2025-04-24T08:34:56Z
|
||||
|
||||
- Improved: Improve UI for downloading updated models (#12145 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Note editor: Hash links: Move cursor to header or anchor associated with link target (#12129 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -266,7 +270,7 @@
|
||||
- Fixed: Markdown Editor: Fix numbered sublist renumbering (#12091 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Settings: Fix desktop-specific setting visible in note > advanced (#12146 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.5](https://github.com/laurent22/joplin/releases/tag/android-v3.3.5) (Pre-release) - 2025-04-07T19:31:26Z
|
||||
## [android-v3.3.5](https://github.com/laurent22/joplin/releases/tag/android-v3.3.5) - 2025-04-07T19:31:26Z
|
||||
|
||||
- New: Add "swap line up" and "swap line down" to toolbar extended options (#12053 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- New: Plugins: Add command to hide the plugin panel viewer (#12018 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -283,12 +287,12 @@
|
||||
- Fixed: Restoring a note which was in a deleted notebook (#12016) (#11934)
|
||||
- Fixed: Voice typing: Fix incorrectly-calculated audio length (#12012 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.4](https://github.com/laurent22/joplin/releases/tag/android-v3.3.4) (Pre-release) - 2025-03-21T18:07:00Z
|
||||
## [android-v3.3.4](https://github.com/laurent22/joplin/releases/tag/android-v3.3.4) - 2025-03-21T18:07:00Z
|
||||
|
||||
- Improved: Voice typing: Improve processing with larger models (#11983 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Voice typing: Improve re-download button UI (#11979) (#11955 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.3](https://github.com/laurent22/joplin/releases/tag/android-v3.3.3) (Pre-release) - 2025-03-16T10:29:52Z
|
||||
## [android-v3.3.3](https://github.com/laurent22/joplin/releases/tag/android-v3.3.3) - 2025-03-16T10:29:52Z
|
||||
|
||||
- New: Add setting migration for ocr.enabled (ab86b95)
|
||||
- Improved: Accessibility: Improve focus handling in the note actions menu and modal dialogs (#11929 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -298,7 +302,7 @@
|
||||
- Fixed: Make tab size consistent between Markdown editor and viewer (and RTE) (#11940) (#11673)
|
||||
- Fixed: Voice typing: Fix potential output duplication when finalizing voice typing (#11953 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.2](https://github.com/laurent22/joplin/releases/tag/android-v3.3.2) (Pre-release) - 2025-03-03T22:35:08Z
|
||||
## [android-v3.3.2](https://github.com/laurent22/joplin/releases/tag/android-v3.3.2) - 2025-03-03T22:35:08Z
|
||||
|
||||
- Improved: Improve encryption config screen accessibility (#11874) (#11846 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Switch default library used for Whisper voice typing (#11881 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -307,7 +311,7 @@
|
||||
- Fixed: Fix disabled encryption keys list showing enabled keys (#11861) (#11858 by [@pedr](https://github.com/pedr))
|
||||
- Fixed: Fix voice recorder crash (#11876) (#11864 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.1](https://github.com/laurent22/joplin/releases/tag/android-v3.3.1) (Pre-release) - 2025-02-19T16:01:54Z
|
||||
## [android-v3.3.1](https://github.com/laurent22/joplin/releases/tag/android-v3.3.1) - 2025-02-19T16:01:54Z
|
||||
|
||||
- New: Add support for plugin editor views (#11831)
|
||||
- Improved: Accessibility: Improve contrast of faded URLs in Markdown editor (#11635 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -337,17 +341,17 @@
|
||||
- Fixed: Use alternative fix to set the sqlite CursorWindow size to 50mb (#11726) (#11571 by [@mrjo118](https://github.com/mrjo118))
|
||||
- Security: Improve comment escaping (#11706 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.2.7](https://github.com/laurent22/joplin/releases/tag/android-v3.2.7) (Pre-release) - 2025-01-13T17:03:36Z
|
||||
## [android-v3.2.7](https://github.com/laurent22/joplin/releases/tag/android-v3.2.7) - 2025-01-13T17:03:36Z
|
||||
|
||||
- Fixed: Clicking on an external note link from within a note logs an error (#11619) (#11455 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Fix clicking "Draw picture" results in blank screen with very old WebView versions (#11604 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.2.5](https://github.com/laurent22/joplin/releases/tag/android-v3.2.5) (Pre-release) - 2025-01-07T23:35:43Z
|
||||
## [android-v3.2.5](https://github.com/laurent22/joplin/releases/tag/android-v3.2.5) - 2025-01-07T23:35:43Z
|
||||
|
||||
- Improved: Allow re-downloading voice typing models on URL change and error (#11557 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Upgrade js-draw to 1.26.0 (#11589 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.2.4](https://github.com/laurent22/joplin/releases/tag/android-v3.2.4) (Pre-release) - 2025-01-06T12:50:23Z
|
||||
## [android-v3.2.4](https://github.com/laurent22/joplin/releases/tag/android-v3.2.4) - 2025-01-06T12:50:23Z
|
||||
|
||||
- New: Plugin API: Add support for the renderMarkup command (#11494 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Accessibility: Improve sidemenu notebook list accessibility (#11556 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -362,7 +366,7 @@
|
||||
- Fixed: Fix missing "Insert Time" button (#11542) (#11539 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Locked out of mobile app due to broken fingerprint scanner (#10926)
|
||||
|
||||
## [android-v3.2.3](https://github.com/laurent22/joplin/releases/tag/android-v3.2.3) (Pre-release) - 2024-12-11T13:58:14Z
|
||||
## [android-v3.2.3](https://github.com/laurent22/joplin/releases/tag/android-v3.2.3) - 2024-12-11T13:58:14Z
|
||||
|
||||
- New: Accessibility: Add checked/unchecked accessibility information to the "sort notes by" dialog (#11411 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- New: Translation: Add sk_SK.po (Slovak) (#11433 by [@dodog](https://github.com/dodog))
|
||||
@@ -384,7 +388,7 @@
|
||||
- Fixed: Fix the error caused by undefined isCodeBlock_ (turndown-plugin-gfm) (#11471 by Manabu Nakazawa)
|
||||
- Fixed: Upgrade CodeMirror packages (#11440) (#11318 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.2.2](https://github.com/laurent22/joplin/releases/tag/android-v3.2.2) (Pre-release) - 2024-11-19T01:12:43Z
|
||||
## [android-v3.2.2](https://github.com/laurent22/joplin/releases/tag/android-v3.2.2) - 2024-11-19T01:12:43Z
|
||||
|
||||
- Improved: Accessibility: Improve dialog accessibility (#11395 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Deprecated OneDrive sync method (e36f377)
|
||||
@@ -393,7 +397,7 @@
|
||||
- Fixed: Fix race condition which may cause data loss, particularly before or after pasting text in the note editor (#11334) (#11317 by [@mrjo118](https://github.com/mrjo118))
|
||||
- Fixed: Fix vertical alignment of checkboxes when text wraps over multiple lines (226a8b3)
|
||||
|
||||
## [android-v3.2.1](https://github.com/laurent22/joplin/releases/tag/android-v3.2.1) (Pre-release) - 2024-11-10T14:23:47Z
|
||||
## [android-v3.2.1](https://github.com/laurent22/joplin/releases/tag/android-v3.2.1) - 2024-11-10T14:23:47Z
|
||||
|
||||
- New: Add new encryption methods based on native crypto libraries (#10696 by Self Not Found)
|
||||
- New: Add setting to disable markup autocompletion (#11222 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -411,17 +415,17 @@
|
||||
- Fixed: Handle callback url triggered app launch (#11280) (#9204 by [@tiberiusteng](https://github.com/tiberiusteng))
|
||||
- Fixed: Upgrade react-native-quick-crypto to v0.7.5 (#11294 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.1.8](https://github.com/laurent22/joplin/releases/tag/android-v3.1.8) (Pre-release) - 2024-11-09T13:02:33Z
|
||||
## [android-v3.1.8](https://github.com/laurent22/joplin/releases/tag/android-v3.1.8) - 2024-11-09T13:02:33Z
|
||||
|
||||
- Fixed: Fix error on creating new notes if the user is a share recipient (#11326) (#11325 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Fix new note button is pushed off-screen on certain Android devices (#11323) (#11276 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Fix sharing to Joplin causes back navigation to get stuck (#11355) (#11324 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.1.7](https://github.com/laurent22/joplin/releases/tag/android-v3.1.7) (Pre-release) - 2024-11-04T20:27:52Z
|
||||
## [android-v3.1.7](https://github.com/laurent22/joplin/releases/tag/android-v3.1.7) - 2024-11-04T20:27:52Z
|
||||
|
||||
- Fixed: Fix search result note hidden after powering on device (#11297) (#11197 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.1.6](https://github.com/laurent22/joplin/releases/tag/android-v3.1.6) (Pre-release) - 2024-10-17T22:13:06Z
|
||||
## [android-v3.1.6](https://github.com/laurent22/joplin/releases/tag/android-v3.1.6) - 2024-10-17T22:13:06Z
|
||||
|
||||
- Improved: Downgrade CodeMirror packages to fix various Android regressions (#11170 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Plugins: Name webview root attribute so that it can be styled (75b8caf)
|
||||
@@ -431,7 +435,7 @@
|
||||
- Fixed: Fix new note/edit buttons only work if pressed quickly (#11185) (#11183 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Fix regression: Search screen not hidden when cached for search result navigation (#11131) (#11130 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.1.5](https://github.com/laurent22/joplin/releases/tag/android-v3.1.5) (Pre-release) - 2024-10-11T22:11:20Z
|
||||
## [android-v3.1.5](https://github.com/laurent22/joplin/releases/tag/android-v3.1.5) - 2024-10-11T22:11:20Z
|
||||
|
||||
- Improved: Downgrade CodeMirror packages to fix various Android regressions (#11170 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Plugins: Name webview root attribute so that it can be styled (75b8caf)
|
||||
@@ -441,7 +445,7 @@
|
||||
- Fixed: Fix new note/edit buttons only work if pressed quickly (#11185) (#11183 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Fix regression: Search screen not hidden when cached for search result navigation (#11131) (#11130 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.1.4](https://github.com/laurent22/joplin/releases/tag/android-v3.1.4) (Pre-release) - 2024-09-24T14:21:42Z
|
||||
## [android-v3.1.4](https://github.com/laurent22/joplin/releases/tag/android-v3.1.4) - 2024-09-24T14:21:42Z
|
||||
|
||||
- Improved: Automatically detect and use operating system theme by default (5beb80b)
|
||||
- Improved: Make pressing "back" navigate to the previous note after following a link (#11086) (#11082 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -461,7 +465,7 @@
|
||||
- Fixed: Move accessibility focus to the first note action menu item on open (#11031) (#10253 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: WebDAV synchronisation not working because of URL encoding differences (#11076) (#10608 by [@pedr](https://github.com/pedr))
|
||||
|
||||
## [android-v3.1.3](https://github.com/laurent22/joplin/releases/tag/android-v3.1.3) (Pre-release) - 2024-09-02T12:16:46Z
|
||||
## [android-v3.1.3](https://github.com/laurent22/joplin/releases/tag/android-v3.1.3) - 2024-09-02T12:16:46Z
|
||||
|
||||
- Improved: Added feature flag to disable sync lock support (#10925) (#10407)
|
||||
- Improved: Make feature flags advanced settings by default (700ffa2)
|
||||
@@ -473,7 +477,7 @@
|
||||
- Fixed: Fixed italic support in Fountain documents (5fdd088)
|
||||
- Fixed: Markdown editor: Fix toggling bulleted lists when items start with asterisks (#10902) (#10891 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.1.2](https://github.com/laurent22/joplin/releases/tag/android-v3.1.2) (Pre-release) - 2024-08-10T11:44:30Z
|
||||
## [android-v3.1.2](https://github.com/laurent22/joplin/releases/tag/android-v3.1.2) - 2024-08-10T11:44:30Z
|
||||
|
||||
- Fixed: Fix WebDAV sync on mobile (#10849) (#10848 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Improve RTL support in the Markdown editor (#10810 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -490,26 +494,26 @@
|
||||
- Fixed: Remove search bar from plugins screen (#10648) (#10596 by Siddhant Paritosh Rao)
|
||||
- Fixed: Show notification in case Joplin Cloud credential is not valid anymore (#10649) (#10645 by [@pedr](https://github.com/pedr))
|
||||
|
||||
## [android-v3.0.9](https://github.com/laurent22/joplin/releases/tag/android-v3.0.9) (Pre-release) - 2024-07-28T14:00:59Z
|
||||
## [android-v3.0.9](https://github.com/laurent22/joplin/releases/tag/android-v3.0.9) - 2024-07-28T14:00:59Z
|
||||
|
||||
- Fixed: #10677: Following a link to a previously open note wouldn't work (#10750) (#10677 by [@pedr](https://github.com/pedr))
|
||||
- Fixed: Fix manual resource download mode (#10748 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.0.8](https://github.com/laurent22/joplin/releases/tag/android-v3.0.8) (Pre-release) - 2024-07-06T10:26:06Z
|
||||
## [android-v3.0.8](https://github.com/laurent22/joplin/releases/tag/android-v3.0.8) - 2024-07-06T10:26:06Z
|
||||
|
||||
- Fixed: Fix sidebar performance regression with many nested notebooks (#10676) (#10674 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.0.7](https://github.com/laurent22/joplin/releases/tag/android-v3.0.7) (Pre-release) - 2024-07-01T15:47:15Z
|
||||
## [android-v3.0.7](https://github.com/laurent22/joplin/releases/tag/android-v3.0.7) - 2024-07-01T15:47:15Z
|
||||
|
||||
- Improved: Set min version for synchronising to 3.0.0 (e4b8976)
|
||||
- Fixed: Show notification in case Joplin Cloud credential is not valid anymore (#10649) (#10645 by [@pedr](https://github.com/pedr))
|
||||
|
||||
## [android-v3.0.6](https://github.com/laurent22/joplin/releases/tag/android-v3.0.6) (Pre-release) - 2024-06-29T09:41:10Z
|
||||
## [android-v3.0.6](https://github.com/laurent22/joplin/releases/tag/android-v3.0.6) - 2024-06-29T09:41:10Z
|
||||
|
||||
- Updated Chinese and German translation (#10660 by [@cedecode](https://github.com/cedecode))
|
||||
- Fixed: Fix refocusing the note editor (#10644) (#10637 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.0.5](https://github.com/laurent22/joplin/releases/tag/android-v3.0.5) (Pre-release) - 2024-06-19T12:02:12Z
|
||||
## [android-v3.0.5](https://github.com/laurent22/joplin/releases/tag/android-v3.0.5) - 2024-06-19T12:02:12Z
|
||||
|
||||
- Improved: Don't render empty title page for Fountain (#10631 by [@XPhyro](https://github.com/XPhyro))
|
||||
- Improved: Don't show an "expand" arrow by "Installed plugins" when no plugins are installed (#10583 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -528,7 +532,7 @@
|
||||
- Fixed: Plugin settings screen: Fix plugin states not set correctly when installing multiple plugins at once (#10580 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Plugin settings: Fix plugins without settings can't be disabled without reinstall (#10579 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.0.4](https://github.com/laurent22/joplin/releases/tag/android-v3.0.4) (Pre-release) - 2024-06-12T20:38:44Z
|
||||
## [android-v3.0.4](https://github.com/laurent22/joplin/releases/tag/android-v3.0.4) - 2024-06-12T20:38:44Z
|
||||
|
||||
- New: Add Joplin Cloud account information to configuration screen (#10553 by [@pedr](https://github.com/pedr))
|
||||
- New: Add button on Synchronization to Joplin Cloud login screen (#10569 by [@pedr](https://github.com/pedr))
|
||||
@@ -558,7 +562,7 @@
|
||||
- Fixed: Fix plugins not reloaded when the plugin runner reloads (#10540 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Maintain cursor position when changing list indentation (#10441) (#10439 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.0.3](https://github.com/laurent22/joplin/releases/tag/android-v3.0.3) (Pre-release) - 2024-04-27T11:21:48Z
|
||||
## [android-v3.0.3](https://github.com/laurent22/joplin/releases/tag/android-v3.0.3) - 2024-04-27T11:21:48Z
|
||||
|
||||
- Improved: Display a message when Joplin Cloud user don't have access to email to note feature (#10322 by [@pedr](https://github.com/pedr))
|
||||
- Improved: Make editor styles closer to desktop (#10377 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -573,7 +577,7 @@
|
||||
- Fixed: Fix sync icon off-center (#10350) (#10351 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Plugins: Fix API incompatibility in arguments to `onMessage` listeners in panels (#10375 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.0.2](https://github.com/laurent22/joplin/releases/tag/android-v3.0.2) (Pre-release) - 2024-04-15T18:06:46Z
|
||||
## [android-v3.0.2](https://github.com/laurent22/joplin/releases/tag/android-v3.0.2) - 2024-04-15T18:06:46Z
|
||||
|
||||
- Improved: Allow marking a plugin as mobile-only or desktop-only (#10229) (#10206 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Allow marking items as "ignored" in sync status (#10261) (#10245 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -604,7 +608,7 @@
|
||||
- Fixed: Plugin API: Fix unable to require `@codemirror/search` (#10205 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Plugins: Fix event listener memory leak when disabling/uninstalling plugins (#10280 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.0.1](https://github.com/laurent22/joplin/releases/tag/android-v3.0.1) (Pre-release) - 2024-03-21T18:27:47Z
|
||||
## [android-v3.0.1](https://github.com/laurent22/joplin/releases/tag/android-v3.0.1) - 2024-03-21T18:27:47Z
|
||||
|
||||
- New: Add support for Markdown editor plugins (#10086 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- New: Add support for plugin panels and dialogs (#10121 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
@@ -628,23 +632,23 @@
|
||||
- Fixed: Plugins: Fix warning after reloading plugins (#10165 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Shows only the real folders in the dropdown of parent folders. (#10147) (#10143 by [@Sidd-R](https://github.com/Sidd-R))
|
||||
|
||||
## [android-v2.14.9](https://github.com/laurent22/joplin/releases/tag/android-v2.14.9) (Pre-release) - 2024-02-26T19:56:11Z
|
||||
## [android-v2.14.9](https://github.com/laurent22/joplin/releases/tag/android-v2.14.9) - 2024-02-26T19:56:11Z
|
||||
|
||||
- Fixed: Note editor: Support older WebView versions (#9986) (#9521 by Henry Heino)
|
||||
- Fixed: Sort notebooks in a case-insensitive way (#9996)
|
||||
|
||||
## [android-v2.14.8](https://github.com/laurent22/joplin/releases/tag/android-v2.14.8) (Pre-release) - 2024-02-22T22:29:24Z
|
||||
## [android-v2.14.8](https://github.com/laurent22/joplin/releases/tag/android-v2.14.8) - 2024-02-22T22:29:24Z
|
||||
|
||||
- Improved: Immediately sort notes after toggling a checkbox (5820f63)
|
||||
- Fixed: Fix auto-indentation in some types of code blocks (#9972) (#9971 by Henry Heino)
|
||||
|
||||
## [android-v2.14.7](https://github.com/laurent22/joplin/releases/tag/android-v2.14.7) (Pre-release) - 2024-02-19T10:40:10Z
|
||||
## [android-v2.14.7](https://github.com/laurent22/joplin/releases/tag/android-v2.14.7) - 2024-02-19T10:40:10Z
|
||||
|
||||
- Improved: Migrate profile in preparation for trash feature (115eb5d)
|
||||
- Improved: Updated packages tar-stream (v3.1.7)
|
||||
- Fixed: Fix full text search broken on Android 7 and earlier (#9914) (#9905 by Henry Heino)
|
||||
|
||||
## [android-v2.14.6](https://github.com/laurent22/joplin/releases/tag/android-v2.14.6) (Pre-release) - 2024-02-09T12:41:18Z
|
||||
## [android-v2.14.6](https://github.com/laurent22/joplin/releases/tag/android-v2.14.6) - 2024-02-09T12:41:18Z
|
||||
|
||||
- Improved: Improve search engine error handling when preparing text for search (#9871 by Henry Heino)
|
||||
- Improved: Updated packages @js-draw/material-icons (v1.16.1), @react-native-community/netinfo (v11.2.1), @react-native-community/slider (v4.5.0), async-mutex (v0.4.1), follow-redirects (v1.15.5), js-draw (v1.16.1), moment (v2.30.1), react-native-document-picker (v9.1.0), react-native-localize (v3.0.6), react-native-paper (v5.11.7), react-native-safe-area-context (v4.8.2), react-native-share (v10.0.2), react-native-webview (v13.6.4), sass (v1.69.7), sharp (v0.33.2), sqlite3 (v5.1.7)
|
||||
@@ -654,7 +658,7 @@
|
||||
- Fixed: Fix share to Joplin when only "All notes" has been opened (#9876) (#9863 by Henry Heino)
|
||||
- Fixed: Increase space available for Notebook icon (#9877) (#9475 by [@pedr](https://github.com/pedr))
|
||||
|
||||
## [android-v2.14.5](https://github.com/laurent22/joplin/releases/tag/android-v2.14.5) (Pre-release) - 2024-02-02T23:09:50Z
|
||||
## [android-v2.14.5](https://github.com/laurent22/joplin/releases/tag/android-v2.14.5) - 2024-02-02T23:09:50Z
|
||||
|
||||
- Improved: Allow note viewer to extend to the edge of the screen while pinch zooming (#9820) (#9819 by Henry Heino)
|
||||
- Improved: Do not allow switching the sync target if not all resources are downloaded (#9263)
|
||||
@@ -667,7 +671,7 @@
|
||||
- Fixed: Fix note editor errors/logs not sent to Joplin's logs (#9808) (#9807 by Henry Heino)
|
||||
- Fixed: Fix synchronization happens every 10 seconds even if nothing has changed (#9814) (#9800 by Henry Heino)
|
||||
|
||||
## [android-v2.14.4](https://github.com/laurent22/joplin/releases/tag/android-v2.14.4) (Pre-release) - 2024-01-26T10:46:28Z
|
||||
## [android-v2.14.4](https://github.com/laurent22/joplin/releases/tag/android-v2.14.4) - 2024-01-26T10:46:28Z
|
||||
|
||||
- New: Add support for showing only lines of log that contain a filter (#9728 by Henry Heino)
|
||||
- Improved: Allow setting a minimum app version on the sync target (#9778)
|
||||
@@ -677,18 +681,18 @@
|
||||
- Improved: Updated packages @js-draw/material-icons (v1.15.0), follow-redirects (v1.15.4), fs-extra (v11.2.0), js-draw (v1.15.0), react, react-native-device-info (v10.12.0), react-native-image-picker (v7.1.0), react-native-paper (v5.11.5), react-native-vector-icons (v10.0.3), sharp (v0.33.1)
|
||||
- Fixed: Fix AWS S3 sync error (#9696) (#8891 by Henry Heino)
|
||||
|
||||
## [android-v2.14.3](https://github.com/laurent22/joplin/releases/tag/android-v2.14.3) (Pre-release) - 2024-01-06T12:30:29Z
|
||||
## [android-v2.14.3](https://github.com/laurent22/joplin/releases/tag/android-v2.14.3) - 2024-01-06T12:30:29Z
|
||||
|
||||
- Improved: Fix table-of-contents links to headings with duplicate content (#9610) (#9594 by Henry Heino)
|
||||
- Improved: Improve sync by reducing how often note list is sorted (f95ee68)
|
||||
- Improved: Render mermaid diagrams in dark mode when Joplin is in dark mode (#9631) (#3201 by Henry Heino)
|
||||
- Improved: Updated packages deprecated-react-native-prop-types (v5), react-native-paper (v5.11.4)
|
||||
|
||||
## [android-v2.14.2](https://github.com/laurent22/joplin/releases/tag/android-v2.14.2) (Pre-release) - 2023-12-31T16:14:25Z
|
||||
## [android-v2.14.2](https://github.com/laurent22/joplin/releases/tag/android-v2.14.2) - 2023-12-31T16:14:25Z
|
||||
|
||||
- Improved: Updated packages react-native-get-random-values (v1.10.0)
|
||||
|
||||
## [android-v2.14.1](https://github.com/laurent22/joplin/releases/tag/android-v2.14.1) (Pre-release) - 2023-12-29T22:12:14Z
|
||||
## [android-v2.14.1](https://github.com/laurent22/joplin/releases/tag/android-v2.14.1) - 2023-12-29T22:12:14Z
|
||||
|
||||
- Improved: CodeMirror 6 markdown editor: Support highlighting more languages (#9563) (#9562 by Henry Heino)
|
||||
- Improved: Don't attach empty drawings when a user exits without saving (#9386) (#9377 by Henry Heino)
|
||||
@@ -710,22 +714,22 @@
|
||||
- Fixed: Fix tooltips don't disappear on some devices (upgrade to js-draw 1.13.2) (#9401) (#9374 by Henry Heino)
|
||||
- Fixed: Sidebar is not dismissed when creating a note (#9376)
|
||||
|
||||
## [android-v2.13.10](https://github.com/laurent22/joplin/releases/tag/android-v2.13.10) (Pre-release) - 2023-12-01T11:16:17Z
|
||||
## [android-v2.13.10](https://github.com/laurent22/joplin/releases/tag/android-v2.13.10) - 2023-12-01T11:16:17Z
|
||||
|
||||
- Improved: Drawing: Revert recent changes to input system (#9426) (#9427 by Henry Heino)
|
||||
|
||||
## [android-v2.13.9](https://github.com/laurent22/joplin/releases/tag/android-v2.13.9) (Pre-release) - 2023-11-30T17:55:54Z
|
||||
## [android-v2.13.9](https://github.com/laurent22/joplin/releases/tag/android-v2.13.9) - 2023-11-30T17:55:54Z
|
||||
|
||||
- Improved: Don't attach empty drawings when a user exits without saving (#9386) (#9377 by Henry Heino)
|
||||
- Fixed: Fix tooltips don't disappear on some devices (upgrade to js-draw 1.13.2) (#9401) (#9374 by Henry Heino)
|
||||
|
||||
## [android-v2.13.8](https://github.com/laurent22/joplin/releases/tag/android-v2.13.8) (Pre-release) - 2023-11-26T12:37:00Z
|
||||
## [android-v2.13.8](https://github.com/laurent22/joplin/releases/tag/android-v2.13.8) - 2023-11-26T12:37:00Z
|
||||
|
||||
- Fixed: Fix to-dos options toggle don't toggle a rerender (#9364) (#9361 by [@pedr](https://github.com/pedr))
|
||||
- Fixed: Fix new note/to-do buttons not visible on app startup in some cases (#9329) (#9328 by Henry Heino)
|
||||
- Fixed: Sidebar is not dismissed when creating a note (#9376)
|
||||
|
||||
## [android-v2.13.7](https://github.com/laurent22/joplin/releases/tag/android-v2.13.7) (Pre-release) - 2023-11-16T13:17:53Z
|
||||
## [android-v2.13.7](https://github.com/laurent22/joplin/releases/tag/android-v2.13.7) - 2023-11-16T13:17:53Z
|
||||
|
||||
- Improved: Add more space between settings title and description (#9270) (#9258 by Henry Heino)
|
||||
- Improved: Fade settings screen icons (#9268) (#9260 by Henry Heino)
|
||||
@@ -739,7 +743,7 @@
|
||||
- Fixed: Fix settings save confirmation not shown when navigating to encryption/profile/log screens (#9313) (#9312 by Henry Heino)
|
||||
- Fixed: Restore scroll position when returning to the note viewer from the editor or camera (#9324) (#9321 by Henry Heino)
|
||||
|
||||
## [android-v2.13.6](https://github.com/laurent22/joplin/releases/tag/android-v2.13.6) (Pre-release) - 2023-11-09T19:45:21Z
|
||||
## [android-v2.13.6](https://github.com/laurent22/joplin/releases/tag/android-v2.13.6) - 2023-11-09T19:45:21Z
|
||||
|
||||
- Improved: Add a "Retry all" button when multiple resources could not be downloaded (#9158)
|
||||
- Improved: Image editor: Allow loading from save when the image editor is reloaded in the background (#9135) (#9134 by Henry Heino)
|
||||
@@ -751,13 +755,13 @@
|
||||
- Fixed: Fix search highlighting (#9206) (#9207 by Henry Heino)
|
||||
- Fixed: Image editor resets on theme change (#9190) (#9188 by Henry Heino)
|
||||
|
||||
## [android-v2.13.5](https://github.com/laurent22/joplin/releases/tag/android-v2.13.5) (Pre-release) - 2023-10-30T22:49:19Z
|
||||
## [android-v2.13.5](https://github.com/laurent22/joplin/releases/tag/android-v2.13.5) - 2023-10-30T22:49:19Z
|
||||
|
||||
- Improved: Allow searching by note ID or using a callback URL (3667bf3)
|
||||
- Improved: Updated packages @react-native-community/datetimepicker (v7.6.0), react-native-device-info (v10.11.0), react-native-webview (v13.6.2)
|
||||
- Fixed: Beta editor: Fix image timestamps not updated after editing (#9176) (#9175 by Henry Heino)
|
||||
|
||||
## [android-v2.13.4](https://github.com/laurent22/joplin/releases/tag/android-v2.13.4) (Pre-release) - 2023-10-24T18:29:09Z
|
||||
## [android-v2.13.4](https://github.com/laurent22/joplin/releases/tag/android-v2.13.4) - 2023-10-24T18:29:09Z
|
||||
|
||||
- Improved: Allow modifying a resource metadata only when synchronising (#9114)
|
||||
- Improved: Support for plural translations (#9033)
|
||||
@@ -767,7 +771,7 @@
|
||||
- Fixed: Fixed issues related to sharing notes on read-only notebooks (1c7d22e)
|
||||
- Fixed: Improve list toggle logic (#9103) (#9066 by Henry Heino)
|
||||
|
||||
## [android-v2.13.2](https://github.com/laurent22/joplin/releases/tag/android-v2.13.2) (Pre-release) - 2023-10-07T16:42:16Z
|
||||
## [android-v2.13.2](https://github.com/laurent22/joplin/releases/tag/android-v2.13.2) - 2023-10-07T16:42:16Z
|
||||
|
||||
- New: Add share button to log screen (#8364 by Henry Heino)
|
||||
- New: Add support for drawing pictures (#7588 by Henry Heino)
|
||||
@@ -787,18 +791,18 @@
|
||||
- Fixed: Hide the keyboard when showing the attach dialog (#8911) (#8774 by Henry Heino)
|
||||
- Fixed: Prevent accessibility tools from focusing the notes list when it's invisible (#8799) (#8798 by Henry Heino)
|
||||
|
||||
## [android-v2.12.3](https://github.com/laurent22/joplin/releases/tag/android-v2.12.3) (Pre-release) - 2023-09-11T20:01:44Z
|
||||
## [android-v2.12.3](https://github.com/laurent22/joplin/releases/tag/android-v2.12.3) - 2023-09-11T20:01:44Z
|
||||
|
||||
- Improved: Add screen reader labels to search/note actions buttons (#8797) (#8796 by Henry Heino)
|
||||
- Improved: Improve accessibility of side menu (#8839 by Henry Heino)
|
||||
- Fixed: Fix older Android versions unable to set alarms (#8837) (#8789 by Henry Heino)
|
||||
- Fixed: Revert to `react-native-sidemenu-updated` for navigation drawers (#8820) (#8791 by Henry Heino)
|
||||
|
||||
## [android-v2.12.2](https://github.com/laurent22/joplin/releases/tag/android-v2.12.2) (Pre-release) - 2023-08-22T13:15:18Z
|
||||
## [android-v2.12.2](https://github.com/laurent22/joplin/releases/tag/android-v2.12.2) - 2023-08-22T13:15:18Z
|
||||
|
||||
- Improved: Only include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" in APK (4e2d366)
|
||||
|
||||
## [android-v2.12.1](https://github.com/laurent22/joplin/releases/tag/android-v2.12.1) (Pre-release) - 2023-08-19T22:32:39Z
|
||||
## [android-v2.12.1](https://github.com/laurent22/joplin/releases/tag/android-v2.12.1) - 2023-08-19T22:32:39Z
|
||||
|
||||
- New: Add JEX export (#8428 by Henry Heino)
|
||||
- New: Add support for Joplin Cloud email to note functionality (#8460 by [@pedr](https://github.com/pedr))
|
||||
@@ -824,40 +828,40 @@
|
||||
- Fixed: Unrevert #7953: Migrate to react-native-drawer-layout (#8379) (#7918 by Henry Heino)
|
||||
- Security: Prevent XSS when passing specially encoded string to a link (57b4198)
|
||||
|
||||
## [android-v2.11.32](https://github.com/laurent22/joplin/releases/tag/android-v2.11.32) (Pre-release) - 2023-07-03T11:33:54Z
|
||||
## [android-v2.11.32](https://github.com/laurent22/joplin/releases/tag/android-v2.11.32) - 2023-07-03T11:33:54Z
|
||||
|
||||
- Improved: Allow configuring voice typing model URL (2aab85f)
|
||||
|
||||
## [android-v2.11.31](https://github.com/laurent22/joplin/releases/tag/android-v2.11.31) (Pre-release) - 2023-06-25T14:26:21Z
|
||||
## [android-v2.11.31](https://github.com/laurent22/joplin/releases/tag/android-v2.11.31) - 2023-06-25T14:26:21Z
|
||||
|
||||
- Improved: Upgrade E2EE encryption method to AES-256 (#7686)
|
||||
|
||||
## [android-v2.11.30](https://github.com/laurent22/joplin/releases/tag/android-v2.11.30) (Pre-release) - 2023-06-20T15:21:15Z
|
||||
## [android-v2.11.30](https://github.com/laurent22/joplin/releases/tag/android-v2.11.30) - 2023-06-20T15:21:15Z
|
||||
|
||||
- New: Add support for Voice Typing for most languages (#8309)
|
||||
|
||||
## [android-v2.11.27](https://github.com/laurent22/joplin/releases/tag/android-v2.11.27) (Pre-release) - 2023-06-10T15:58:58Z
|
||||
## [android-v2.11.27](https://github.com/laurent22/joplin/releases/tag/android-v2.11.27) - 2023-06-10T15:58:58Z
|
||||
|
||||
- Upgraded to React Native 0.71
|
||||
- Improved: Updated packages @react-native-community/datetimepicker (v7), buildTools, domutils (v3.1.0), react-native-document-picker (v8.2.1), react-native-safe-area-context (v4.5.3), tar (v6.1.15)
|
||||
|
||||
## [android-v2.11.26](https://github.com/laurent22/joplin/releases/tag/android-v2.11.26) (Pre-release) - 2023-06-08T16:13:02Z
|
||||
## [android-v2.11.26](https://github.com/laurent22/joplin/releases/tag/android-v2.11.26) - 2023-06-08T16:13:02Z
|
||||
|
||||
- Improved: Updated packages @react-native-community/datetimepicker (v7), buildTools, domutils (v3.1.0), react-native-document-picker (v8.2.1), react-native-safe-area-context (v4.5.3), tar (v6.1.15)
|
||||
- Fixed: Allow certain HTML anchor tags (#8286)
|
||||
- Fixed: Fix alarms for latest Android versions (#8229)
|
||||
- Fixed: Fix sharing data with the app (#8285)
|
||||
|
||||
## [android-v2.11.25](https://github.com/laurent22/joplin/releases/tag/android-v2.11.25) (Pre-release) - 2023-06-03T16:40:08Z
|
||||
## [android-v2.11.25](https://github.com/laurent22/joplin/releases/tag/android-v2.11.25) - 2023-06-03T16:40:08Z
|
||||
|
||||
- Fixed: Fix Vosk logic (60b3921)
|
||||
- Fixed: Fixed error "Download interrupted" when downloading resources from Joplin Cloud/Server.
|
||||
|
||||
## [android-v2.11.24](https://github.com/laurent22/joplin/releases/tag/android-v2.11.24) (Pre-release) - 2023-06-02T15:22:04Z
|
||||
## [android-v2.11.24](https://github.com/laurent22/joplin/releases/tag/android-v2.11.24) - 2023-06-02T15:22:04Z
|
||||
|
||||
- Improved: Write to note in realtime using voice typing (7779879)
|
||||
|
||||
## [android-v2.11.23](https://github.com/laurent22/joplin/releases/tag/android-v2.11.23) (Pre-release) - 2023-06-01T17:19:16Z
|
||||
## [android-v2.11.23](https://github.com/laurent22/joplin/releases/tag/android-v2.11.23) - 2023-06-01T17:19:16Z
|
||||
|
||||
- Improved: Auto-detect language on start (e48d55c)
|
||||
- Improved: Implement parenting of notebooks (#7980) (#8193 by [@jcgurango](https://github.com/jcgurango))
|
||||
@@ -869,59 +873,59 @@
|
||||
- Security: Disable SVG tag support in editor to prevent XSS (caf6606)
|
||||
- Security: Prevent XSS by sanitizing certain HTML attributes (9e90d90)
|
||||
|
||||
## [android-v2.11.22](https://github.com/laurent22/joplin/releases/tag/android-v2.11.22) (Pre-release) - 2023-05-14T13:44:28Z
|
||||
## [android-v2.11.22](https://github.com/laurent22/joplin/releases/tag/android-v2.11.22) - 2023-05-14T13:44:28Z
|
||||
|
||||
- Fixed: Fix "Download interrupted" error (b023f58)
|
||||
|
||||
## [android-v2.11.21](https://github.com/laurent22/joplin/releases/tag/android-v2.11.21) (Pre-release) - 2023-05-14T11:05:15Z
|
||||
## [android-v2.11.21](https://github.com/laurent22/joplin/releases/tag/android-v2.11.21) - 2023-05-14T11:05:15Z
|
||||
|
||||
- Improved: Updated packages react-native-paper (v5.6.0)
|
||||
|
||||
## [android-v2.11.16](https://github.com/laurent22/joplin/releases/tag/android-v2.11.16) (Pre-release) - 2023-05-12T12:43:08Z
|
||||
## [android-v2.11.16](https://github.com/laurent22/joplin/releases/tag/android-v2.11.16) - 2023-05-12T12:43:08Z
|
||||
|
||||
- Improved: Sync as soon as the app starts, and immediately after changing a note (3eb44d2)
|
||||
|
||||
## [android-v2.11.14](https://github.com/laurent22/joplin/releases/tag/android-v2.11.14) (Pre-release) - 2023-05-10T12:24:40Z
|
||||
## [android-v2.11.14](https://github.com/laurent22/joplin/releases/tag/android-v2.11.14) - 2023-05-10T12:24:40Z
|
||||
|
||||
- Improved: Translate Welcome notes (#8154)
|
||||
|
||||
## [android-v2.11.13](https://github.com/laurent22/joplin/releases/tag/android-v2.11.13) (Pre-release) - 2023-05-08T20:28:29Z
|
||||
## [android-v2.11.13](https://github.com/laurent22/joplin/releases/tag/android-v2.11.13) - 2023-05-08T20:28:29Z
|
||||
|
||||
- Improved: Tells whether Hermes engine is enabled or not (5ecae17)
|
||||
|
||||
## [android-v2.11.10](https://github.com/laurent22/joplin/releases/tag/android-v2.11.10) (Pre-release) - 2023-05-08T10:26:14Z
|
||||
## [android-v2.11.10](https://github.com/laurent22/joplin/releases/tag/android-v2.11.10) - 2023-05-08T10:26:14Z
|
||||
|
||||
- Improved: Disable Hermes engine (e9e9986)
|
||||
- Fixed: Fix voice typing (d5eeb12)
|
||||
|
||||
## [android-v2.11.7](https://github.com/laurent22/joplin/releases/tag/android-v2.11.7) (Pre-release) - 2023-05-07T14:29:08Z
|
||||
## [android-v2.11.7](https://github.com/laurent22/joplin/releases/tag/android-v2.11.7) - 2023-05-07T14:29:08Z
|
||||
|
||||
- Fixed crash when starting voice typing.
|
||||
|
||||
## [android-v2.11.6](https://github.com/laurent22/joplin/releases/tag/android-v2.11.6) (Pre-release) - 2023-05-07T13:53:31Z
|
||||
## [android-v2.11.6](https://github.com/laurent22/joplin/releases/tag/android-v2.11.6) - 2023-05-07T13:53:31Z
|
||||
|
||||
- Disabled Hermes engine
|
||||
|
||||
## [android-v2.11.5](https://github.com/laurent22/joplin/releases/tag/android-v2.11.5) (Pre-release) - 2023-05-07T12:14:21Z
|
||||
## [android-v2.11.5](https://github.com/laurent22/joplin/releases/tag/android-v2.11.5) - 2023-05-07T12:14:21Z
|
||||
|
||||
- Improved: Improved Vosk support (beta, fr only) (#8131)
|
||||
- Improved: Updated packages react-native-share (v8.2.2), reselect (v4.1.8), sharp (v0.32.0)
|
||||
|
||||
## [android-v2.11.4](https://github.com/laurent22/joplin/releases/tag/android-v2.11.4) (Pre-release) - 2023-05-03T11:57:27Z
|
||||
## [android-v2.11.4](https://github.com/laurent22/joplin/releases/tag/android-v2.11.4) - 2023-05-03T11:57:27Z
|
||||
|
||||
- New: Add support for offline speech to text (Beta - FR only) (#8115)
|
||||
- Improved: Updated packages @react-native-community/netinfo (v9.3.9), aws, react-native-document-picker (v8.2.0), react-native-paper (v5.5.2), react-native-safe-area-context (v4.5.1), sass (v1.60.0)
|
||||
- Fixed: Fixed sync crash (#8056) (#8017 by Arun Kumar)
|
||||
- Fixed: Fixes issue where the note body is not updated after attaching a file (991c120)
|
||||
|
||||
## [android-v2.11.2](https://github.com/laurent22/joplin/releases/tag/android-v2.11.2) (Pre-release) - 2023-04-09T12:04:06Z
|
||||
## [android-v2.11.2](https://github.com/laurent22/joplin/releases/tag/android-v2.11.2) - 2023-04-09T12:04:06Z
|
||||
|
||||
- Improved: Resolve #8022: Editor syntax highlighting was broken (#8023) (#8022 by Henry Heino)
|
||||
- Improved: Updated packages @react-native-community/netinfo (v9.3.8)
|
||||
- Fixed: Removed `MasterKey` from Sync Status report (#8026) (#7940 by Arun Kumar)
|
||||
- Security: Prevent bypassing fingerprint lock on certain devices (6b72f86)
|
||||
|
||||
## [android-v2.11.1](https://github.com/laurent22/joplin/releases/tag/android-v2.11.1) (Pre-release) - 2023-04-08T08:49:19Z
|
||||
## [android-v2.11.1](https://github.com/laurent22/joplin/releases/tag/android-v2.11.1) - 2023-04-08T08:49:19Z
|
||||
|
||||
- New: Add log info for biometrics feature (efdbaeb)
|
||||
- New: Add setting to enable/disable the markdown toolbar (#7929 by Henry Heino)
|
||||
@@ -929,11 +933,11 @@
|
||||
- Fixed: Fix OneDrive sync attempting to call method on `null` variable (#7987) (#7986 by Henry Heino)
|
||||
- Updated packages @lezer/highlight (v1.1.4), fs-extra (v11.1.1), jsdom (v21.1.1), markdown-it-multimd-table (v4.2.1), nanoid (v3.3.6), node-persist (v3.1.3), nodemon (v2.0.22), react-native-document-picker (v8.1.4), react-native-image-picker (v5.3.1), react-native-paper (v5.4.1), react-native-share (v8.2.1), sass (v1.59.3), sqlite3 (v5.1.6), turndown (v7.1.2), yargs (v17.7.1)
|
||||
|
||||
## [android-v2.10.9](https://github.com/laurent22/joplin/releases/tag/android-v2.10.9) (Pre-release) - 2023-03-22T18:40:57Z
|
||||
## [android-v2.10.9](https://github.com/laurent22/joplin/releases/tag/android-v2.10.9) - 2023-03-22T18:40:57Z
|
||||
|
||||
- Improved: Mark biometrics feature as beta and ensure no call is made if it is not enabled (e44a934)
|
||||
|
||||
## [android-v2.10.8](https://github.com/laurent22/joplin/releases/tag/android-v2.10.8) (Pre-release) - 2023-02-28T18:09:21Z
|
||||
## [android-v2.10.8](https://github.com/laurent22/joplin/releases/tag/android-v2.10.8) - 2023-02-28T18:09:21Z
|
||||
|
||||
- Improved: Stop synchronization with unsupported WebDAV providers (#7819) (#7661 by [@julien](https://github.com/julien))
|
||||
- Fixed: Custom sort order not synchronized (#7729) (#6956 by Tao Klerks)
|
||||
@@ -944,20 +948,20 @@
|
||||
- Fixed: Hide main content while biometric is enabled and not authenticated (#7781) (#7762 by [@pedr](https://github.com/pedr))
|
||||
- Fixed: Sharing pictures to Joplin creates recurring duplications (#7807) (#7791 by [@jd1378](https://github.com/jd1378))
|
||||
|
||||
## [android-v2.10.6](https://github.com/laurent22/joplin/releases/tag/android-v2.10.6) (Pre-release) - 2023-02-10T16:22:28Z
|
||||
## [android-v2.10.6](https://github.com/laurent22/joplin/releases/tag/android-v2.10.6) - 2023-02-10T16:22:28Z
|
||||
|
||||
- Improved: Add create sub-notebook feature (#7728) (#1044 by [@carlosngo](https://github.com/carlosngo))
|
||||
- Fixed: Fix double-scroll issue in long notes (#7701) (#7700 by Henry Heino)
|
||||
- Fixed: Fix startup error (#7688) (#7687 by Henry Heino)
|
||||
- Fixed: Sharing file to Joplin does not work (#7691)
|
||||
|
||||
## [android-v2.10.5](https://github.com/laurent22/joplin/releases/tag/android-v2.10.5) (Pre-release) - 2023-01-21T14:21:23Z
|
||||
## [android-v2.10.5](https://github.com/laurent22/joplin/releases/tag/android-v2.10.5) - 2023-01-21T14:21:23Z
|
||||
|
||||
- Improved: Improve dialogue spacing in Fountain renderer (#7628) (#7627 by [@Elleo](https://github.com/Elleo))
|
||||
- Improved: Improve filesystem sync performance (#7637) (#6942 by [@jd1378](https://github.com/jd1378))
|
||||
- Fixed: Fixes non-working alarms (138bc81)
|
||||
|
||||
## [android-v2.10.4](https://github.com/laurent22/joplin/releases/tag/android-v2.10.4) (Pre-release) - 2023-01-14T17:30:34Z
|
||||
## [android-v2.10.4](https://github.com/laurent22/joplin/releases/tag/android-v2.10.4) - 2023-01-14T17:30:34Z
|
||||
|
||||
- New: Add support for multiple profiles (6bb52d5)
|
||||
- Improved: Configurable editor font size (#7596 by Henry Heino)
|
||||
@@ -968,19 +972,19 @@
|
||||
- Fixed: Fixed issue when floating keyboard is visible (#7593) (#6682 by Henry Heino)
|
||||
- Fixed: Remove gray line around text editor (#7595) (#7594 by Henry Heino)
|
||||
|
||||
## [android-v2.10.3](https://github.com/laurent22/joplin/releases/tag/android-v2.10.3) (Pre-release) - 2023-01-05T11:29:06Z
|
||||
## [android-v2.10.3](https://github.com/laurent22/joplin/releases/tag/android-v2.10.3) - 2023-01-05T11:29:06Z
|
||||
|
||||
- New: Add support for locking the app using biometrics (f10d9f7)
|
||||
- Improved: Make the new text editor the default one (f5ef318)
|
||||
- Fixed: Fixed proxy timeout setting UI (275c80a)
|
||||
- Fixed: Settings save button visible even when no settings have been changed (#7503)
|
||||
|
||||
## [android-v2.10.2](https://github.com/laurent22/joplin/releases/tag/android-v2.10.2) (Pre-release) - 2023-01-02T17:44:15Z
|
||||
## [android-v2.10.2](https://github.com/laurent22/joplin/releases/tag/android-v2.10.2) - 2023-01-02T17:44:15Z
|
||||
|
||||
- New: Add support for realtime search (767213c)
|
||||
- Fixed: Enable autocorrect with spellcheck (#7532) (#6175 by Henry Heino)
|
||||
|
||||
## [android-v2.10.1](https://github.com/laurent22/joplin/releases/tag/android-v2.10.1) (Pre-release) - 2022-12-29T13:55:48Z
|
||||
## [android-v2.10.1](https://github.com/laurent22/joplin/releases/tag/android-v2.10.1) - 2022-12-29T13:55:48Z
|
||||
|
||||
- Improved: Switch license to AGPL-3.0 (faf0a4e)
|
||||
- Improved: Tag search case insensitive (#7368 by [@JackGruber](https://github.com/JackGruber))
|
||||
@@ -992,16 +996,16 @@
|
||||
- Fixed: Update CodeMirror (#7262) (#7253 by Henry Heino)
|
||||
- Security: Fix XSS when a specially crafted string is passed to the renderer (762b4e8)
|
||||
|
||||
## [android-v2.9.8](https://github.com/laurent22/joplin/releases/tag/android-v2.9.8) (Pre-release) - 2022-11-01T15:45:36Z
|
||||
## [android-v2.9.8](https://github.com/laurent22/joplin/releases/tag/android-v2.9.8) - 2022-11-01T15:45:36Z
|
||||
|
||||
- Updated translations
|
||||
|
||||
## [android-v2.9.7](https://github.com/laurent22/joplin/releases/tag/android-v2.9.7) (Pre-release) - 2022-10-30T10:25:01Z
|
||||
## [android-v2.9.7](https://github.com/laurent22/joplin/releases/tag/android-v2.9.7) - 2022-10-30T10:25:01Z
|
||||
|
||||
- Fixed: Fixed notebook icons alignment (ea6b7ca)
|
||||
- Fixed: Fixed crash when attaching a file.
|
||||
|
||||
## [android-v2.9.6](https://github.com/laurent22/joplin/releases/tag/android-v2.9.6) (Pre-release) - 2022-10-23T16:23:25Z
|
||||
## [android-v2.9.6](https://github.com/laurent22/joplin/releases/tag/android-v2.9.6) - 2022-10-23T16:23:25Z
|
||||
|
||||
- New: Add monochrome icon (#6954 by Tom Bursch)
|
||||
- Fixed: Fix file system sync issues (#6943 by [@jd1378](https://github.com/jd1378))
|
||||
@@ -1009,12 +1013,12 @@
|
||||
- Fixed: Fixed notebook icon spacing (633c9ac)
|
||||
- Fixed: Support non-ASCII characters in OneDrive (#6916) (#6838 by Self Not Found)
|
||||
|
||||
## [android-v2.9.5](https://github.com/laurent22/joplin/releases/tag/android-v2.9.5) (Pre-release) - 2022-10-11T13:52:00Z
|
||||
## [android-v2.9.5](https://github.com/laurent22/joplin/releases/tag/android-v2.9.5) - 2022-10-11T13:52:00Z
|
||||
|
||||
- Improved: Disable multi-highlighting to fix context menu (9b348fd)
|
||||
- Improved: Display icon for all notebooks if at least one notebook has an icon (ec97dd8)
|
||||
|
||||
## [android-v2.9.3](https://github.com/laurent22/joplin/releases/tag/android-v2.9.3) (Pre-release) - 2022-10-07T11:12:56Z
|
||||
## [android-v2.9.3](https://github.com/laurent22/joplin/releases/tag/android-v2.9.3) - 2022-10-07T11:12:56Z
|
||||
|
||||
- Improved: Convert empty bolded regions to bold-italic regions in beta editor (#6807) (#6808 by Henry Heino)
|
||||
- Improved: Increase the attachment size limit to 200MB (#6848 by Self Not Found)
|
||||
@@ -1026,7 +1030,7 @@
|
||||
- Fixed: Fix resources sync when proxy is set (#6817) (#6688 by Self Not Found)
|
||||
- Fixed: Fixed crash when trying to move note to notebook (#6898)
|
||||
|
||||
## [android-v2.9.2](https://github.com/laurent22/joplin/releases/tag/android-v2.9.2) (Pre-release) - 2022-09-01T11:14:58Z
|
||||
## [android-v2.9.2](https://github.com/laurent22/joplin/releases/tag/android-v2.9.2) - 2022-09-01T11:14:58Z
|
||||
|
||||
- New: Add Markdown toolbar (#6753 by Henry Heino)
|
||||
- New: Add long-press tooltips (#6758 by Henry Heino)
|
||||
@@ -1038,7 +1042,7 @@
|
||||
- Fixed: Fixed Android filesystem sync (resources) (#6789) (#6779 by [@jd1378](https://github.com/jd1378))
|
||||
- Fixed: Fixed handling of normal paths in filesystem sync (#6792) (#6791 by [@jd1378](https://github.com/jd1378))
|
||||
|
||||
## [android-v2.9.1](https://github.com/laurent22/joplin/releases/tag/android-v2.9.1) (Pre-release) - 2022-08-12T17:14:49Z
|
||||
## [android-v2.9.1](https://github.com/laurent22/joplin/releases/tag/android-v2.9.1) - 2022-08-12T17:14:49Z
|
||||
|
||||
- New: Add alt text/roles to some buttons to improve accessibility (#6616 by Henry Heino)
|
||||
- New: Add keyboard-activatable markdown commands (e.g. bold, italicize) on text editor (#6707 by Henry Heino)
|
||||
@@ -1051,7 +1055,7 @@
|
||||
- Fixed: Note links with HTML notation did not work (#6515)
|
||||
- Fixed: Scroll selection into view in beta editor when window resizes (#6610) (#5949 by Henry Heino)
|
||||
|
||||
## [android-v2.8.1](https://github.com/laurent22/joplin/releases/tag/android-v2.8.1) (Pre-release) - 2022-05-18T13:35:01Z
|
||||
## [android-v2.8.1](https://github.com/laurent22/joplin/releases/tag/android-v2.8.1) - 2022-05-18T13:35:01Z
|
||||
|
||||
- Improved: Allow filtering tags in tag dialog (#6221 by [@shinglyu](https://github.com/shinglyu))
|
||||
- Improved: Automatically start sync after setting the sync parameters (ff066ba)
|
||||
@@ -1069,7 +1073,7 @@
|
||||
- Fixed: Support inserting attachments from Beta Editor (#6325) (#6324 by Henry Heino)
|
||||
- Fixed: The camera button remains clickable after taking a photo bug (#6222 by [@shinglyu](https://github.com/shinglyu))
|
||||
|
||||
## [android-v2.7.2](https://github.com/laurent22/joplin/releases/tag/android-v2.7.2) (Pre-release) - 2022-02-12T12:51:29Z
|
||||
## [android-v2.7.2](https://github.com/laurent22/joplin/releases/tag/android-v2.7.2) - 2022-02-12T12:51:29Z
|
||||
|
||||
- New: Add additional time format HH.mm (#6086 by [@vincentjocodes](https://github.com/vincentjocodes))
|
||||
- Improved: Do not duplicate resources when duplicating a note (721d008)
|
||||
@@ -1087,11 +1091,11 @@
|
||||
- Improved: Update Mermaid: 8.12.1 -> 8.13.5 (#5831 by Helmut K. C. Tessarek)
|
||||
- Fixed: Links in flowchart Mermaid diagrams (#5830) (#5801 by Helmut K. C. Tessarek)
|
||||
|
||||
## [android-v2.6.5](https://github.com/laurent22/joplin/releases/tag/android-v2.6.5) (Pre-release) - 2021-12-13T09:41:18Z
|
||||
## [android-v2.6.5](https://github.com/laurent22/joplin/releases/tag/android-v2.6.5) - 2021-12-13T09:41:18Z
|
||||
|
||||
- Fixed: Fixed "Invalid lock client type" error when migrating sync target (e0e93c4)
|
||||
|
||||
## [android-v2.6.4](https://github.com/laurent22/joplin/releases/tag/android-v2.6.4) (Pre-release) - 2021-12-01T11:38:49Z
|
||||
## [android-v2.6.4](https://github.com/laurent22/joplin/releases/tag/android-v2.6.4) - 2021-12-01T11:38:49Z
|
||||
|
||||
- Improved: Also duplicate resources when duplicating a note (c0a8c33)
|
||||
- Improved: Improved S3 sync error handling and reliability, and upgraded S3 SDK (#5312 by Lee Matos)
|
||||
@@ -1100,7 +1104,7 @@
|
||||
- Fixed: Fixed opening attachments (6950c40)
|
||||
- Fixed: Handle duplicate attachments when the parent notebook is shared (#5796)
|
||||
|
||||
## [android-v2.6.3](https://github.com/laurent22/joplin/releases/tag/android-v2.6.3) (Pre-release) - 2021-11-21T16:59:46Z
|
||||
## [android-v2.6.3](https://github.com/laurent22/joplin/releases/tag/android-v2.6.3) - 2021-11-21T16:59:46Z
|
||||
|
||||
- New: Add date format YYYY/MM/DD (#5759 by Helmut K. C. Tessarek)
|
||||
- New: Add support for faster built-in sync locks (#5662)
|
||||
@@ -1113,18 +1117,18 @@
|
||||
- Fixed: Fixed issue with parts of HTML notes not being displayed in some cases (#5687)
|
||||
- Fixed: Sharing multiple notebooks via Joplin Server with the same user results in an error (#5721)
|
||||
|
||||
## [android-v2.6.1](https://github.com/laurent22/joplin/releases/tag/android-v2.6.1) (Pre-release) - 2021-11-02T20:49:53Z
|
||||
## [android-v2.6.1](https://github.com/laurent22/joplin/releases/tag/android-v2.6.1) - 2021-11-02T20:49:53Z
|
||||
|
||||
- Improved: Upgraded React Native from 0.64 to 0.66 (66e79cc)
|
||||
- Fixed: Fixed potential infinite loop when Joplin Server session is invalid (c5569ef)
|
||||
|
||||
## [android-v2.5.5](https://github.com/laurent22/joplin/releases/tag/android-v2.5.5) (Pre-release) - 2021-10-31T11:03:16Z
|
||||
## [android-v2.5.5](https://github.com/laurent22/joplin/releases/tag/android-v2.5.5) - 2021-10-31T11:03:16Z
|
||||
|
||||
- New: Add padding around beta text editor (365e152)
|
||||
- Improved: Capitalise first word of sentence in beta editor (4128be9)
|
||||
- Fixed: Do not render very large code blocks to prevent app from freezing (#5593)
|
||||
|
||||
## [android-v2.5.3](https://github.com/laurent22/joplin/releases/tag/android-v2.5.3) (Pre-release) - 2021-10-28T21:47:18Z
|
||||
## [android-v2.5.3](https://github.com/laurent22/joplin/releases/tag/android-v2.5.3) - 2021-10-28T21:47:18Z
|
||||
|
||||
- New: Add support for public-private key pairs and improved master password support (#5438)
|
||||
- New: Added mechanism to migrate default settings to new values (72db8e4)
|
||||
@@ -1139,13 +1143,13 @@
|
||||
|
||||
- Fixed: Fix default sync target (4b39d30)
|
||||
|
||||
## [android-v2.4.2](https://github.com/laurent22/joplin/releases/tag/android-v2.4.2) (Pre-release) - 2021-09-22T17:02:37Z
|
||||
## [android-v2.4.2](https://github.com/laurent22/joplin/releases/tag/android-v2.4.2) - 2021-09-22T17:02:37Z
|
||||
|
||||
- Improved: Allow disabling any master key, including default or active one (9407efd)
|
||||
- Improved: Update Mermaid 8.10.2 -> 8.12.1 and fix gitGraph crash (#5448) (#5295 by Helmut K. C. Tessarek)
|
||||
- Fixed: Misinterpreted search term after filter in quotation marks (#5445) (#5444 by [@JackGruber](https://github.com/JackGruber))
|
||||
|
||||
## [android-v2.4.1](https://github.com/laurent22/joplin/releases/tag/android-v2.4.1) (Pre-release) - 2021-08-30T13:37:34Z
|
||||
## [android-v2.4.1](https://github.com/laurent22/joplin/releases/tag/android-v2.4.1) - 2021-08-30T13:37:34Z
|
||||
|
||||
- New: Add a way to disable a master key (7faa58e)
|
||||
- New: Add support for single master password, to simplify handling of multiple encryption keys (ce89ee5)
|
||||
@@ -1155,21 +1159,21 @@
|
||||
- Improved: Show the used tags first in the tagging dialog (#5315 by [@JackGruber](https://github.com/JackGruber))
|
||||
- Fixed: Fixed crash when a required master key does not exist (#5391)
|
||||
|
||||
## [android-v2.3.4](https://github.com/laurent22/joplin/releases/tag/android-v2.3.4) (Pre-release) - 2021-08-15T13:27:57Z
|
||||
## [android-v2.3.4](https://github.com/laurent22/joplin/releases/tag/android-v2.3.4) - 2021-08-15T13:27:57Z
|
||||
|
||||
- Fixed: Bump highlight.js to v11.2 (#5278) (#5245 by Roman Musin)
|
||||
|
||||
## [android-v2.3.3](https://github.com/laurent22/joplin/releases/tag/android-v2.3.3) (Pre-release) - 2021-08-12T20:46:15Z
|
||||
## [android-v2.3.3](https://github.com/laurent22/joplin/releases/tag/android-v2.3.3) - 2021-08-12T20:46:15Z
|
||||
|
||||
- Improved: Improved E2EE usability by making its state a property of the sync target (#5276)
|
||||
|
||||
## [android-v2.2.5](https://github.com/laurent22/joplin/releases/tag/android-v2.2.5) (Pre-release) - 2021-08-11T10:54:38Z
|
||||
## [android-v2.2.5](https://github.com/laurent22/joplin/releases/tag/android-v2.2.5) - 2021-08-11T10:54:38Z
|
||||
|
||||
- Revert "Plugins: Add ability to make dialogs fit the application window (#5219)" as it breaks several plugin webviews.
|
||||
- Revert "Resolves #4810, Resolves #4610: Fix AWS S3 sync error and upgrade framework to v3 (#5212)" due to incompatibility with some AWS providers.
|
||||
- Improved: Upgraded React Native to v0.64 (afb7e1a)
|
||||
|
||||
## [android-v2.2.3](https://github.com/laurent22/joplin/releases/tag/android-v2.2.3) (Pre-release) - 2021-08-09T18:48:29Z
|
||||
## [android-v2.2.3](https://github.com/laurent22/joplin/releases/tag/android-v2.2.3) - 2021-08-09T18:48:29Z
|
||||
|
||||
- Improved: Ensure that timestamps are not changed when sharing or unsharing a note (cafaa9c)
|
||||
- Improved: Fix AWS S3 sync error and upgrade framework to v3 (#5212) (#4810 by Lee Matos)
|
||||
@@ -1177,7 +1181,7 @@
|
||||
- Improved: Make sync icon spin in the right direction (#5275) (#4588 by Lee Matos)
|
||||
- Fixed: Fixed issue with orphaned resource being created in case of a resource conflict (#5223)
|
||||
|
||||
## [android-v2.2.1](https://github.com/laurent22/joplin/releases/tag/android-v2.2.1) (Pre-release) - 2021-07-13T17:37:38Z
|
||||
## [android-v2.2.1](https://github.com/laurent22/joplin/releases/tag/android-v2.2.1) - 2021-07-13T17:37:38Z
|
||||
|
||||
- New: Added improved editor (beta)
|
||||
- Improved: Disable backup to Google Drive (#5114 by Roman Musin)
|
||||
@@ -1197,12 +1201,12 @@
|
||||
- Fixed: Fixed search when the index contains non-existing notes (5ecac21)
|
||||
- Fixed: Fixed version number on config screen (65e9268)
|
||||
|
||||
## [android-v2.1.2](https://github.com/laurent22/joplin/releases/tag/android-v2.1.2) (Pre-release) - 2021-06-20T18:36:23Z
|
||||
## [android-v2.1.2](https://github.com/laurent22/joplin/releases/tag/android-v2.1.2) - 2021-06-20T18:36:23Z
|
||||
|
||||
- Fixed: Fixed error that could prevent a revision from being created, and that would prevent the revision service from processing the rest of the notes (#5051)
|
||||
- Fixed: Fixed issue when trying to sync an item associated with a share that no longer exists (5bb68ba)
|
||||
|
||||
## [android-v2.1.1](https://github.com/laurent22/joplin/releases/tag/android-v2.1.1) (Pre-release) - 2021-06-19T16:42:57Z
|
||||
## [android-v2.1.1](https://github.com/laurent22/joplin/releases/tag/android-v2.1.1) - 2021-06-19T16:42:57Z
|
||||
|
||||
- New: Add version number to log (525ab01)
|
||||
- New: Added feature flags to disable Joplin Server sync optimisations by default, so that it still work with server 2.0 (326fef4)
|
||||
@@ -1217,7 +1221,7 @@
|
||||
|
||||
- Improved: Prevent sync process from being stuck when the download state of a resource is invalid (5c6fd93)
|
||||
|
||||
## [android-v2.0.3](https://github.com/laurent22/joplin/releases/tag/android-v2.0.3) (Pre-release) - 2021-06-16T09:48:58Z
|
||||
## [android-v2.0.3](https://github.com/laurent22/joplin/releases/tag/android-v2.0.3) - 2021-06-16T09:48:58Z
|
||||
|
||||
- Improved: Verbose mode for synchronizer (4bbb3d1)
|
||||
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# Joplin Desktop Changelog
|
||||
|
||||
## [v3.6.2](https://github.com/laurent22/joplin/releases/tag/v3.6.2) (Pre-release) - 2026-01-18T20:10:43Z
|
||||
|
||||
- Improved: Accessibility: Include accessibility information in exported PDFs ([#14111](https://github.com/laurent22/joplin/issues/14111)) ([#14086](https://github.com/laurent22/joplin/issues/14086) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Improved: Editor: Inline rendering: Render inline HTML (colorized text, superscript, subscript, strikethrough) ([#14133](https://github.com/laurent22/joplin/issues/14133) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: .onepkg import: Fix Unicode issues, support Linux and MacOS ([#14094](https://github.com/laurent22/joplin/issues/14094)) ([#14084](https://github.com/laurent22/joplin/issues/14084) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Application crashes when profile database has been analyzed ([#14144](https://github.com/laurent22/joplin/issues/14144))
|
||||
- Fixed: Built-in plugins: Upgrade Freehand Drawing to v4.3.0 ([#14123](https://github.com/laurent22/joplin/issues/14123)) ([#14092](https://github.com/laurent22/joplin/issues/14092) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [v3.6.1](https://github.com/laurent22/joplin/releases/tag/v3.6.1) (Pre-release) - 2026-01-17T14:17:29Z
|
||||
|
||||
- New: Add more error information when the profile is corrupted ([f075b56](https://github.com/laurent22/joplin/commit/f075b56))
|
||||
- New: Add support for external embeds, eg. YouTube videos ([#14012](https://github.com/laurent22/joplin/issues/14012))
|
||||
- Improved: Improve Fountain notes exported as PDF ([#14120](https://github.com/laurent22/joplin/issues/14120)) ([#14106](https://github.com/laurent22/joplin/issues/14106))
|
||||
- Improved: Updated packages @rollup/plugin-commonjs (v28.0.8), @rollup/plugin-node-resolve (v16.0.3), style-to-js (v1.1.18)
|
||||
- Fixed: Experimental auto-updater: Fix application crash on update failure ([#14083](https://github.com/laurent22/joplin/issues/14083)) ([#13430](https://github.com/laurent22/joplin/issues/13430) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Rich Text Editor: Fix cut, copy, paste, and select all menu items ([#14125](https://github.com/laurent22/joplin/issues/14125) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [v3.5.12](https://github.com/laurent22/joplin/releases/tag/v3.5.12) - 2026-01-17T14:20:33Z
|
||||
|
||||
- Fixed: Experimental auto-updater: Fix application crash on update failure ([#14083](https://github.com/laurent22/joplin/issues/14083)) ([#13430](https://github.com/laurent22/joplin/issues/13430) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Rich Text Editor: Fix cut, copy, paste, and select all menu items ([#14125](https://github.com/laurent22/joplin/issues/14125) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [v3.5.11](https://github.com/laurent22/joplin/releases/tag/v3.5.11) - 2026-01-12T15:17:25Z
|
||||
|
||||
- Improved: OneNote importer: Simplify error report ([#14074](https://github.com/laurent22/joplin/issues/14074) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
@@ -8,7 +8,7 @@ Joplin Server Business is a synchronisation server that you can install on your
|
||||
|
||||
Your teams can collaborate on notebooks and share information. They can also publish notes to the internet or within your own intranet. All that secured by Joplin end-to-end encryption.
|
||||
|
||||
Interested? [Contact us for a quote](mailto:jsb-inquiry@joplin.cloud?subject=Joplin%20Server%20Business%20inquiry)
|
||||
Interested? [Contact us for a quote](https://tally.so/r/D4BlOE)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ Keep all your resources in one place. Save and share images, PDFs, videos, audio
|
||||
|
||||
To find out more about Joplin Server Business and how it can be integrated to your organisation, feel free to contact us. Our experts can prepare a demo for you. We can provide a quote to accommodate your company’s needs.
|
||||
|
||||
[Contact us for a quote!](mailto:jsb-inquiry@joplin.cloud?subject=Joplin%20Server%20Business%20inquiry)
|
||||
[Contact us for a quote!](https://tally.so/r/D4BlOE)
|
||||
|
||||
## Difference with Joplin Server
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Your download of <span class="downloaded-filename">Joplin</span> is in progress.
|
||||
|
||||
Access your notes on Windows, macOS or Linux.
|
||||
|
||||
<!-- DESKTOP-DOWNLOAD-LINKS --><a class="download-link-windows" href='https://objects.joplinusercontent.com/v3.5.11/Joplin-Setup-3.5.11.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> <a class="download-link-macOs" href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11.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> <a class="download-link-macOsM1" href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11-arm64.DMG?source=JoplinWebsite&type=New'><img alt='Get it on macOS M1 (Silicon)' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOSM1.png'/></a> <a class="download-link-linux" href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11.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><!-- DESKTOP-DOWNLOAD-LINKS -->
|
||||
<!-- DESKTOP-DOWNLOAD-LINKS --><a class="download-link-windows" href='https://objects.joplinusercontent.com/v3.5.12/Joplin-Setup-3.5.12.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> <a class="download-link-macOs" href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12.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> <a class="download-link-macOsM1" href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12-arm64.DMG?source=JoplinWebsite&type=New'><img alt='Get it on macOS M1 (Silicon)' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOSM1.png'/></a> <a class="download-link-linux" href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12.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><!-- DESKTOP-DOWNLOAD-LINKS -->
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ 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/v3.5.11/Joplin-Setup-3.5.11.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/v3.5.11/Joplin-3.5.11.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>
|
||||
macOS M1 (Apple Silicon) | <a href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11-arm64.DMG?source=JoplinWebsite&type=New'><img alt='Get it on macOS M1 (Silicon)' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOSM1.png'/></a>
|
||||
Linux | <a href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11.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://objects.joplinusercontent.com/v3.5.12/Joplin-Setup-3.5.12.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/v3.5.12/Joplin-3.5.12.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>
|
||||
macOS M1 (Apple Silicon) | <a href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12-arm64.DMG?source=JoplinWebsite&type=New'><img alt='Get it on macOS M1 (Silicon)' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOSM1.png'/></a>
|
||||
Linux | <a href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12.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>
|
||||
|
||||
**On Windows**, you may also use the <a href='https://objects.joplinusercontent.com/v3.5.11/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://objects.joplinusercontent.com/v3.5.12/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 Linux**, the recommended way is to use the following installation script as it will handle the desktop icon too:
|
||||
|
||||
|
||||
256
yarn.lock
256
yarn.lock
@@ -10537,11 +10537,11 @@ __metadata:
|
||||
"@react-native-community/netinfo": "npm:11.4.1"
|
||||
"@react-native-community/push-notification-ios": "npm:1.11.0"
|
||||
"@react-native-documents/picker": "npm:10.1.7"
|
||||
"@react-native-vector-icons/fontawesome5": "npm:12.3.0"
|
||||
"@react-native-vector-icons/fontawesome5": "patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch"
|
||||
"@react-native-vector-icons/get-image": "npm:12.3.0"
|
||||
"@react-native-vector-icons/ionicons": "npm:12.3.0"
|
||||
"@react-native-vector-icons/material-design-icons": "npm:12.4.0"
|
||||
"@react-native-vector-icons/material-icons": "npm:12.4.0"
|
||||
"@react-native-vector-icons/ionicons": "patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch"
|
||||
"@react-native-vector-icons/material-design-icons": "patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch"
|
||||
"@react-native-vector-icons/material-icons": "patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch"
|
||||
"@react-native/babel-preset": "npm:0.80.1"
|
||||
"@react-native/metro-config": "npm:0.79.5"
|
||||
"@react-native/typescript-config": "npm:0.80.2"
|
||||
@@ -10552,7 +10552,7 @@ __metadata:
|
||||
"@types/node": "npm:18.19.130"
|
||||
"@types/react": "npm:19.0.14"
|
||||
"@types/react-redux": "npm:7.1.33"
|
||||
"@types/serviceworker": "npm:0.0.158"
|
||||
"@types/serviceworker": "npm:0.0.160"
|
||||
"@types/tar-stream": "npm:3.1.4"
|
||||
assert-browserify: "npm:2.0.0"
|
||||
babel-jest: "npm:29.7.0"
|
||||
@@ -10587,7 +10587,7 @@ __metadata:
|
||||
react: "npm:19.0.0"
|
||||
react-dom: "npm:19.0.0"
|
||||
react-native: "npm:0.79.2"
|
||||
react-native-device-info: "npm:14.0.4"
|
||||
react-native-device-info: "npm:14.1.1"
|
||||
react-native-dropdownalert: "npm:5.2.0"
|
||||
react-native-exit-app: "npm:2.0.0"
|
||||
react-native-file-viewer: "npm:2.1.5"
|
||||
@@ -10641,9 +10641,9 @@ __metadata:
|
||||
resolution: "@joplin/default-plugins@workspace:packages/default-plugins"
|
||||
dependencies:
|
||||
"@joplin/utils": "npm:~3.6"
|
||||
"@types/yargs": "npm:17.0.33"
|
||||
"@types/yargs": "npm:17.0.34"
|
||||
fs-extra: "npm:11.3.2"
|
||||
joplin-plugin-freehand-drawing: "npm:4.2.0"
|
||||
joplin-plugin-freehand-drawing: "npm:4.3.0"
|
||||
ts-node: "npm:10.9.2"
|
||||
typescript: "npm:5.8.3"
|
||||
yargs: "npm:17.7.2"
|
||||
@@ -11032,10 +11032,10 @@ __metadata:
|
||||
"@types/mustache": "npm:4.2.6"
|
||||
"@types/node": "npm:18.19.130"
|
||||
"@types/node-os-utils": "npm:1.3.4"
|
||||
"@types/nodemailer": "npm:6.4.20"
|
||||
"@types/nodemailer": "npm:6.4.21"
|
||||
"@types/qrcode": "npm:1.5.6"
|
||||
"@types/uuid": "npm:10.0.0"
|
||||
"@types/yargs": "npm:17.0.33"
|
||||
"@types/yargs": "npm:17.0.34"
|
||||
"@types/zxcvbn": "npm:4.4.5"
|
||||
bcryptjs: "npm:2.4.3"
|
||||
bulma: "npm:1.0.4"
|
||||
@@ -11065,10 +11065,10 @@ __metadata:
|
||||
prettycron: "npm:0.10.0"
|
||||
qrcode: "npm:1.5.4"
|
||||
query-string: "npm:7.1.3"
|
||||
rate-limiter-flexible: "npm:7.3.2"
|
||||
rate-limiter-flexible: "npm:7.4.0"
|
||||
raw-body: "npm:3.0.1"
|
||||
samlify: "npm:2.10.1"
|
||||
short-uuid: "npm:4.2.0"
|
||||
short-uuid: "npm:5.2.0"
|
||||
source-map-support: "npm:0.5.21"
|
||||
sqlite3: "npm:5.1.6"
|
||||
stripe: "npm:13.9.0"
|
||||
@@ -11096,7 +11096,7 @@ __metadata:
|
||||
"@types/mustache": "npm:4.2.6"
|
||||
"@types/node": "npm:18.19.130"
|
||||
"@types/node-fetch": "npm:2.6.13"
|
||||
"@types/yargs": "npm:17.0.33"
|
||||
"@types/yargs": "npm:17.0.34"
|
||||
compare-versions: "npm:6.1.1"
|
||||
dayjs: "npm:1.11.18"
|
||||
execa: "npm:4.1.0"
|
||||
@@ -11142,7 +11142,7 @@ __metadata:
|
||||
"@types/koa": "npm:2.15.0"
|
||||
"@types/sharp": "npm:0.32.0"
|
||||
"@types/uuid": "npm:10.0.0"
|
||||
dotenv: "npm:16.6.1"
|
||||
dotenv: "npm:17.2.3"
|
||||
file-type: "npm:16.5.4"
|
||||
fs-extra: "npm:11.3.2"
|
||||
gulp: "npm:4.0.2"
|
||||
@@ -11165,7 +11165,7 @@ __metadata:
|
||||
browserify: "npm:14.5.0"
|
||||
rollup: "npm:0.50.1"
|
||||
standard: "npm:17.1.2"
|
||||
turndown: "npm:7.2.1"
|
||||
turndown: "npm:7.2.2"
|
||||
turndown-attendant: "npm:0.0.3"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -11175,7 +11175,7 @@ __metadata:
|
||||
resolution: "@joplin/turndown@workspace:packages/turndown"
|
||||
dependencies:
|
||||
"@adobe/css-tools": "npm:4.4.4"
|
||||
"@rollup/plugin-commonjs": "npm:28.0.8"
|
||||
"@rollup/plugin-commonjs": "npm:28.0.9"
|
||||
"@rollup/plugin-node-resolve": "npm:16.0.3"
|
||||
"@rollup/plugin-replace": "npm:6.0.2"
|
||||
browserify: "npm:14.5.0"
|
||||
@@ -13688,6 +13688,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-vector-icons/fontawesome5@patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch":
|
||||
version: 12.3.0
|
||||
resolution: "@react-native-vector-icons/fontawesome5@patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch::version=12.3.0&hash=8ed269"
|
||||
dependencies:
|
||||
"@react-native-vector-icons/common": "npm:^12.4.0"
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 10/1bfe068fdf6b9edf4608d02acf2c329fe186bbee70e7949a781f56024f386d192ac00967463afc099bc43cca162e3711a884cf7dad01e40a50cea0b612f72f5e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-vector-icons/get-image@npm:12.3.0":
|
||||
version: 12.3.0
|
||||
resolution: "@react-native-vector-icons/get-image@npm:12.3.0"
|
||||
@@ -13710,6 +13722,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-vector-icons/ionicons@patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch":
|
||||
version: 12.3.0
|
||||
resolution: "@react-native-vector-icons/ionicons@patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch::version=12.3.0&hash=e0b183"
|
||||
dependencies:
|
||||
"@react-native-vector-icons/common": "npm:^12.3.0"
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 10/f94852e0f4973e2e695e0413d69349ae1145b46253f73963f253fc83a6fdf43e39329dee1f6535b9675170e7e4e1def679ea9c2550e70d67a669434fe9a0f08f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-vector-icons/material-design-icons@npm:12.4.0":
|
||||
version: 12.4.0
|
||||
resolution: "@react-native-vector-icons/material-design-icons@npm:12.4.0"
|
||||
@@ -13722,6 +13746,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-vector-icons/material-design-icons@patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch":
|
||||
version: 12.4.0
|
||||
resolution: "@react-native-vector-icons/material-design-icons@patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch::version=12.4.0&hash=23abfd"
|
||||
dependencies:
|
||||
"@react-native-vector-icons/common": "npm:^12.4.0"
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 10/75335f9e595ca314d93ef8760cca760fe64c36673841237b119e91ddd76b16bb194060471c79c1007d0920d2b703b1c19abd24bafbee09200f52cbf2357907c5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-vector-icons/material-icons@npm:12.4.0":
|
||||
version: 12.4.0
|
||||
resolution: "@react-native-vector-icons/material-icons@npm:12.4.0"
|
||||
@@ -13734,6 +13770,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native-vector-icons/material-icons@patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch":
|
||||
version: 12.4.0
|
||||
resolution: "@react-native-vector-icons/material-icons@patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch::version=12.4.0&hash=81fbb4"
|
||||
dependencies:
|
||||
"@react-native-vector-icons/common": "npm:^12.4.0"
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 10/64ff8df53e5a339635278b0fc6cce6a16620d404cad1d97dfb8a5e25542654bbd30fc20ece1cfdce7cd9382a1d999b938c1554122743ee0f2193d5bbcb08c151
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-native/assets-registry@npm:0.79.2":
|
||||
version: 0.79.2
|
||||
resolution: "@react-native/assets-registry@npm:0.79.2"
|
||||
@@ -14238,9 +14286,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/plugin-commonjs@npm:28.0.8":
|
||||
version: 28.0.8
|
||||
resolution: "@rollup/plugin-commonjs@npm:28.0.8"
|
||||
"@rollup/plugin-commonjs@npm:28.0.9":
|
||||
version: 28.0.9
|
||||
resolution: "@rollup/plugin-commonjs@npm:28.0.9"
|
||||
dependencies:
|
||||
"@rollup/pluginutils": "npm:^5.0.1"
|
||||
commondir: "npm:^1.0.1"
|
||||
@@ -14254,7 +14302,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
checksum: 10/0533210ed86523ff2dfb952bcb13ae081a226d21c8246bf91f7e4553665c7e639f3df9bc60fadf5daae5d81de5b66101a82dc8dfc6a9f24c8ea882b40b9d8b84
|
||||
checksum: 10/68b040339ac4476bc4e75444424e85f9d22726b23e54148b6e22b80c0a06d58b4cd8ccf8c1ccc8e768076b19c9c8474f53e58b11370ecaea2a4de748101bf87a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -16835,13 +16883,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/nodemailer@npm:6.4.20":
|
||||
version: 6.4.20
|
||||
resolution: "@types/nodemailer@npm:6.4.20"
|
||||
"@types/nodemailer@npm:6.4.21":
|
||||
version: 6.4.21
|
||||
resolution: "@types/nodemailer@npm:6.4.21"
|
||||
dependencies:
|
||||
"@aws-sdk/client-ses": "npm:^3.731.1"
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10/050b6aa95c97a1bf645f0735c5da20a5b4aa30df1fc8bcc8a60c039b5f45f450689e5f51821d144d038148f3dae8427bb7d91f503c6390960a1390e562147a4f
|
||||
checksum: 10/3388d11defbef0b1b471720f88ff2fd206ab5879bc7089d90d3467198f869299dc374f01cf07054025c7e87b186a7297b1040d318d65e9ec19a130732aa1448d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -17156,10 +17204,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/serviceworker@npm:0.0.158":
|
||||
version: 0.0.158
|
||||
resolution: "@types/serviceworker@npm:0.0.158"
|
||||
checksum: 10/ec31c5f07f24aea8ae50a54c98cef70e60b16fb2463ca4a46a8062b2d7c4113adee5b01dd6f765fec885e75818123908bde7e4b918a61cb9996b24a5287e40f3
|
||||
"@types/serviceworker@npm:0.0.160":
|
||||
version: 0.0.160
|
||||
resolution: "@types/serviceworker@npm:0.0.160"
|
||||
checksum: 10/713d7614e8810dd7ba42fa37b2a16ab4c2086f31059c2068b90b6893b8126ce50fa4b31cdd11310d6f65e97c2de588e5de0e26d3d243ccab98deba4efbdabad7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -17291,12 +17339,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/yargs@npm:17.0.33":
|
||||
version: 17.0.33
|
||||
resolution: "@types/yargs@npm:17.0.33"
|
||||
"@types/yargs@npm:17.0.34":
|
||||
version: 17.0.34
|
||||
resolution: "@types/yargs@npm:17.0.34"
|
||||
dependencies:
|
||||
"@types/yargs-parser": "npm:*"
|
||||
checksum: 10/16f6681bf4d99fb671bf56029141ed01db2862e3db9df7fc92d8bea494359ac96a1b4b1c35a836d1e95e665fb18ad753ab2015fc0db663454e8fd4e5d5e2ef91
|
||||
checksum: 10/8e7907479e649e9115dcca94cb059dfe2322992ac5d29120f759564c078abfc13673a31f7ad86a3a5c9de7f241a4e3d70042ba38b794fd1601e44f9a1bc5cefd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -22038,13 +22086,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^5.6.0":
|
||||
version: 5.6.2
|
||||
resolution: "chalk@npm:5.6.2"
|
||||
checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"change-case@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "change-case@npm:4.1.2"
|
||||
@@ -22946,7 +22987,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^14.0.0":
|
||||
"commander@npm:^14.0.1":
|
||||
version: 14.0.2
|
||||
resolution: "commander@npm:14.0.2"
|
||||
checksum: 10/2d202db5e5f9bb770112a3c1579b893d17ac6f6d932183077308bdd96d0f87f0bbe6a68b5b9ed2cf3b2514be6bb7de637480703c0e2db9741ee1b383237deb26
|
||||
@@ -25068,7 +25109,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:^4.4.1":
|
||||
"debug@npm:^4.3.6":
|
||||
version: 4.4.3
|
||||
resolution: "debug@npm:4.4.3"
|
||||
dependencies:
|
||||
@@ -26190,10 +26231,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dotenv@npm:16.6.1":
|
||||
version: 16.6.1
|
||||
resolution: "dotenv@npm:16.6.1"
|
||||
checksum: 10/1d1897144344447ffe62aa1a6d664f4cd2e0784e0aff787eeeec1940ded32f8e4b5b506d665134fc87157baa086fce07ec6383970a2b6d2e7985beaed6a4cc14
|
||||
"dotenv@npm:17.2.3":
|
||||
version: 17.2.3
|
||||
resolution: "dotenv@npm:17.2.3"
|
||||
checksum: 10/f8b78626ebfff6e44420f634773375c9651808b3e1a33df6d4cc19120968eea53e100f59f04ec35f2a20b2beb334b6aba4f24040b2f8ad61773f158ac042a636
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -32394,7 +32435,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ignore@npm:^5.3.1":
|
||||
"ignore@npm:^5.3.1, ignore@npm:^5.3.2":
|
||||
version: 5.3.2
|
||||
resolution: "ignore@npm:5.3.2"
|
||||
checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98
|
||||
@@ -35004,13 +35045,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"joplin-plugin-freehand-drawing@npm:4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "joplin-plugin-freehand-drawing@npm:4.2.0"
|
||||
"joplin-plugin-freehand-drawing@npm:4.3.0":
|
||||
version: 4.3.0
|
||||
resolution: "joplin-plugin-freehand-drawing@npm:4.3.0"
|
||||
dependencies:
|
||||
"@js-draw/material-icons": "npm:1.33.0"
|
||||
js-draw: "npm:1.33.0"
|
||||
checksum: 10/457c23b7fbd6f1e3a48568395c1e456d570a3c68e25b40fba97973fd848de1b513be2f1ece6eb9babae862e19ba140aca871eee0e3a8a9cb5a033e0c0fd78934
|
||||
checksum: 10/b9ee96e149637f4bd52e894f75ba72e5c64b2512002df3a95ecf23c0b6593fdb260d61c0a30cab5450da142520b1aec63c709b11bf3a2491caa3049db0174fdf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -35475,7 +35516,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsonc-parser@npm:^3.2.1":
|
||||
"jsonc-parser@npm:^3.3.1":
|
||||
version: 3.3.1
|
||||
resolution: "jsonc-parser@npm:3.3.1"
|
||||
checksum: 10/9b0dc391f20b47378f843ef1e877e73ec652a5bdc3c5fa1f36af0f119a55091d147a86c1ee86a232296f55c929bba174538c2bf0312610e0817a22de131cc3f4
|
||||
@@ -36280,13 +36321,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lilconfig@npm:^3.1.3":
|
||||
version: 3.1.3
|
||||
resolution: "lilconfig@npm:3.1.3"
|
||||
checksum: 10/b932ce1af94985f0efbe8896e57b1f814a48c8dbd7fc0ef8469785c6303ed29d0090af3ccad7e36b626bfca3a4dc56cc262697e9a8dd867623cf09a39d54e4c3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lines-and-columns@npm:^1.1.6":
|
||||
version: 1.2.4
|
||||
resolution: "lines-and-columns@npm:1.2.4"
|
||||
@@ -36303,27 +36337,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lint-staged@npm:16.1.6":
|
||||
version: 16.1.6
|
||||
resolution: "lint-staged@npm:16.1.6"
|
||||
"lint-staged@npm:16.2.6":
|
||||
version: 16.2.6
|
||||
resolution: "lint-staged@npm:16.2.6"
|
||||
dependencies:
|
||||
chalk: "npm:^5.6.0"
|
||||
commander: "npm:^14.0.0"
|
||||
debug: "npm:^4.4.1"
|
||||
lilconfig: "npm:^3.1.3"
|
||||
listr2: "npm:^9.0.3"
|
||||
commander: "npm:^14.0.1"
|
||||
listr2: "npm:^9.0.5"
|
||||
micromatch: "npm:^4.0.8"
|
||||
nano-spawn: "npm:^1.0.2"
|
||||
nano-spawn: "npm:^2.0.0"
|
||||
pidtree: "npm:^0.6.0"
|
||||
string-argv: "npm:^0.3.2"
|
||||
yaml: "npm:^2.8.1"
|
||||
bin:
|
||||
lint-staged: bin/lint-staged.js
|
||||
checksum: 10/922b4392ae5d3d56130e4eba706c2fa6151d5da5e21f57ab601b1d6ce9cc635ceb5e4c3dc00e7da83ba8f0cb244b82604469c7ea1470b1e6b6ea0fc12454aa08
|
||||
checksum: 10/c419f1347166ddd06746d4a3e4ce6441f3d0e82dcdcbc3d3615ddf7b82b36c603df2e43dbb1edad5ed00ae29857479e1f880bcca271f8bf4b0db7d0f77861d21
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"listr2@npm:^9.0.3":
|
||||
"listr2@npm:^9.0.5":
|
||||
version: 9.0.5
|
||||
resolution: "listr2@npm:9.0.5"
|
||||
dependencies:
|
||||
@@ -39088,10 +39119,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nano-spawn@npm:^1.0.2":
|
||||
version: 1.0.3
|
||||
resolution: "nano-spawn@npm:1.0.3"
|
||||
checksum: 10/72c56e68ae733c81c459a338fd51e2aa3be06b1cca746c2abe83df7acfac7eee008b01833f5a8781f4ac9fc1eafd23036a44755257a669dfcc2ff2453850822a
|
||||
"nano-spawn@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "nano-spawn@npm:2.0.0"
|
||||
checksum: 10/117d35d7bd85b146908de5d3d1177d2b2ee3174e5d884d6bc9555583bf6e50a265f4038b5c134b7cdd768a10d53598ccde5c00d6f55e25e7eed31b86b8d29646
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -39899,30 +39930,30 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"npm-package-json-lint@npm:8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "npm-package-json-lint@npm:8.0.0"
|
||||
"npm-package-json-lint@npm:9.0.0":
|
||||
version: 9.0.0
|
||||
resolution: "npm-package-json-lint@npm:9.0.0"
|
||||
dependencies:
|
||||
ajv: "npm:^6.12.6"
|
||||
ajv-errors: "npm:^1.0.1"
|
||||
chalk: "npm:^4.1.2"
|
||||
cosmiconfig: "npm:^8.3.6"
|
||||
debug: "npm:^4.3.4"
|
||||
debug: "npm:^4.3.6"
|
||||
globby: "npm:^11.1.0"
|
||||
ignore: "npm:^5.3.1"
|
||||
ignore: "npm:^5.3.2"
|
||||
is-plain-obj: "npm:^3.0.0"
|
||||
jsonc-parser: "npm:^3.2.1"
|
||||
jsonc-parser: "npm:^3.3.1"
|
||||
log-symbols: "npm:^4.1.0"
|
||||
meow: "npm:^9.0.0"
|
||||
plur: "npm:^4.0.0"
|
||||
semver: "npm:^7.6.2"
|
||||
semver: "npm:^7.6.3"
|
||||
slash: "npm:^3.0.0"
|
||||
strip-json-comments: "npm:^3.1.1"
|
||||
type-fest: "npm:^4.20.0"
|
||||
validate-npm-package-name: "npm:^5.0.1"
|
||||
type-fest: "npm:^4.26.1"
|
||||
validate-npm-package-name: "npm:^6.0.0"
|
||||
bin:
|
||||
npmPkgJsonLint: dist/cli.js
|
||||
checksum: 10/a60ff54288b66c7cc6a750a47ab37fd619d6747933a406daf92c95bc1efcf24300aa324c52ab34c9eeee019e05d31a9ee7406f745bf801d9000efd9929abe9d0
|
||||
checksum: 10/449a0d4235e8f12f752fd181718bec7916513fe8f64b9ba3155853da7f05450021e2d7e546fcc3811d4239f16cb996b6aaabe6d7213c2e08acc6bc0c39213833
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -43612,10 +43643,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rate-limiter-flexible@npm:7.3.2":
|
||||
version: 7.3.2
|
||||
resolution: "rate-limiter-flexible@npm:7.3.2"
|
||||
checksum: 10/f95c9af17c52899ab939593a9fa60a0a2d69b8724b1b87bd67370b0414c1845d5a9e5cee5ad1dbafa7ced3f7d804fba1e42393817b1dee17eb44888987217e6d
|
||||
"rate-limiter-flexible@npm:7.4.0":
|
||||
version: 7.4.0
|
||||
resolution: "rate-limiter-flexible@npm:7.4.0"
|
||||
checksum: 10/85012124949028213494ac7a24eaf6f3b850aa6bc3d503afb1cd23d6d42721a9a05172e3b7cf1a59b818580ce3815a96eaad54b484eb573917911246f28311d9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -43933,12 +43964,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-device-info@npm:14.0.4":
|
||||
version: 14.0.4
|
||||
resolution: "react-native-device-info@npm:14.0.4"
|
||||
"react-native-device-info@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "react-native-device-info@npm:14.1.1"
|
||||
peerDependencies:
|
||||
react-native: "*"
|
||||
checksum: 10/bf031048551597b1a9ab2965d498cbd073eacf50005dffa4e3496286578734a45854141d47654e7e58ef8531b8c2cd6d1670bfd75625271c91aab3b3b8d0a8d8
|
||||
checksum: 10/560c5f8d990fc47e7f07ba526b0b016352854b9badac2ec1d5676912ea81df1a15eb05b78e4fb077d552e9dc4b6eff585ea0aad0f5d6a8f23a5cd4bfdf4b4652
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -46162,11 +46193,11 @@ __metadata:
|
||||
http-server: "npm:14.1.1"
|
||||
husky: "npm:9.1.7"
|
||||
lerna: "npm:3.22.1"
|
||||
lint-staged: "npm:16.1.6"
|
||||
lint-staged: "npm:16.2.6"
|
||||
madge: "npm:8.0.0"
|
||||
node-gyp: "npm:11.4.2"
|
||||
nodemon: "npm:3.1.10"
|
||||
npm-package-json-lint: "npm:8.0.0"
|
||||
npm-package-json-lint: "npm:9.0.0"
|
||||
typescript: "npm:5.8.3"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -46802,7 +46833,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.2":
|
||||
"semver@npm:^7.6.3, semver@npm:^7.7.2":
|
||||
version: 7.7.2
|
||||
resolution: "semver@npm:7.7.2"
|
||||
bin:
|
||||
@@ -47410,13 +47441,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"short-uuid@npm:4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "short-uuid@npm:4.2.0"
|
||||
"short-uuid@npm:5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "short-uuid@npm:5.2.0"
|
||||
dependencies:
|
||||
any-base: "npm:^1.1.0"
|
||||
uuid: "npm:^8.3.2"
|
||||
checksum: 10/c9765dd0f21c5a6e0b2245926c73a13079b021dab34f5d152579cf76c11895c20ece6fab06c0dff786041a1c878f12032daec89adc03863fe6d68849e0a475c5
|
||||
uuid: "npm:^9.0.1"
|
||||
checksum: 10/9915e92c512c96684683e8fd56cdd848ce6d139db602567cc10fbd26313700db6c09f138687ee4bc13a7f41c71099948f4c9169e8b111e2bbffb1f8090b0af7c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -50918,12 +50949,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"turndown@npm:7.2.1":
|
||||
version: 7.2.1
|
||||
resolution: "turndown@npm:7.2.1"
|
||||
"turndown@npm:7.2.2":
|
||||
version: 7.2.2
|
||||
resolution: "turndown@npm:7.2.2"
|
||||
dependencies:
|
||||
"@mixmark-io/domino": "npm:^2.2.0"
|
||||
checksum: 10/8c8986e7b6d2f93af25d7b1af6f50a2c53f6bb8229b5a333fb404bc54f3bd99bb96992fe78651bbb12ed830d590a8a5f5608289ed3a067de5a9d51b92ed27b49
|
||||
checksum: 10/e0a6f7f0c2bc8447ca7ea145348c9e337163a00c9d95f49e5eecdbc9931002aa0253c7561fbc299280c854d5308ed5a0eacfc9b4e399d698fce64be4d2d7bb52
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -51055,7 +51086,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-fest@npm:^4.20.0, type-fest@npm:^4.41.0":
|
||||
"type-fest@npm:^4.26.1, type-fest@npm:^4.41.0":
|
||||
version: 4.41.0
|
||||
resolution: "type-fest@npm:4.41.0"
|
||||
checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212
|
||||
@@ -52383,6 +52414,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uuid@npm:^9.0.1":
|
||||
version: 9.0.1
|
||||
resolution: "uuid@npm:9.0.1"
|
||||
bin:
|
||||
uuid: dist/bin/uuid
|
||||
checksum: 10/9d0b6adb72b736e36f2b1b53da0d559125ba3e39d913b6072f6f033e0c87835b414f0836b45bcfaf2bdf698f92297fea1c3cc19b0b258bc182c9c43cc0fab9f2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"v8-compile-cache-lib@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "v8-compile-cache-lib@npm:3.0.1"
|
||||
@@ -52438,10 +52478,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"validate-npm-package-name@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "validate-npm-package-name@npm:5.0.1"
|
||||
checksum: 10/0d583a1af23aeffea7748742cf22b6802458736fb8b60323ba5949763824d46f796474b0e1b9206beb716f9d75269e19dbd7795d6b038b29d561be95dd827381
|
||||
"validate-npm-package-name@npm:^6.0.0":
|
||||
version: 6.0.2
|
||||
resolution: "validate-npm-package-name@npm:6.0.2"
|
||||
checksum: 10/f0e022b0a7f11345a92b64121b059b720204cd64406a0d65d81526181dcb70aef551c7c6bf9ca37b91607a7c6ff4d62e1f63a86c8d9b7346d722a641a4bd8789
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user