You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-02-04 07:53:44 +02:00
Compare commits
52 Commits
notarize_p
...
frontmatte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ea277d17 | ||
|
|
26ce17e0d8 | ||
|
|
c278b45c78 | ||
|
|
0dafd21db0 | ||
|
|
490d35919c | ||
|
|
4c1ca5480d | ||
|
|
d414c6354a | ||
|
|
7651d8e3c4 | ||
|
|
d5c72c13cb | ||
|
|
4377634e7b | ||
|
|
69ec5c7f86 | ||
|
|
f02b0f48d8 | ||
|
|
4d77c1385f | ||
|
|
c83f9ddeac | ||
|
|
1b9c11df7b | ||
|
|
333a8723e8 | ||
|
|
e030c8271d | ||
|
|
560bc31445 | ||
|
|
c71aeb74b2 | ||
|
|
ffaf2acb66 | ||
|
|
f442f1fb23 | ||
|
|
81a1451820 | ||
|
|
b3a3d71461 | ||
|
|
1db38c3232 | ||
|
|
42e645eb70 | ||
|
|
3860f44d06 | ||
|
|
4df0f8668d | ||
|
|
306d0fddd8 | ||
|
|
56d12b28f2 | ||
|
|
6c5ea4872a | ||
|
|
9856e8ae93 | ||
|
|
5712da4c0f | ||
|
|
4f7ee56444 | ||
|
|
8e2b6ca296 | ||
|
|
0172bb0ad8 | ||
|
|
1d38e443ba | ||
|
|
5ad19b7261 | ||
|
|
70293478a2 | ||
|
|
3aaa20254f | ||
|
|
42c248f7ca | ||
|
|
ac1e94a8df | ||
|
|
daff4496cf | ||
|
|
1e00078228 | ||
|
|
03a1de9370 | ||
|
|
55ef256c65 | ||
|
|
6d115db16f | ||
|
|
5853031fde | ||
|
|
47db2ae962 | ||
|
|
b960a2a8b0 | ||
|
|
fcaa7d2a98 | ||
|
|
99284ae135 | ||
|
|
66ae58c81b |
@@ -1045,6 +1045,8 @@ packages/editor/CodeMirror/extensions/links/utils/getUrlAtPosition.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/openLink.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownMathExtension.test.js
|
||||
@@ -1811,6 +1813,8 @@ packages/renderer/MdToHtml/rules/code_inline.js
|
||||
packages/renderer/MdToHtml/rules/externalEmbed.js
|
||||
packages/renderer/MdToHtml/rules/fence.js
|
||||
packages/renderer/MdToHtml/rules/fountain.js
|
||||
packages/renderer/MdToHtml/rules/frontmatter.test.js
|
||||
packages/renderer/MdToHtml/rules/frontmatter.js
|
||||
packages/renderer/MdToHtml/rules/highlight_keywords.js
|
||||
packages/renderer/MdToHtml/rules/html_image.js
|
||||
packages/renderer/MdToHtml/rules/image.js
|
||||
@@ -1851,17 +1855,22 @@ packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/model/ResourceRecord.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/extractResourceIds.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
packages/tools/fuzzer/utils/hangingIndent.js
|
||||
packages/tools/fuzzer/utils/logDiffDebug.js
|
||||
packages/tools/fuzzer/utils/openDebugSession.js
|
||||
packages/tools/fuzzer/utils/randomId.test.js
|
||||
packages/tools/fuzzer/utils/randomId.js
|
||||
packages/tools/fuzzer/utils/randomString.js
|
||||
packages/tools/fuzzer/utils/retryWithCount.js
|
||||
packages/tools/generate-database-types.js
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1019,6 +1019,8 @@ packages/editor/CodeMirror/extensions/links/utils/getUrlAtPosition.js
|
||||
packages/editor/CodeMirror/extensions/links/utils/openLink.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownDecorationExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/markdownHighlightExtension.js
|
||||
packages/editor/CodeMirror/extensions/markdownMathExtension.test.js
|
||||
@@ -1785,6 +1787,8 @@ packages/renderer/MdToHtml/rules/code_inline.js
|
||||
packages/renderer/MdToHtml/rules/externalEmbed.js
|
||||
packages/renderer/MdToHtml/rules/fence.js
|
||||
packages/renderer/MdToHtml/rules/fountain.js
|
||||
packages/renderer/MdToHtml/rules/frontmatter.test.js
|
||||
packages/renderer/MdToHtml/rules/frontmatter.js
|
||||
packages/renderer/MdToHtml/rules/highlight_keywords.js
|
||||
packages/renderer/MdToHtml/rules/html_image.js
|
||||
packages/renderer/MdToHtml/rules/image.js
|
||||
@@ -1825,17 +1829,22 @@ packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/model/ResourceRecord.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/extractResourceIds.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
packages/tools/fuzzer/utils/hangingIndent.js
|
||||
packages/tools/fuzzer/utils/logDiffDebug.js
|
||||
packages/tools/fuzzer/utils/openDebugSession.js
|
||||
packages/tools/fuzzer/utils/randomId.test.js
|
||||
packages/tools/fuzzer/utils/randomId.js
|
||||
packages/tools/fuzzer/utils/randomString.js
|
||||
packages/tools/fuzzer/utils/retryWithCount.js
|
||||
packages/tools/generate-database-types.js
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"vips.dev": {
|
||||
"platforms": ["aarch64-darwin"],
|
||||
},
|
||||
"nodejs": "24.5.0",
|
||||
"nodejs": "24.8.0",
|
||||
"pkg-config": "latest",
|
||||
"python": "3.13.3",
|
||||
"bat": "latest",
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
"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": "9.0.0",
|
||||
"typescript": "5.8.3"
|
||||
|
||||
@@ -51,6 +51,15 @@ function newBlockSource(language = '', content = '', previousSource: SourceInfo
|
||||
} else {
|
||||
fence = '$$';
|
||||
}
|
||||
} else if (language === 'frontmatter') {
|
||||
// Frontmatter uses --- delimiters instead of code fences
|
||||
return {
|
||||
openCharacters: '---\n',
|
||||
closeCharacters: '\n---\n',
|
||||
content: content,
|
||||
node: null,
|
||||
language: language,
|
||||
};
|
||||
}
|
||||
|
||||
const fenceLanguage = language === 'katex' ? '' : language;
|
||||
|
||||
@@ -131,7 +131,9 @@ module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
testEnvironmentOptions: {
|
||||
customExportConditions: ['node', 'require'],
|
||||
},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"debounce": "1.2.1",
|
||||
"electron": "39.2.3",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-updater": "6.6.2",
|
||||
"electron-updater": "6.6.8",
|
||||
"electron-window-state": "5.0.3",
|
||||
"esbuild": "^0.25.3",
|
||||
"formatcoords": "1.1.3",
|
||||
|
||||
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.
|
||||
|
||||
@@ -438,9 +438,8 @@ const useInputEventHandlers = ({
|
||||
const onSubmit = useCallback(() => {
|
||||
if (selectedResult) {
|
||||
onItemSelected(selectedResult, selectedIndex);
|
||||
setSearch('');
|
||||
}
|
||||
}, [onItemSelected, selectedResult, selectedIndex, setSearch]);
|
||||
}, [onItemSelected, selectedResult, selectedIndex]);
|
||||
|
||||
// For now, onKeyPress only works on web.
|
||||
// See https://github.com/react-native-community/discussions-and-proposals/issues/249
|
||||
|
||||
@@ -694,10 +694,17 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
</Menu>
|
||||
);
|
||||
|
||||
// Updating the state of this component can result in the left most element becoming hidden, so add a dummy as the first element to prevent this
|
||||
// See https://github.com/laurent22/joplin/issues/14153
|
||||
const zeroWidthSpacer = (
|
||||
<View style={{ width: 0 }} pointerEvents="none"/>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={this.styles().outerContainer}>
|
||||
<View style={this.styles().aboveHeader}/>
|
||||
<View style={this.styles().innerContainer}>
|
||||
{zeroWidthSpacer}
|
||||
{sideMenuComp}
|
||||
{backButtonComp}
|
||||
{renderUndoButton()}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@joplin/utils": "~3.6",
|
||||
"@js-draw/material-icons": "1.33.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-community/datetimepicker": "8.4.5",
|
||||
"@react-native-community/datetimepicker": "8.4.7",
|
||||
"@react-native-community/geolocation": "3.4.0",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
@@ -73,7 +73,7 @@
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-quick-crypto": "0.7.17",
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "5.6.1",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "12.2.0",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
@@ -114,13 +114,13 @@
|
||||
"@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.164",
|
||||
"@types/tar-stream": "3.1.4",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"babel-plugin-react-native-web": "0.21.2",
|
||||
"esbuild": "0.25.11",
|
||||
"esbuild": "0.25.12",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "11.3.2",
|
||||
"gulp": "4.0.2",
|
||||
@@ -132,7 +132,7 @@
|
||||
"punycode": "2.3.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-native-web": "0.21.2",
|
||||
"react-refresh": "0.17.0",
|
||||
"react-refresh": "0.18.0",
|
||||
"react-test-renderer": "19.0.0",
|
||||
"sharp": "0.34.4",
|
||||
"sqlite3": "5.1.6",
|
||||
|
||||
@@ -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,7 +13,7 @@
|
||||
"url": "git+https://github.com/laurent22/joplin.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/yargs": "17.0.33",
|
||||
"@types/yargs": "17.0.34",
|
||||
"joplin-plugin-freehand-drawing": "4.3.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.8.3"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { deleteMarkupBackward, markdown, markdownLanguage } from '@codemirror/la
|
||||
import { GFM as GitHubFlavoredMarkdownExtension } from '@lezer/markdown';
|
||||
import markdownMathExtension from './extensions/markdownMathExtension';
|
||||
import markdownHighlightExtension from './extensions/markdownHighlightExtension';
|
||||
import markdownFrontMatterExtension from './extensions/markdownFrontMatterExtension';
|
||||
import lookUpLanguage from './utils/markdown/codeBlockLanguages/lookUpLanguage';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { defaultKeymap, emacsStyleKeymap } from '@codemirror/commands';
|
||||
@@ -30,6 +31,9 @@ const configFromSettings = (settings: EditorSettings, context: RenderedContentCo
|
||||
extensions: [
|
||||
GitHubFlavoredMarkdownExtension,
|
||||
|
||||
// FrontMatter support (YAML blocks at start of document)
|
||||
markdownFrontMatterExtension,
|
||||
|
||||
settings.markdownMarkEnabled ? markdownHighlightExtension : [],
|
||||
|
||||
// Don't highlight KaTeX if the user disabled it
|
||||
|
||||
@@ -32,6 +32,10 @@ const mathBlockDecoration = Decoration.line({
|
||||
attributes: { class: 'cm-mathBlock', ...noSpellCheckAttrs },
|
||||
});
|
||||
|
||||
const frontMatterDecoration = Decoration.line({
|
||||
attributes: { class: 'cm-frontMatter', ...noSpellCheckAttrs },
|
||||
});
|
||||
|
||||
const inlineMathDecoration = Decoration.mark({
|
||||
attributes: { class: 'cm-inlineMath', ...noSpellCheckAttrs },
|
||||
});
|
||||
@@ -116,6 +120,7 @@ const nodeNameToLineDecoration: Record<string, Decoration> = {
|
||||
'FencedCode': codeBlockDecoration,
|
||||
'CodeBlock': codeBlockDecoration,
|
||||
'BlockMath': mathBlockDecoration,
|
||||
'FrontMatter': frontMatterDecoration,
|
||||
'Blockquote': blockQuoteDecoration,
|
||||
'OrderedList': orderedListDecoration,
|
||||
'BulletList': unorderedListDecoration,
|
||||
@@ -152,6 +157,7 @@ const multilineNodes = {
|
||||
'FencedCode': true,
|
||||
'CodeBlock': true,
|
||||
'BlockMath': true,
|
||||
'FrontMatter': true,
|
||||
'Blockquote': true,
|
||||
'OrderedList': true,
|
||||
'BulletList': true,
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { EditorSelection, EditorState } from '@codemirror/state';
|
||||
import { frontMatterTagName, frontMatterContentTagName, frontMatterMarkerTagName } from './markdownFrontMatterExtension';
|
||||
|
||||
import createTestEditor from '../testing/createTestEditor';
|
||||
import findNodesWithName from '../testing/findNodesWithName';
|
||||
|
||||
// Creates an EditorState with FrontMatter and markdown extensions
|
||||
const createEditorState = async (initialText: string, expectedTags: string[]): Promise<EditorState> => {
|
||||
return (await createTestEditor(initialText, EditorSelection.cursor(0), expectedTags)).state;
|
||||
};
|
||||
|
||||
describe('MarkdownFrontMatterExtension', () => {
|
||||
|
||||
jest.retryTimes(2);
|
||||
|
||||
it('should parse a basic FrontMatter block at the start of the document', async () => {
|
||||
const documentText = '---\ntitle: Test\n---\n\n# Heading';
|
||||
const editor = await createEditorState(documentText, [frontMatterTagName, 'ATXHeading1']);
|
||||
const frontMatterNodes = findNodesWithName(editor, frontMatterTagName);
|
||||
|
||||
expect(frontMatterNodes.length).toBe(1);
|
||||
expect(frontMatterNodes[0].from).toBe(0);
|
||||
expect(frontMatterNodes[0].to).toBe('---\ntitle: Test\n---'.length);
|
||||
});
|
||||
|
||||
it('should parse FrontMatter with multiple properties', async () => {
|
||||
const frontMatter = '---\ntitle: Test\ndate: 2024-01-01\ntags: [one, two]\n---';
|
||||
const documentText = `${frontMatter}\n\nContent here.`;
|
||||
const editor = await createEditorState(documentText, [frontMatterTagName]);
|
||||
const frontMatterNodes = findNodesWithName(editor, frontMatterTagName);
|
||||
|
||||
expect(frontMatterNodes.length).toBe(1);
|
||||
expect(frontMatterNodes[0].from).toBe(0);
|
||||
expect(frontMatterNodes[0].to).toBe(frontMatter.length);
|
||||
});
|
||||
|
||||
it('should not parse FrontMatter if not at document start', async () => {
|
||||
const documentText = 'Some text\n\n---\ntitle: Test\n---';
|
||||
const editor = await createEditorState(documentText, ['Paragraph']);
|
||||
const frontMatterNodes = findNodesWithName(editor, frontMatterTagName);
|
||||
|
||||
// Should not be recognized as FrontMatter since it's not at the start
|
||||
expect(frontMatterNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not parse FrontMatter without closing delimiter', async () => {
|
||||
// Test document with --- at start but no closing delimiter
|
||||
// This should be parsed as a horizontal rule followed by content
|
||||
const documentText = '# Heading\n\n---\ntitle: Test';
|
||||
const editor = await createEditorState(documentText, ['ATXHeading1', 'HorizontalRule']);
|
||||
const frontMatterNodes = findNodesWithName(editor, frontMatterTagName);
|
||||
|
||||
// FrontMatter only works at the very start of the document, so this should not be recognized
|
||||
expect(frontMatterNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle empty FrontMatter block', async () => {
|
||||
const documentText = '---\n---\n\n# Heading';
|
||||
const editor = await createEditorState(documentText, [frontMatterTagName, 'ATXHeading1']);
|
||||
const frontMatterNodes = findNodesWithName(editor, frontMatterTagName);
|
||||
|
||||
expect(frontMatterNodes.length).toBe(1);
|
||||
expect(frontMatterNodes[0].from).toBe(0);
|
||||
expect(frontMatterNodes[0].to).toBe('---\n---'.length);
|
||||
});
|
||||
|
||||
it('should have FrontMatterContent as child node', async () => {
|
||||
const documentText = '---\nkey: value\n---';
|
||||
const editor = await createEditorState(documentText, [frontMatterTagName]);
|
||||
const frontMatterNodes = findNodesWithName(editor, frontMatterTagName);
|
||||
const contentNodes = findNodesWithName(editor, frontMatterContentTagName);
|
||||
|
||||
expect(frontMatterNodes.length).toBe(1);
|
||||
// Content node may be replaced by YAML parser, but if not, it should exist
|
||||
// The presence depends on whether YAML language was loaded
|
||||
expect(contentNodes.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should not confuse horizontal rules with FrontMatter', async () => {
|
||||
const documentText = '# Title\n\n---\n\nSome text';
|
||||
const editor = await createEditorState(documentText, ['ATXHeading1', 'HorizontalRule']);
|
||||
const frontMatterNodes = findNodesWithName(editor, frontMatterTagName);
|
||||
const hrNodes = findNodesWithName(editor, 'HorizontalRule');
|
||||
|
||||
expect(frontMatterNodes.length).toBe(0);
|
||||
expect(hrNodes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should create FrontMatterMarker nodes for the delimiters', async () => {
|
||||
const documentText = '---\ntitle: Test\n---\n\n# Heading';
|
||||
const editor = await createEditorState(documentText, [frontMatterTagName, frontMatterMarkerTagName]);
|
||||
const markerNodes = findNodesWithName(editor, frontMatterMarkerTagName);
|
||||
|
||||
// Should have two markers: opening and closing ---
|
||||
expect(markerNodes.length).toBe(2);
|
||||
|
||||
// Opening marker
|
||||
expect(markerNodes[0].from).toBe(0);
|
||||
expect(markerNodes[0].to).toBe(3); // '---'.length
|
||||
|
||||
// Closing marker
|
||||
expect(markerNodes[1].from).toBe('---\ntitle: Test\n'.length);
|
||||
expect(markerNodes[1].to).toBe('---\ntitle: Test\n---'.length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
// Extension for parsing and highlighting YAML FrontMatter blocks at the start of a document.
|
||||
//
|
||||
// A FrontMatter block is delimited by --- at the very start of the document:
|
||||
// ---
|
||||
// title: My Document
|
||||
// date: 2024-01-01
|
||||
// ---
|
||||
|
||||
import { Tag } from '@lezer/highlight';
|
||||
import { parseMixed, SyntaxNodeRef, Input, NestedParse, ParseWrapper } from '@lezer/common';
|
||||
import { MarkdownConfig, BlockContext, Line, LeafBlock, MarkdownExtension } from '@lezer/markdown';
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
|
||||
export const frontMatterTagName = 'FrontMatter';
|
||||
export const frontMatterContentTagName = 'FrontMatterContent';
|
||||
export const frontMatterMarkerTagName = 'FrontMatterMarker';
|
||||
|
||||
export const frontMatterTag = Tag.define();
|
||||
|
||||
// Create the YAML language parser using the legacy mode
|
||||
const yamlLanguage = StreamLanguage.define(yaml);
|
||||
|
||||
// Wraps a YAML parser for the FrontMatter content.
|
||||
// This replaces [nodeTag] from the syntax tree with a region handled by the YAML parser.
|
||||
const wrappedYamlParser = (nodeTag: string): ParseWrapper => {
|
||||
return parseMixed((node: SyntaxNodeRef, _input: Input): NestedParse => {
|
||||
if (node.name !== nodeTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
parser: yamlLanguage.parser,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Regex to match the FrontMatter delimiter (--- at start of line)
|
||||
const frontMatterDelimiterRegex = /^---\s*$/;
|
||||
|
||||
const frontMatterConfig: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
{
|
||||
name: frontMatterTagName,
|
||||
style: frontMatterTag,
|
||||
},
|
||||
{
|
||||
name: frontMatterContentTagName,
|
||||
},
|
||||
{
|
||||
name: frontMatterMarkerTagName,
|
||||
style: frontMatterTag,
|
||||
},
|
||||
],
|
||||
parseBlock: [{
|
||||
name: frontMatterTagName,
|
||||
before: 'HorizontalRule',
|
||||
parse(cx: BlockContext, line: Line): boolean {
|
||||
// FrontMatter must be at the very start of the document
|
||||
if (cx.lineStart !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the first line is ---
|
||||
if (!frontMatterDelimiterRegex.test(line.text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store the opening delimiter position
|
||||
const openingMarkerStart = cx.lineStart;
|
||||
const openingMarkerEnd = cx.lineStart + line.text.length;
|
||||
|
||||
const contentStart = openingMarkerEnd + 1; // Start after the opening --- and newline
|
||||
let foundEnd = false;
|
||||
|
||||
// Consume lines until we find the closing ---
|
||||
while (cx.nextLine()) {
|
||||
if (frontMatterDelimiterRegex.test(line.text)) {
|
||||
foundEnd = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundEnd) {
|
||||
// No closing delimiter found - not a valid FrontMatter block
|
||||
return false;
|
||||
}
|
||||
|
||||
// The content is between the two --- delimiters
|
||||
const contentEnd = cx.lineStart; // Start of the closing --- line
|
||||
|
||||
// Closing delimiter positions
|
||||
const closingMarkerStart = cx.lineStart;
|
||||
const closingMarkerEnd = cx.lineStart + line.text.length;
|
||||
|
||||
// Create marker elements for the --- delimiters
|
||||
const openingMarkerElem = cx.elt(frontMatterMarkerTagName, openingMarkerStart, openingMarkerEnd);
|
||||
const closingMarkerElem = cx.elt(frontMatterMarkerTagName, closingMarkerStart, closingMarkerEnd);
|
||||
|
||||
// Create the content element (the YAML content between delimiters)
|
||||
const contentElem = cx.elt(frontMatterContentTagName, contentStart, contentEnd);
|
||||
|
||||
// Create the container element spanning from start of first --- to end of last ---
|
||||
const containerElement = cx.elt(
|
||||
frontMatterTagName,
|
||||
0, // Start at document beginning
|
||||
closingMarkerEnd, // End after closing ---
|
||||
[openingMarkerElem, contentElem, closingMarkerElem],
|
||||
);
|
||||
|
||||
cx.addElement(containerElement);
|
||||
|
||||
// Move past the closing delimiter
|
||||
cx.nextLine();
|
||||
|
||||
return true;
|
||||
},
|
||||
// FrontMatter blocks can end leaf blocks like paragraphs
|
||||
endLeaf(_cx: BlockContext, line: Line, _leaf: LeafBlock): boolean {
|
||||
return frontMatterDelimiterRegex.test(line.text);
|
||||
},
|
||||
}],
|
||||
wrap: wrappedYamlParser(frontMatterContentTagName),
|
||||
};
|
||||
|
||||
const markdownFrontMatterExtension: MarkdownExtension = [frontMatterConfig];
|
||||
|
||||
export default markdownFrontMatterExtension;
|
||||
@@ -6,6 +6,33 @@ import makeBlockReplaceExtension from './utils/makeBlockReplaceExtension';
|
||||
|
||||
const imageClassName = 'cm-md-image';
|
||||
|
||||
class ImageHeightCache {
|
||||
private readonly cache = new Map<string, number>();
|
||||
private readonly maxEntries = 500;
|
||||
|
||||
public get(key: string): number | undefined {
|
||||
const value = this.cache.get(key);
|
||||
if (value !== undefined) {
|
||||
// Refresh recency
|
||||
this.cache.delete(key);
|
||||
this.cache.set(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public set(key: string, height: number): void {
|
||||
if (this.cache.has(key)) {
|
||||
this.cache.delete(key);
|
||||
} else if (this.cache.size >= this.maxEntries) {
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
if (firstKey) this.cache.delete(firstKey);
|
||||
}
|
||||
this.cache.set(key, height);
|
||||
}
|
||||
}
|
||||
|
||||
const imageHeightCache = new ImageHeightCache();
|
||||
|
||||
class ImageWidget extends WidgetType {
|
||||
private resolvedSrc_: string;
|
||||
|
||||
@@ -41,9 +68,16 @@ class ImageWidget extends WidgetType {
|
||||
|
||||
const updateImageUrl = () => {
|
||||
if (this.resolvedSrc_) {
|
||||
// Use a background-image style property rather than img[src=]. This
|
||||
// simplifies setting the image to the correct size/position.
|
||||
image.src = this.resolvedSrc_;
|
||||
// When the image loads, measure and cache the height
|
||||
image.onload = () => {
|
||||
// Measure container height (what CodeMirror uses for scroll calculations).
|
||||
if (dom.isConnected) {
|
||||
imageHeightCache.set(this.cacheKey, dom.offsetHeight);
|
||||
}
|
||||
|
||||
dom.style.minHeight = '';
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,10 +90,16 @@ class ImageWidget extends WidgetType {
|
||||
updateImageUrl();
|
||||
}
|
||||
|
||||
// Apply cached height as min-height to prevent collapse during load.
|
||||
const cached = imageHeightCache.get(this.cacheKey);
|
||||
if (cached) {
|
||||
dom.style.minHeight = `${cached}px`;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public toDOM() {
|
||||
public toDOM(_view: EditorView) {
|
||||
const container = document.createElement('div');
|
||||
container.classList.add(imageClassName);
|
||||
|
||||
@@ -72,8 +112,12 @@ class ImageWidget extends WidgetType {
|
||||
return container;
|
||||
}
|
||||
|
||||
private get cacheKey() {
|
||||
return `${this.src_}_${this.width_ ?? ''}_${this.reloadCounter_}`;
|
||||
}
|
||||
|
||||
public get estimatedHeight() {
|
||||
return -1;
|
||||
return imageHeightCache.get(this.cacheKey) ?? -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ class DividerWidget extends WidgetType {
|
||||
|
||||
const dividerLineMark = Decoration.line({ class: dividerLineClassName });
|
||||
|
||||
// Node names that should be rendered as dividers
|
||||
const dividerNodeNames = ['HorizontalRule', 'FrontMatterMarker'];
|
||||
|
||||
const replaceDividers = [
|
||||
EditorView.theme({
|
||||
[`& .cm-line.${dividerLineClassName}`]: {
|
||||
@@ -47,7 +50,7 @@ const replaceDividers = [
|
||||
}),
|
||||
makeInlineReplaceExtension({
|
||||
createDecoration: (node) => {
|
||||
if (node.name === 'HorizontalRule') {
|
||||
if (dividerNodeNames.includes(node.name)) {
|
||||
return new DividerWidget();
|
||||
}
|
||||
return null;
|
||||
@@ -55,7 +58,7 @@ const replaceDividers = [
|
||||
}),
|
||||
makeInlineReplaceExtension({
|
||||
createDecoration: (node) => {
|
||||
if (node.name === 'HorizontalRule') {
|
||||
if (dividerNodeNames.includes(node.name)) {
|
||||
return dividerLineMark;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -14,13 +14,18 @@ const createEditor = async (initialMarkdown: string, expectedTags: string[] = ['
|
||||
|
||||
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 }) => {
|
||||
{ markdown: '<sup>Test</sup>', expectedDomTags: 'sup' },
|
||||
{ markdown: '<strike>Test</strike>', expectedDomTags: 'strike' },
|
||||
{ markdown: 'Test: <span style="color: red;">Test</span>', expectedDomTags: 'span[style]' },
|
||||
{ markdown: 'Test: <span style="color: rgb(123, 0, 0);">Test</span>', expectedDomTags: 'span[style]' },
|
||||
{
|
||||
markdown: '<sup>Test *test*...</sup>',
|
||||
expectedDomTags: 'sup',
|
||||
initialSyntaxTags: ['HTMLTag', 'Emphasis'],
|
||||
},
|
||||
])('should render inline HTML (case %#)', async ({ markdown, expectedDomTags: expectedTagsQuery, initialSyntaxTags }) => {
|
||||
// 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`);
|
||||
const editor = await createEditor(`\n\n${markdown}\n\n`, initialSyntaxTags);
|
||||
|
||||
expect(editor.contentDOM.querySelector(expectedTagsQuery)).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -31,9 +31,9 @@ const createHtmlReplacementExtension = (tagName: string, onRenderContent: OnRend
|
||||
// Find the matching closing tag
|
||||
for (; !!cursor && nestedTagCounter > 0; cursor = cursor.nextSibling) {
|
||||
const info = htmlNodeInfo(cursor, state);
|
||||
if (isMatchingOpeningTag(info)) {
|
||||
if (info && isMatchingOpeningTag(info)) {
|
||||
nestedTagCounter ++;
|
||||
} else if (isMatchingClosingTag(info)) {
|
||||
} else if (info && isMatchingClosingTag(info)) {
|
||||
nestedTagCounter --;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import forceFullParse from './forceFullParse';
|
||||
import loadLanguages from './loadLanguages';
|
||||
import markdownMathExtension from '../extensions/markdownMathExtension';
|
||||
import markdownHighlightExtension from '../extensions/markdownHighlightExtension';
|
||||
import markdownFrontMatterExtension from '../extensions/markdownFrontMatterExtension';
|
||||
|
||||
// Creates and returns a minimal editor with markdown extensions. Waits to return the editor
|
||||
// until all syntax tree tags in `expectedSyntaxTreeTags` exist.
|
||||
@@ -26,7 +27,7 @@ const createTestEditor = async (
|
||||
selection: EditorSelection.create(initialSelection),
|
||||
extensions: [
|
||||
markdown({
|
||||
extensions: [markdownMathExtension, markdownHighlightExtension, GithubFlavoredMarkdownExt],
|
||||
extensions: [markdownMathExtension, markdownHighlightExtension, markdownFrontMatterExtension, GithubFlavoredMarkdownExt],
|
||||
addKeymap: addMarkdownKeymap,
|
||||
}),
|
||||
indentUnit.of('\t'),
|
||||
|
||||
47
packages/editor/CodeMirror/theme.ts
vendored
47
packages/editor/CodeMirror/theme.ts
vendored
@@ -139,25 +139,7 @@ const createTheme = (theme: EditorTheme): Extension[] => {
|
||||
},
|
||||
|
||||
'& .cm-codeBlock': {
|
||||
'&.cm-regionFirstLine, &.cm-regionLastLine': {
|
||||
borderRadius: '3px',
|
||||
},
|
||||
'&:not(.cm-regionFirstLine)': {
|
||||
borderTop: 'none',
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
},
|
||||
'&:not(.cm-regionLastLine)': {
|
||||
borderBottom: 'none',
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.colorFaded,
|
||||
backgroundColor: 'rgba(155, 155, 155, 0.1)',
|
||||
|
||||
backgroundColor: 'rgba(155, 155, 155, 0.07)',
|
||||
...monospaceStyle,
|
||||
},
|
||||
|
||||
@@ -269,8 +251,8 @@ const createTheme = (theme: EditorTheme): Extension[] => {
|
||||
},
|
||||
{
|
||||
tag: tags.comment,
|
||||
opacity: 0.9,
|
||||
fontStyle: 'italic',
|
||||
color: isDarkTheme ? '#b18eb1' : '#6d7086',
|
||||
},
|
||||
{
|
||||
tag: tags.link,
|
||||
@@ -281,26 +263,23 @@ const createTheme = (theme: EditorTheme): Extension[] => {
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
|
||||
// Content of code blocks
|
||||
// Content of code blocks. This should roughly match the colors used by the default
|
||||
// highlight.js theme in the note viewer, while also preserving at least 4.5:1 contrast.
|
||||
{
|
||||
tag: tags.keyword,
|
||||
color: isDarkTheme ? '#ff7' : '#740',
|
||||
},
|
||||
{
|
||||
tag: tags.operator,
|
||||
color: isDarkTheme ? '#f7f' : '#805',
|
||||
color: isDarkTheme ? '#F92672' : '#a626a4',
|
||||
},
|
||||
{
|
||||
tag: tags.literal,
|
||||
color: isDarkTheme ? '#aaf' : '#037',
|
||||
},
|
||||
{
|
||||
tag: tags.operator,
|
||||
color: isDarkTheme ? '#fa9' : '#490',
|
||||
tag: tags.number,
|
||||
color: isDarkTheme ? '#d19a66' : '#986801',
|
||||
},
|
||||
{
|
||||
tag: tags.typeName,
|
||||
color: isDarkTheme ? '#7ff' : '#a00',
|
||||
color: isDarkTheme ? '#d19a66' : '#986801',
|
||||
},
|
||||
{
|
||||
tag: tags.inserted,
|
||||
@@ -312,13 +291,21 @@ const createTheme = (theme: EditorTheme): Extension[] => {
|
||||
},
|
||||
{
|
||||
tag: tags.propertyName,
|
||||
color: isDarkTheme ? '#d96' : '#940',
|
||||
color: isDarkTheme ? '#61aeee' : '#406be5',
|
||||
},
|
||||
{
|
||||
tag: tags.string,
|
||||
color: isDarkTheme ? '#98c379' : '#50a14f',
|
||||
},
|
||||
{
|
||||
// CSS class names (and class names in other languages)
|
||||
tag: tags.className,
|
||||
color: isDarkTheme ? '#d8a' : '#904',
|
||||
},
|
||||
{
|
||||
tag: tags.macroName,
|
||||
color: isDarkTheme ? '#e6c07b' : '#986801',
|
||||
},
|
||||
]);
|
||||
|
||||
return [
|
||||
|
||||
@@ -28,21 +28,21 @@
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "6.18.3",
|
||||
"@codemirror/commands": "6.7.1",
|
||||
"@codemirror/lang-html": "6.4.9",
|
||||
"@codemirror/lang-markdown": "6.3.1",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/lang-html": "6.4.11",
|
||||
"@codemirror/lang-markdown": "6.5.0",
|
||||
"@codemirror/language": "6.10.4",
|
||||
"@codemirror/language-data": "6.3.1",
|
||||
"@codemirror/legacy-modes": "6.4.2",
|
||||
"@codemirror/lint": "6.8.3",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/lint": "6.9.2",
|
||||
"@codemirror/search": "6.5.8",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.35.0",
|
||||
"@joplin/fork-uslug": "^2.0.0",
|
||||
"@lezer/common": "1.2.3",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lezer/markdown": "1.3.2",
|
||||
"@lezer/common": "1.5.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lezer/markdown": "1.6.3",
|
||||
"@replit/codemirror-vim": "6.2.1",
|
||||
"dompurify": "3.2.7",
|
||||
"orderedmap": "2.1.1",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -520,6 +520,50 @@ describe('models/Folder.sharing', () => {
|
||||
expect(note4.user_updated_time).toBe(userUpdatedTimes[note4.id]);
|
||||
});
|
||||
|
||||
it('should prefer duplicating resources in unshared folders to shared folders', async () => {
|
||||
const resourceService = new ResourceService();
|
||||
|
||||
const folder1 = await createFolderTree('', [
|
||||
{
|
||||
title: 'folder 1', // Share 1
|
||||
children: [
|
||||
{
|
||||
title: 'note 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'folder 2', // Not shared
|
||||
children: [
|
||||
{
|
||||
title: 'note 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
let note1: NoteEntity = await Note.loadByTitle('note 1');
|
||||
let note2: NoteEntity = await Note.loadByTitle('note 2');
|
||||
|
||||
await Folder.save({ id: folder1.id, share_id: 'share1' });
|
||||
|
||||
note1 = await shim.attachFileToNote(note1, testImagePath);
|
||||
note2 = await Note.save({ id: note2.id, body: note1.body });
|
||||
|
||||
await msleep(1);
|
||||
|
||||
await resourceService.indexNoteResources(); // Populate note_resources
|
||||
await Folder.updateAllShareIds(resourceService, []);
|
||||
|
||||
// After
|
||||
expect(await Resource.all()).toHaveLength(2);
|
||||
|
||||
// note1 should have the same body
|
||||
expect(await Note.load(note1.id)).toMatchObject({ body: note1.body, share_id: 'share1' });
|
||||
// note2's body should be updated
|
||||
expect(await Note.load(note2.id)).not.toMatchObject({ body: note2.body, share_id: '' });
|
||||
});
|
||||
|
||||
it('should clear share_ids for items that are no longer part of an existing share', async () => {
|
||||
await createFolderTree('', [
|
||||
{
|
||||
|
||||
@@ -639,12 +639,21 @@ export default class Folder extends BaseItem {
|
||||
// one note. If it is not, we create duplicate resources so that
|
||||
// each note has its own separate resource.
|
||||
|
||||
// Order unshared items first: This makes conflicts less likely, since shared
|
||||
// items are more likely to be duplicated by multiple users.
|
||||
const orderingSql = 'ORDER BY is_shared ASC';
|
||||
|
||||
const noteResourceAssociations = await this.db().selectAll(`
|
||||
SELECT resource_id, note_id, notes.share_id
|
||||
SELECT
|
||||
resource_id,
|
||||
note_id,
|
||||
notes.share_id,
|
||||
(notes.share_id != '') AS is_shared
|
||||
FROM note_resources
|
||||
LEFT JOIN notes ON notes.id = note_resources.note_id
|
||||
WHERE resource_id IN (${this.escapeIdsForSql(resourceIds)})
|
||||
AND is_associated = 1
|
||||
${orderingSql}
|
||||
`) as NoteResourceRow[];
|
||||
|
||||
const resourceIdToNotes: Record<string, NoteResourceRow[]> = {};
|
||||
|
||||
@@ -48,7 +48,20 @@ describe('services/ResourceService', () => {
|
||||
expect(!(await NoteResource.all()).length).toBe(true);
|
||||
}));
|
||||
|
||||
it('should not delete resource if still associated with at least one note', (async () => {
|
||||
it.each([
|
||||
{
|
||||
linkStyle: 'image 1',
|
||||
markupTag: (id: string) => ``,
|
||||
},
|
||||
{
|
||||
linkStyle: 'image 2',
|
||||
markupTag: (id: string) => `![image][image]\n\n[image]: :/${id}`,
|
||||
},
|
||||
{
|
||||
linkStyle: 'html link',
|
||||
markupTag: (id: string) => `<a href=":/${id}">test</a>`,
|
||||
},
|
||||
])('should not delete resource if still associated with at least one note (link style: $linkStyle)', (async ({ markupTag }) => {
|
||||
const service = new ResourceService();
|
||||
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
@@ -63,7 +76,7 @@ describe('services/ResourceService', () => {
|
||||
|
||||
await service.indexNoteResources();
|
||||
|
||||
await Note.save({ id: note2.id, body: Resource.markupTag(resource1) });
|
||||
await Note.save({ id: note2.id, body: markupTag(resource1.id) });
|
||||
|
||||
await service.indexNoteResources();
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ describe('urlUtils', () => {
|
||||
['Bla [](:/11111111111111111111111111111111 "Some title") bla [](:/22222222222222222222222222222222 "something else") bla', ['11111111111111111111111111111111', '22222222222222222222222222222222']],
|
||||
['Bla <img src=":/fcca2938a96a22570e8eae2565bc6b0b"/> bla [](:/22222222222222222222222222222222) bla', ['fcca2938a96a22570e8eae2565bc6b0b', '22222222222222222222222222222222']],
|
||||
['Bla <img src=":/fcca2938a96a22570e8eae2565bc6b0b"/> bla <a href=":/33333333333333333333333333333333"/>Some note link</a> blu [](:/22222222222222222222222222222222) bla', ['fcca2938a96a22570e8eae2565bc6b0b', '33333333333333333333333333333333', '22222222222222222222222222222222']],
|
||||
['Link to [a test note] and [another] note.\n\n[a test note]: :/fcca2938a96a22570e8eae2565bc6b0b\n[another]: :/f04a2938a26822570e8eae2505bc6b0c', ['fcca2938a96a22570e8eae2565bc6b0b', 'f04a2938a26822570e8eae2505bc6b0c']],
|
||||
['nothing here', []],
|
||||
['', []],
|
||||
];
|
||||
|
||||
@@ -94,13 +94,20 @@ export const fileUrlToResourceUrl = (fileUrl: string, resourceDir: string) => {
|
||||
};
|
||||
|
||||
export const extractResourceUrls = (text: string) => {
|
||||
const markdownLinksRE = /\]\((.*?)\)/g;
|
||||
const markdownLinkRegexes = [
|
||||
// Standard [link](...)-style links
|
||||
/\]\((.*?)\)/g,
|
||||
// Reference links
|
||||
/\]:(.*?)(?:[\n]|$)/g,
|
||||
];
|
||||
const output = [];
|
||||
let result = null;
|
||||
|
||||
while ((result = markdownLinksRE.exec(text)) !== null) {
|
||||
const resourceUrlInfo = parseResourceUrl(result[1]);
|
||||
if (resourceUrlInfo) output.push(resourceUrlInfo);
|
||||
for (const regex of markdownLinkRegexes) {
|
||||
while ((result = regex.exec(text)) !== null) {
|
||||
const resourceUrlInfo = parseResourceUrl(result[1].trim());
|
||||
if (resourceUrlInfo) output.push(resourceUrlInfo);
|
||||
}
|
||||
}
|
||||
|
||||
const htmlRegexes = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -767,14 +767,9 @@ impl AttachmentInfo {
|
||||
.into())
|
||||
} else if self.data_ref.starts_with("<invfdo>") {
|
||||
// "invalid"
|
||||
log_warn!("Attempted to load an invalid {} file", self.extension);
|
||||
Err(parser_error!(
|
||||
ResolutionFailed,
|
||||
"Unable to load invalid file reference: {} (ext: {})",
|
||||
self.data_ref,
|
||||
self.extension
|
||||
)
|
||||
.into())
|
||||
log_warn!("Attempted to load an invalid {} file. Importing an empty file.", self.extension);
|
||||
// Return empty data
|
||||
Ok(FileBlob::default())
|
||||
} else {
|
||||
Err(parser_error!(
|
||||
ResolutionFailed,
|
||||
|
||||
@@ -42,6 +42,7 @@ interface RendererPlugins {
|
||||
|
||||
// /!\/!\ Note: the order of rules is important!! /!\/!\
|
||||
const rules: RendererRules = {
|
||||
frontmatter: require('./MdToHtml/rules/frontmatter').default,
|
||||
fence: require('./MdToHtml/rules/fence').default,
|
||||
sanitize_html: require('./MdToHtml/rules/sanitize_html').default,
|
||||
image: require('./MdToHtml/rules/image').default,
|
||||
|
||||
90
packages/renderer/MdToHtml/rules/frontmatter.test.ts
Normal file
90
packages/renderer/MdToHtml/rules/frontmatter.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, test, expect } from '@jest/globals';
|
||||
import MarkdownIt = require('markdown-it');
|
||||
import frontmatter from './frontmatter';
|
||||
|
||||
const createMarkdownIt = () => {
|
||||
const markdownIt = new MarkdownIt();
|
||||
markdownIt.use(frontmatter.plugin, {});
|
||||
return markdownIt;
|
||||
};
|
||||
|
||||
describe('frontmatter', () => {
|
||||
|
||||
test('should render a basic frontmatter block', () => {
|
||||
const md = createMarkdownIt();
|
||||
const input = '---\ntitle: Test\n---\n\n# Heading';
|
||||
const output = md.render(input);
|
||||
|
||||
expect(output).toContain('joplin-editable');
|
||||
expect(output).toContain('joplin-frontmatter');
|
||||
expect(output).toContain('joplin-source');
|
||||
expect(output).toContain('data-joplin-language="frontmatter"');
|
||||
expect(output).toContain('title: Test');
|
||||
expect(output).toContain('<h1>Heading</h1>');
|
||||
});
|
||||
|
||||
test('should render frontmatter with multiple properties', () => {
|
||||
const md = createMarkdownIt();
|
||||
const input = '---\ntitle: My Document\ndate: 2024-01-01\ntags: [one, two]\n---\n\nContent here.';
|
||||
const output = md.render(input);
|
||||
|
||||
expect(output).toContain('joplin-frontmatter');
|
||||
expect(output).toContain('title: My Document');
|
||||
expect(output).toContain('date: 2024-01-01');
|
||||
expect(output).toContain('tags: [one, two]');
|
||||
expect(output).toContain('<p>Content here.</p>');
|
||||
});
|
||||
|
||||
test('should not parse frontmatter if not at document start', () => {
|
||||
const md = createMarkdownIt();
|
||||
const input = 'Some text\n\n---\ntitle: Test\n---';
|
||||
const output = md.render(input);
|
||||
|
||||
// Should not contain frontmatter class
|
||||
expect(output).not.toContain('joplin-frontmatter');
|
||||
// The --- should be treated as a horizontal rule
|
||||
expect(output).toContain('<hr');
|
||||
});
|
||||
|
||||
test('should not parse frontmatter without closing delimiter', () => {
|
||||
const md = createMarkdownIt();
|
||||
const input = '---\ntitle: Test\n\n# Heading';
|
||||
const output = md.render(input);
|
||||
|
||||
// Should not contain frontmatter class since there's no closing ---
|
||||
expect(output).not.toContain('joplin-frontmatter');
|
||||
// The --- at the start becomes a horizontal rule
|
||||
expect(output).toContain('<hr');
|
||||
});
|
||||
|
||||
test('should handle empty frontmatter block', () => {
|
||||
const md = createMarkdownIt();
|
||||
const input = '---\n---\n\n# Heading';
|
||||
const output = md.render(input);
|
||||
|
||||
expect(output).toContain('joplin-frontmatter');
|
||||
expect(output).toContain('<h1>Heading</h1>');
|
||||
});
|
||||
|
||||
test('should include horizontal rule markers in rendered output', () => {
|
||||
const md = createMarkdownIt();
|
||||
const input = '---\ntitle: Test\n---\n\nContent';
|
||||
const output = md.render(input);
|
||||
|
||||
// Should have the frontmatter markers (rendered as <hr>)
|
||||
expect(output).toContain('joplin-frontmatter-marker');
|
||||
});
|
||||
|
||||
test('should have correct source-open and source-close attributes for round-trip editing', () => {
|
||||
const md = createMarkdownIt();
|
||||
const input = '---\nkey: value\n---\n\nText';
|
||||
const output = md.render(input);
|
||||
|
||||
// The joplin-source should contain just the YAML content
|
||||
expect(output).toContain('key: value');
|
||||
// The data-joplin-source-open should have the opening delimiter with newline
|
||||
expect(output).toContain('data-joplin-source-open="--- "');
|
||||
// The data-joplin-source-close should have newline + closing delimiter + newline
|
||||
expect(output).toContain('data-joplin-source-close=" --- "');
|
||||
});
|
||||
});
|
||||
142
packages/renderer/MdToHtml/rules/frontmatter.ts
Normal file
142
packages/renderer/MdToHtml/rules/frontmatter.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type * as MarkdownIt from 'markdown-it';
|
||||
import type * as StateBlock from 'markdown-it/lib/rules_block/state_block';
|
||||
import hljs from '../../highlight';
|
||||
|
||||
// Regex to match the FrontMatter delimiter (--- at start of line, optionally with trailing whitespace)
|
||||
const frontMatterDelimiterRegex = /^---\s*$/;
|
||||
|
||||
const plugin = (markdownIt: MarkdownIt, _ruleOptions: unknown) => {
|
||||
// Add a block rule to parse frontmatter at the beginning of the document
|
||||
markdownIt.block.ruler.before('fence', 'frontmatter', (state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean => {
|
||||
// FrontMatter must be at the very start of the document
|
||||
if (startLine !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const startPos = state.bMarks[startLine] + state.tShift[startLine];
|
||||
const startMax = state.eMarks[startLine];
|
||||
const startLineText = state.src.slice(startPos, startMax);
|
||||
|
||||
// Check if the first line is ---
|
||||
if (!frontMatterDelimiterRegex.test(startLineText)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the closing ---
|
||||
let nextLine = startLine + 1;
|
||||
let foundEnd = false;
|
||||
|
||||
while (nextLine < endLine) {
|
||||
const pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
||||
const max = state.eMarks[nextLine];
|
||||
const lineText = state.src.slice(pos, max);
|
||||
|
||||
if (frontMatterDelimiterRegex.test(lineText)) {
|
||||
foundEnd = true;
|
||||
break;
|
||||
}
|
||||
nextLine++;
|
||||
}
|
||||
|
||||
if (!foundEnd) {
|
||||
// No closing delimiter - not a valid FrontMatter block
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we're just checking if the rule matches (silent mode), return true
|
||||
if (silent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Extract the content between the delimiters
|
||||
const contentStartLine = startLine + 1;
|
||||
const contentEndLine = nextLine;
|
||||
|
||||
let content = '';
|
||||
for (let line = contentStartLine; line < contentEndLine; line++) {
|
||||
const pos = state.bMarks[line];
|
||||
const max = state.eMarks[line];
|
||||
content += `${state.src.slice(pos, max)}\n`;
|
||||
}
|
||||
// Remove trailing newline
|
||||
content = content.slice(0, -1);
|
||||
|
||||
// Create the token
|
||||
const token = state.push('frontmatter', 'div', 0);
|
||||
token.content = content;
|
||||
token.map = [startLine, nextLine + 1];
|
||||
token.markup = '---';
|
||||
|
||||
// Move past the closing delimiter
|
||||
state.line = nextLine + 1;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Add a renderer for the frontmatter token
|
||||
markdownIt.renderer.rules.frontmatter = (tokens, idx) => {
|
||||
const token = tokens[idx];
|
||||
const content = token.content;
|
||||
const escapeHtml = markdownIt.utils.escapeHtml;
|
||||
|
||||
// Escape the content for HTML
|
||||
const contentHtml = escapeHtml(content);
|
||||
|
||||
// Apply YAML syntax highlighting
|
||||
let highlightedContent: string;
|
||||
try {
|
||||
if (hljs.getLanguage('yaml')) {
|
||||
highlightedContent = hljs.highlight(content, { language: 'yaml', ignoreIllegals: true }).value;
|
||||
} else {
|
||||
highlightedContent = contentHtml;
|
||||
}
|
||||
} catch (_error) {
|
||||
highlightedContent = contentHtml;
|
||||
}
|
||||
|
||||
// Return the joplin-editable block structure
|
||||
// The source block contains just the YAML content (without delimiters)
|
||||
// The data-joplin-source-open and data-joplin-source-close attributes define
|
||||
// what gets prepended/appended when converting back to markdown
|
||||
// is the HTML entity for newline
|
||||
// Note: We use <pre class="hljs"> without a <code> child to avoid the
|
||||
// isCodeBlock() detection in turndown which would convert it to a fenced code block
|
||||
// IMPORTANT: No whitespace between joplin-editable and joplin-source elements!
|
||||
// The turndown joplinEditableBlockInfo function iterates childNodes and crashes
|
||||
// on text nodes (whitespace) because they don't have classList.
|
||||
return `<div class="joplin-editable joplin-frontmatter"><pre class="joplin-source" data-joplin-language="frontmatter" data-joplin-source-open="--- " data-joplin-source-close=" --- ">${contentHtml}</pre><div class="joplin-rendered joplin-frontmatter-rendered"><hr class="joplin-frontmatter-marker"/><pre class="hljs">${highlightedContent}</pre><hr class="joplin-frontmatter-marker"/></div></div>`;
|
||||
};
|
||||
};
|
||||
|
||||
const assets = () => {
|
||||
return [
|
||||
{
|
||||
inline: true,
|
||||
mime: 'text/css',
|
||||
text: `
|
||||
.joplin-frontmatter-rendered {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.joplin-frontmatter-rendered pre.hljs {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.joplin-frontmatter-marker {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-top: 2px solid var(--joplin-divider-color, #ccc);
|
||||
}
|
||||
`,
|
||||
},
|
||||
].map(e => {
|
||||
return {
|
||||
source: 'frontmatter',
|
||||
...e,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
plugin,
|
||||
assets,
|
||||
};
|
||||
@@ -34,7 +34,7 @@
|
||||
"bcryptjs": "2.4.3",
|
||||
"bulma": "1.0.4",
|
||||
"compare-versions": "6.1.1",
|
||||
"dayjs": "1.11.18",
|
||||
"dayjs": "1.11.19",
|
||||
"formidable": "2.1.2",
|
||||
"fs-extra": "11.3.2",
|
||||
"html-entities": "1.4.0",
|
||||
@@ -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,8 +77,8 @@
|
||||
"@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",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import Client from './Client';
|
||||
import ClientPool from './ClientPool';
|
||||
import { assertIsFolder, assertIsNote, FuzzContext, ItemId, RandomFolderOptions } from './types';
|
||||
import { assertIsFolder, assertIsNote, FuzzContext, ItemId, RandomFolderOptions, ResourceData } from './types';
|
||||
import { strict as assert } from 'assert';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import retryWithCount from './utils/retryWithCount';
|
||||
@@ -31,10 +30,12 @@ export default class ActionRunner {
|
||||
await this.clientPool_.checkState();
|
||||
}, {
|
||||
count: 4,
|
||||
delayOnFailure: count => count * Second * 2,
|
||||
onFail: async () => {
|
||||
logger.info('.checkState failed. Syncing all clients...');
|
||||
await this.clientPool_.syncAll();
|
||||
delayOnFailure: count => count * Second * 3,
|
||||
onFail: async ({ willRetry }) => {
|
||||
if (willRetry) {
|
||||
logger.info('.checkState failed. Syncing all clients...');
|
||||
await this.clientPool_.syncAll();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -148,7 +149,7 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
|
||||
// Create a toplevel folder to serve as this
|
||||
// folder's parent if none exist yet
|
||||
if (!parentId) {
|
||||
parentId = uuid.create();
|
||||
parentId = context.randomId();
|
||||
await client.createFolder({
|
||||
parentId: '',
|
||||
id: parentId,
|
||||
@@ -171,7 +172,7 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
|
||||
await client.createNote({
|
||||
...defaultNoteProperties,
|
||||
parentId: await selectOrCreateWriteableFolder(),
|
||||
id: uuid.create(),
|
||||
id: context.randomId(),
|
||||
title: 'Test note',
|
||||
body: 'Body',
|
||||
});
|
||||
@@ -184,12 +185,16 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
|
||||
};
|
||||
|
||||
const noteById = (id: ItemId) => {
|
||||
assert.ok(client.itemExists(id), `Could not find note with ID ${id} in client ${client.email}'s expected state.`);
|
||||
|
||||
const note = client.itemById(id);
|
||||
assertIsNote(note);
|
||||
return note;
|
||||
};
|
||||
|
||||
const folderById = (id: ItemId) => {
|
||||
assert.ok(client.itemExists(id), `Could not find folder with ID ${id} in client ${client.email}'s expected state.`);
|
||||
|
||||
const folder = client.itemById(id);
|
||||
assertIsFolder(folder);
|
||||
return folder;
|
||||
@@ -244,14 +249,29 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
|
||||
|
||||
addAction('updateNoteBody', async ({ id }) => {
|
||||
const note = noteById(id);
|
||||
|
||||
await client.updateNote({
|
||||
...note,
|
||||
body: `${note.body}\n\nUpdated.\n`,
|
||||
body: `${note.body}\n\nUpdated!`,
|
||||
});
|
||||
|
||||
return true;
|
||||
}, { id: selectOrCreateWriteableNote });
|
||||
|
||||
addAction('attachResourceTo', async ({ noteId, resourceId }) => {
|
||||
const resourceData: ResourceData = {
|
||||
id: resourceId,
|
||||
mimeType: 'text/plain',
|
||||
title: 'Test!',
|
||||
};
|
||||
await client.attachResource(noteById(noteId), resourceData);
|
||||
|
||||
return true;
|
||||
}, {
|
||||
noteId: selectOrCreateWriteableNote,
|
||||
resourceId: () => context.randomId(),
|
||||
});
|
||||
|
||||
addAction('moveNote', async ({ noteId, targetFolderId }) => {
|
||||
const note = noteById(noteId);
|
||||
const newParent = await folderByIdOrRandom(targetFolderId, {
|
||||
@@ -267,6 +287,19 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
|
||||
targetFolderId: undefinedId,
|
||||
});
|
||||
|
||||
addAction('duplicateNote', async ({ id, newNoteId }) => {
|
||||
const note = noteById(id);
|
||||
|
||||
await client.createNote({
|
||||
...note,
|
||||
id: newNoteId,
|
||||
});
|
||||
return true;
|
||||
}, {
|
||||
id: selectOrCreateWriteableNote,
|
||||
newNoteId: () => context.randomId(),
|
||||
});
|
||||
|
||||
addAction('deleteNote', async ({ id }) => {
|
||||
const validatedNote = noteById(id); // Ensure, e.g., that the note exists
|
||||
|
||||
@@ -420,8 +453,12 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
|
||||
}, {
|
||||
delayOnFailure: (count) => Second * count,
|
||||
count: 3,
|
||||
onFail: async (error) => {
|
||||
logger.warn('other.sync/other.checkState failed with', error, 'retrying...');
|
||||
onFail: async ({ error, willRetry }) => {
|
||||
logger.warn(
|
||||
'other.sync/other.checkState failed with',
|
||||
error,
|
||||
willRetry ? 'retrying...' : '',
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { ActionableClient, FolderData, FuzzContext, ItemId, NoteData, ShareOptions, TreeItem, assertIsFolder, isFolder } from './types';
|
||||
import { ActionableClient, FolderData, FuzzContext, ItemId, NoteData, ShareOptions, TreeItem, assertIsFolder, isFolder, isNote, isResource } from './types';
|
||||
import FolderRecord from './model/FolderRecord';
|
||||
import { extractResourceUrls } from '@joplin/lib/urlUtils';
|
||||
import ResourceRecord from './model/ResourceRecord';
|
||||
|
||||
interface ClientData {
|
||||
childIds: ItemId[];
|
||||
@@ -68,44 +70,71 @@ class ActionTracker {
|
||||
}
|
||||
|
||||
private checkRep_() {
|
||||
const checkParentId = (item: TreeItem) => {
|
||||
if (item.parentId) {
|
||||
const parent = this.idToItem_.get(item.parentId);
|
||||
assert.ok(parent, `should find parent (id: ${item.parentId})`);
|
||||
|
||||
assert.ok(isFolder(parent), 'parent should be a folder');
|
||||
assert.ok(parent.childIds.includes(item.id), 'parent should include the current item in its children');
|
||||
}
|
||||
};
|
||||
const checkFolder = (folder: FolderRecord) => {
|
||||
for (const childId of folder.childIds) {
|
||||
checkItem(childId);
|
||||
}
|
||||
|
||||
// Shared folders
|
||||
assert.ok(folder.ownedByEmail, 'all folders should have a "shareOwner" property (even if not shared)');
|
||||
if (folder.isRootSharedItem) {
|
||||
assert.equal(folder.parentId, '', 'only toplevel folders should be shared');
|
||||
}
|
||||
for (const sharedWith of folder.shareRecipients) {
|
||||
assert.ok(this.tree_.has(sharedWith), 'all sharee users should exist');
|
||||
}
|
||||
// isSharedWith is only valid for toplevel folders
|
||||
if (folder.parentId === '') {
|
||||
assert.ok(!folder.isSharedWith(folder.ownedByEmail), 'the share owner should not be in an item\'s sharedWith list');
|
||||
}
|
||||
|
||||
// Uniqueness
|
||||
assert.equal(
|
||||
folder.childIds.length,
|
||||
[...new Set(folder.childIds)].length,
|
||||
'child IDs should be unique',
|
||||
);
|
||||
};
|
||||
const checkNote = (note: NoteData) => {
|
||||
assert.ok(!isFolder(note));
|
||||
assert.ok(!isResource(note));
|
||||
};
|
||||
const checkResource = (resource: ResourceRecord) => {
|
||||
assert.ok(!isFolder(resource));
|
||||
assert.ok(!isNote(resource));
|
||||
assert.ok(isResource(resource));
|
||||
|
||||
// References list should be up-to-date
|
||||
for (const noteId of resource.referencedBy) {
|
||||
const note = this.idToItem_.get(noteId);
|
||||
assert.ok(note, `all references should exist (testing ID ${noteId})`);
|
||||
assert.ok(isNote(note), 'all references should be notes');
|
||||
assert.ok(note.body.includes(resource.id), 'all references should include the resource ID');
|
||||
}
|
||||
};
|
||||
const checkItem = (itemId: ItemId) => {
|
||||
assert.match(itemId, /^[a-zA-Z0-9]{32}$/, 'item IDs should be 32 character alphanumeric strings');
|
||||
|
||||
const item = this.idToItem_.get(itemId);
|
||||
assert.ok(!!item, `should find item with ID ${itemId}`);
|
||||
|
||||
if (item.parentId) {
|
||||
const parent = this.idToItem_.get(item.parentId);
|
||||
assert.ok(parent, `should find parent (id: ${item.parentId})`);
|
||||
|
||||
assert.ok(isFolder(parent), 'parent should be a folder');
|
||||
assert.ok(parent.childIds.includes(itemId), 'parent should include the current item in its children');
|
||||
}
|
||||
checkParentId(item);
|
||||
|
||||
if (isFolder(item)) {
|
||||
for (const childId of item.childIds) {
|
||||
checkItem(childId);
|
||||
}
|
||||
|
||||
// Shared folders
|
||||
assert.ok(item.ownedByEmail, 'all folders should have a "shareOwner" property (even if not shared)');
|
||||
if (item.isRootSharedItem) {
|
||||
assert.equal(item.parentId, '', 'only toplevel folders should be shared');
|
||||
}
|
||||
for (const sharedWith of item.shareRecipients) {
|
||||
assert.ok(this.tree_.has(sharedWith), 'all sharee users should exist');
|
||||
}
|
||||
// isSharedWith is only valid for toplevel folders
|
||||
if (item.parentId === '') {
|
||||
assert.ok(!item.isSharedWith(item.ownedByEmail), 'the share owner should not be in an item\'s sharedWith list');
|
||||
}
|
||||
|
||||
// Uniqueness
|
||||
assert.equal(
|
||||
item.childIds.length,
|
||||
[...new Set(item.childIds)].length,
|
||||
'child IDs should be unique',
|
||||
);
|
||||
checkFolder(item);
|
||||
} else if (isNote(item)) {
|
||||
checkNote(item);
|
||||
} else {
|
||||
checkResource(item);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -263,6 +292,8 @@ class ActionTracker {
|
||||
|
||||
removeItemRecursive(childId);
|
||||
}
|
||||
} else if (isNote(item)) {
|
||||
updateResourceReferences(item, { ...item, body: '' });
|
||||
}
|
||||
};
|
||||
const mapItems = <T> (map: (item: TreeItem)=> T, startFolder?: FolderRecord) => {
|
||||
@@ -282,6 +313,16 @@ class ActionTracker {
|
||||
workList.push(childId);
|
||||
}
|
||||
}
|
||||
if (isNote(item)) {
|
||||
// Map linked resources
|
||||
const linkedIds = extractResourceUrls(item.body);
|
||||
for (const id of linkedIds) {
|
||||
const item = this.idToItem_.get(id.itemId);
|
||||
if (!item || !isResource(item)) continue;
|
||||
|
||||
result.push(map(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -342,6 +383,50 @@ class ActionTracker {
|
||||
this.checkRep_();
|
||||
};
|
||||
|
||||
const updateResourceReferences = (noteBefore: NoteData|null, noteAfter: NoteData|null) => {
|
||||
assert.ok(!!noteBefore || !!noteAfter, 'at least one of (noteBefore, noteAfter) must be specified');
|
||||
if (noteBefore && noteAfter) {
|
||||
assert.equal(noteBefore.id, noteAfter.id, 'changing note IDs is not supported');
|
||||
}
|
||||
|
||||
const bodyBefore = noteBefore?.body ?? '';
|
||||
const bodyAfter = noteAfter?.body ?? '';
|
||||
if (bodyBefore === bodyAfter) return;
|
||||
|
||||
const id = noteBefore?.id ?? noteAfter?.id;
|
||||
|
||||
const referencesBefore = extractResourceUrls(bodyBefore).map(r => r.itemId);
|
||||
const referencesAfter = extractResourceUrls(bodyAfter).map(r => r.itemId);
|
||||
|
||||
const newReferences = new Set(referencesAfter);
|
||||
for (const reference of referencesBefore) {
|
||||
newReferences.delete(reference);
|
||||
}
|
||||
|
||||
const removedReferences = new Set(referencesBefore);
|
||||
for (const reference of referencesAfter) {
|
||||
removedReferences.delete(reference);
|
||||
}
|
||||
|
||||
for (const reference of newReferences) {
|
||||
const item = this.idToItem_.get(reference);
|
||||
if (item && isResource(item)) {
|
||||
updateItem(item.id, item.withReference(id), `referenced by ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const reference of removedReferences) {
|
||||
const item = this.idToItem_.get(reference);
|
||||
if (item && isResource(item)) {
|
||||
updateItem(
|
||||
item.id,
|
||||
item.withoutReference(id),
|
||||
`dereferenced by ${id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tracker: ActionableClient = {
|
||||
createNote: (data: NoteData) => {
|
||||
assertWriteable(data.parentId);
|
||||
@@ -350,8 +435,9 @@ class ActionTracker {
|
||||
assert.ok(!this.idToItem_.has(data.id), `note ${data.id} should not yet exist`);
|
||||
updateItem(data.id, {
|
||||
...data,
|
||||
}, 'created');
|
||||
}, `created in ${data.parentId}`);
|
||||
addChild(data.parentId, data.id);
|
||||
updateResourceReferences(null, data);
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
@@ -364,7 +450,7 @@ class ActionTracker {
|
||||
assert.ok(!!data.parentId, `note ${data.id} should have a parentId`);
|
||||
|
||||
// Additional debugging information about what changed:
|
||||
const changedFieldsInfo = Object.entries(data)
|
||||
const changedFields = Object.entries(data)
|
||||
.filter(([key, newValue]) => {
|
||||
const itemKey = key as keyof NoteData;
|
||||
// isShared is a virtual property
|
||||
@@ -378,12 +464,39 @@ class ActionTracker {
|
||||
removeChild(oldItem.parentId, data.id);
|
||||
updateItem(data.id, {
|
||||
...data,
|
||||
}, `updated (changed fields: ${JSON.stringify(changedFieldsInfo)})`);
|
||||
}, `updated (changed fields: ${JSON.stringify(changedFields)})`);
|
||||
addChild(data.parentId, data.id);
|
||||
updateResourceReferences(oldItem, data);
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
},
|
||||
attachResource: async (note, resource) => {
|
||||
const resourceMarkup = `[resource](:/${resource.id})`;
|
||||
const withAttached = { ...note, body: `${note.body}${resourceMarkup}` };
|
||||
|
||||
if (!tracker.itemExists(resource.id)) {
|
||||
await tracker.createResource(resource);
|
||||
}
|
||||
await tracker.updateNote(withAttached);
|
||||
return withAttached;
|
||||
},
|
||||
createResource: async (resource) => {
|
||||
if (tracker.itemExists(resource.id)) {
|
||||
// Don't double-create the item.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
updateItem(
|
||||
resource.id, new ResourceRecord({
|
||||
...resource,
|
||||
referencedBy: [],
|
||||
}),
|
||||
'created',
|
||||
);
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
},
|
||||
createFolder: (data: FolderData) => {
|
||||
const parentId = data.parentId ?? '';
|
||||
assertWriteable(parentId);
|
||||
@@ -395,7 +508,7 @@ class ActionTracker {
|
||||
sharedWith: [],
|
||||
ownedByEmail: clientId,
|
||||
isShared: false,
|
||||
}), 'created');
|
||||
}), `created ${data.parentId ? `in ${data.parentId}` : '(toplevel)'}`);
|
||||
addChild(data.parentId, data.id);
|
||||
|
||||
this.checkRep_();
|
||||
@@ -419,7 +532,7 @@ class ActionTracker {
|
||||
|
||||
const item = this.idToItem_.get(id);
|
||||
if (!item) throw new Error(`Not found ${id}`);
|
||||
assert.ok(!isFolder(item), 'should be a note');
|
||||
assert.ok(isNote(item), 'should be a note');
|
||||
assertWriteable(item);
|
||||
|
||||
removeItemRecursive(id);
|
||||
@@ -480,6 +593,7 @@ class ActionTracker {
|
||||
},
|
||||
moveItem: (itemId, newParentId) => {
|
||||
const item = this.idToItem_.get(itemId);
|
||||
assert.ok(isFolder(item) || isNote(item), `item with ${itemId} should be a folder or a note`);
|
||||
|
||||
const validateParameters = () => {
|
||||
assert.ok(item, `item with ${itemId} should exist`);
|
||||
@@ -514,10 +628,9 @@ class ActionTracker {
|
||||
publishNote: (id) => {
|
||||
const oldItem = this.idToItem_.get(id);
|
||||
assert.ok(oldItem, 'should exist');
|
||||
assert.ok(!isFolder(oldItem), 'folders cannot be published');
|
||||
assert.ok(isNote(oldItem), 'only notes can be published');
|
||||
assert.ok(!oldItem.published, 'should not be published');
|
||||
|
||||
|
||||
updateItem(id, {
|
||||
...oldItem,
|
||||
published: true,
|
||||
@@ -529,7 +642,7 @@ class ActionTracker {
|
||||
unpublishNote: (id) => {
|
||||
const oldItem = this.idToItem_.get(id);
|
||||
assert.ok(oldItem, 'should exist');
|
||||
assert.ok(!isFolder(oldItem), 'folders cannot be unpublished');
|
||||
assert.ok(isNote(oldItem), 'only notes can be unpublished');
|
||||
assert.ok(oldItem.published, 'should be published');
|
||||
|
||||
updateItem(id, {
|
||||
@@ -541,9 +654,15 @@ class ActionTracker {
|
||||
return Promise.resolve();
|
||||
},
|
||||
sync: () => Promise.resolve(),
|
||||
listResources: () => {
|
||||
const items = mapItems(item => {
|
||||
return !isResource(item) ? null : item;
|
||||
}).filter(item => !!item && item.referenceCount > 0);
|
||||
return Promise.resolve(items);
|
||||
},
|
||||
listNotes: () => {
|
||||
const notes = mapItems(item => {
|
||||
return isFolder(item) ? null : item;
|
||||
return !isNote(item) ? null : item;
|
||||
}).filter(item => !!item).map(item => ({
|
||||
...item,
|
||||
isShared: isShared(item),
|
||||
@@ -597,8 +716,11 @@ class ActionTracker {
|
||||
|
||||
return folders.length ? this.context_.randomFrom(folders) : null;
|
||||
},
|
||||
randomNote: async () => {
|
||||
const notes = await tracker.listNotes();
|
||||
randomNote: async (options) => {
|
||||
let notes = await tracker.listNotes();
|
||||
if (!options.includeReadOnly) {
|
||||
notes = notes.filter(note => !isReadOnly(note.id));
|
||||
}
|
||||
const noteIndex = this.context_.randInt(0, notes.length);
|
||||
return notes.length ? notes[noteIndex] : null;
|
||||
},
|
||||
@@ -609,6 +731,18 @@ class ActionTracker {
|
||||
if (!item) throw new Error(`No item found with ID ${id}`);
|
||||
return item;
|
||||
},
|
||||
itemExists: (id: ItemId) => {
|
||||
const item = this.idToItem_.get(id);
|
||||
if (!item) return false;
|
||||
if (isResource(item)) return true;
|
||||
|
||||
const root = this.getToplevelParent_(id);
|
||||
if (isFolder(root)) {
|
||||
return root.ownedByEmail === client.email || root.isSharedWith(client.email);
|
||||
}
|
||||
|
||||
return this.tree_.get(clientId).childIds.includes(id);
|
||||
},
|
||||
};
|
||||
return tracker;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import uuid, { createSecureRandom } from '@joplin/lib/uuid';
|
||||
import { ActionableClient, FolderData, FuzzContext, HttpMethod, ItemId, Json, NoteData, RandomFolderOptions, RandomNoteOptions, ShareOptions } from './types';
|
||||
import { ActionableClient, assertIsNote, FolderData, FuzzContext, HttpMethod, ItemId, Json, NoteData, RandomFolderOptions, RandomNoteOptions, ResourceData, ShareOptions } from './types';
|
||||
import { join } from 'path';
|
||||
import { mkdir, remove } from 'fs-extra';
|
||||
import getStringProperty from './utils/getStringProperty';
|
||||
@@ -14,7 +14,6 @@ import getNumberProperty from './utils/getNumberProperty';
|
||||
import retryWithCount from './utils/retryWithCount';
|
||||
import resolvePathWithinDir from '@joplin/lib/utils/resolvePathWithinDir';
|
||||
import { formatMsToDateTimeLocal, msleep, Second } from '@joplin/utils/time';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { spawn } from 'child_process';
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
import { createInterface } from 'readline/promises';
|
||||
@@ -23,6 +22,9 @@ import ProgressBar from './utils/ProgressBar';
|
||||
import logDiffDebug from './utils/logDiffDebug';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import diffSortedStringArrays from './utils/diffSortedStringArrays';
|
||||
import extractResourceIds from './utils/extractResourceIds';
|
||||
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
||||
import hangingIndent from './utils/hangingIndent';
|
||||
|
||||
const logger = Logger.create('Client');
|
||||
|
||||
@@ -105,6 +107,12 @@ interface CreateRandomItemOptions extends CreateOrUpdateOptions {
|
||||
quiet?: boolean;
|
||||
}
|
||||
|
||||
class ApiResponseError extends Error {
|
||||
public constructor(public readonly code: number, message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
class Client implements ActionableClient {
|
||||
public readonly email: string;
|
||||
|
||||
@@ -122,7 +130,7 @@ class Client implements ActionableClient {
|
||||
}
|
||||
|
||||
private static async fromAccount(account: AccountData, actionTracker: ActionTracker, context: FuzzContext) {
|
||||
const id = uuid.create();
|
||||
const id = context.randomId();
|
||||
const profileDirectory = join(context.baseDir, id);
|
||||
await mkdir(profileDirectory);
|
||||
|
||||
@@ -242,6 +250,14 @@ class Client implements ActionableClient {
|
||||
|
||||
private closed_ = false;
|
||||
public async close() {
|
||||
if (this.closed_) {
|
||||
// This can happen if:
|
||||
// - Multiple cleanup callbacks are registered for the client.
|
||||
// - The client was manually closed, but also has a cleanup callback registered.
|
||||
logger.info('Client', this.clientLabel_, 'already closed. Skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
assert.ok(!this.closed_, 'should not be closed');
|
||||
|
||||
await this.account_.onClientDisconnected();
|
||||
@@ -388,22 +404,24 @@ class Client implements ActionableClient {
|
||||
// eslint-disable-next-line no-dupe-class-members -- This is not a duplicate class member
|
||||
private async execApiCommand_(method: 'GET', route: string): Promise<string>;
|
||||
// eslint-disable-next-line no-dupe-class-members -- This is not a duplicate class member
|
||||
private async execApiCommand_(method: 'POST'|'PUT', route: string, data: Json): Promise<string>;
|
||||
private async execApiCommand_(method: 'POST'|'PUT', route: string, data: Json|FormData): Promise<string>;
|
||||
// eslint-disable-next-line no-dupe-class-members -- This is not a duplicate class member
|
||||
private async execApiCommand_(method: HttpMethod, route: string, data: Json|null = null): Promise<string> {
|
||||
private async execApiCommand_(method: HttpMethod, route: string, data: Json|FormData|null = null): Promise<string> {
|
||||
route = route.replace(/^[/]/, '');
|
||||
const url = new URL(`http://localhost:${this.apiData_.port}/${route}`);
|
||||
url.searchParams.append('token', this.apiData_.token);
|
||||
|
||||
this.transcript_.push(`\n[[${method} ${url}; body: ${JSON.stringify(data)}]]\n`);
|
||||
|
||||
const response = await shim.fetch(url.toString(), {
|
||||
const response = await fetch(url.toString(), {
|
||||
method,
|
||||
...(data ? { body: JSON.stringify(data) } : undefined),
|
||||
...(data ? {
|
||||
body: data instanceof FormData ? data : JSON.stringify(data),
|
||||
} : undefined),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request to ${route} failed with error: ${await response.text()}`);
|
||||
throw new ApiResponseError(response.status, `Request to ${route} failed with error: ${await response.text()}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
@@ -468,11 +486,114 @@ class Client implements ActionableClient {
|
||||
// Certain sync failures self-resolve after a background task is allowed to
|
||||
// run. Delay:
|
||||
delayOnFailure: retry => retry * Second * 2,
|
||||
onFail: async (error) => {
|
||||
onFail: async ({ error, willRetry }) => {
|
||||
logger.debug('Sync error: ', error);
|
||||
logger.info('Sync failed. Retrying...');
|
||||
if (willRetry) {
|
||||
logger.info('Sync failed. Retrying...');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await this.handleResourceIdChanges_();
|
||||
}
|
||||
|
||||
// Joplin occasionally changes the ID of a resource. Handle this here.
|
||||
// Assumes that the client is up-to-date with the server.
|
||||
private async handleResourceIdChanges_() {
|
||||
type UntrackedAttachment = {
|
||||
id: ItemId;
|
||||
linkedNotes: Set<ItemId>;
|
||||
};
|
||||
const collectUntrackedAttachments = async () => {
|
||||
// Maps from untracked item IDs to the notes that contain that item.
|
||||
const untrackedItemsById = new Map<ItemId, UntrackedAttachment>();
|
||||
const noteActualStates = new Map<ItemId, NoteData>();
|
||||
for (const note of await this.listNotes()) {
|
||||
// Skip notes that are not yet in the expected state. It's possible
|
||||
// that these notes still need to be synced by another client. If so,
|
||||
// attachments in these notes will be processed later:
|
||||
if (!this.tracker_.itemExists(note.id)) continue;
|
||||
|
||||
for (const itemId of extractResourceIds(note.body)) {
|
||||
if (this.tracker_.itemExists(itemId)) continue;
|
||||
|
||||
const noteIds = untrackedItemsById.get(itemId);
|
||||
if (noteIds) {
|
||||
noteIds.linkedNotes.add(note.id);
|
||||
} else {
|
||||
untrackedItemsById.set(itemId, {
|
||||
id: itemId,
|
||||
linkedNotes: new Set([note.id]),
|
||||
});
|
||||
}
|
||||
noteActualStates.set(note.id, note);
|
||||
}
|
||||
}
|
||||
|
||||
return { untrackedItemsById, noteActualStates };
|
||||
};
|
||||
|
||||
const fetchResourceData = async (resourceId: ItemId) => {
|
||||
try {
|
||||
const resourceJson = JSON.parse(
|
||||
await this.execApiCommand_('GET', `/resources/${resourceId}?fields=id,title,mime`),
|
||||
);
|
||||
const resourceData: ResourceData = {
|
||||
id: getStringProperty(resourceJson, 'id'),
|
||||
mimeType: getStringProperty(resourceJson, 'mime'),
|
||||
title: getStringProperty(resourceJson, 'title'),
|
||||
};
|
||||
return resourceData;
|
||||
} catch (error) {
|
||||
if (error instanceof ApiResponseError && error.code === 404) {
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeResourceIds = (text: string) => {
|
||||
for (const id of extractResourceIds(text)) {
|
||||
text = text.split(id).join('');
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const textsMatchIgnoringResources = (actual: string, expected: string) => {
|
||||
return removeResourceIds(expected) === removeResourceIds(actual);
|
||||
};
|
||||
|
||||
const { untrackedItemsById, noteActualStates } = await collectUntrackedAttachments();
|
||||
|
||||
for (const { id: resourceId, linkedNotes } of untrackedItemsById.values()) {
|
||||
const resourceData = await fetchResourceData(resourceId);
|
||||
if (!resourceData) {
|
||||
logger.warn('Resource not found:', resourceId);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.createResource(resourceData);
|
||||
for (const id of linkedNotes) {
|
||||
const expected = this.tracker_.itemById(id);
|
||||
assertIsNote(expected);
|
||||
const actual = noteActualStates.get(id);
|
||||
assertIsNote(actual);
|
||||
|
||||
if (textsMatchIgnoringResources(actual.body, expected.body)) {
|
||||
const firstMatchIndex = actual.body.indexOf(resourceId);
|
||||
// This relies on the fact that **all** resource IDs are length-32 strings:
|
||||
const originalId = expected.body.substring(firstMatchIndex, firstMatchIndex + 32);
|
||||
|
||||
logger.info('Resource rewrite: Updating note', id, ': Replacing', originalId, 'with', resourceId);
|
||||
|
||||
await this.tracker_.updateNote({
|
||||
...expected,
|
||||
body: expected.body.split(originalId).join(resourceId),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async createOrUpdateMany(actionCount: number) {
|
||||
@@ -530,7 +651,7 @@ class Client implements ActionableClient {
|
||||
const titleLength = this.context_.randInt(1, 128);
|
||||
const folder = {
|
||||
parentId: parentId,
|
||||
id: id ?? uuid.create(),
|
||||
id: id ?? this.context_.randomId(),
|
||||
title: this.context_.randomString(titleLength).replace(/\n/g, ' '),
|
||||
};
|
||||
|
||||
@@ -609,7 +730,7 @@ class Client implements ActionableClient {
|
||||
parentId,
|
||||
title: this.context_.randomString(titleLength),
|
||||
body: this.context_.randomString(bodyLength),
|
||||
id: id ?? uuid.create(),
|
||||
id: id ?? this.context_.randomId(),
|
||||
}, { quiet });
|
||||
}
|
||||
|
||||
@@ -652,6 +773,52 @@ class Client implements ActionableClient {
|
||||
await this.execCliCommand_('rmnote', '--permanent', '--force', id);
|
||||
}
|
||||
|
||||
public async attachResource(note: NoteData, resource: ResourceData): Promise<NoteData> {
|
||||
logger.info('Attach resource', resource.id, 'to note', note.id);
|
||||
const updatedNote = await this.tracker_.attachResource(note, resource);
|
||||
|
||||
await this.execApiCommand_('PUT', `/notes/${encodeURIComponent(note.id)}`, {
|
||||
title: updatedNote.title,
|
||||
body: updatedNote.body,
|
||||
parent_id: updatedNote.parentId ?? '',
|
||||
});
|
||||
|
||||
// Create the resource on the client *after* attaching it to the note so that the
|
||||
// resource is always referenced by at least one note:
|
||||
await this.createResource(resource);
|
||||
|
||||
await this.assertNoteMatchesState_(updatedNote);
|
||||
return updatedNote;
|
||||
}
|
||||
|
||||
public async createResource(resource: ResourceData): Promise<void> {
|
||||
await this.tracker_.createResource(resource);
|
||||
|
||||
const checkExists = async () => {
|
||||
try {
|
||||
await this.execApiCommand_('GET', `/resources/${resource.id}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof ApiResponseError && error.code === 404) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
if (!await checkExists()) {
|
||||
const resourceForm = new FormData();
|
||||
resourceForm.append('data', new Blob(['test'], { type: resource.mimeType }));
|
||||
resourceForm.append('props', JSON.stringify({
|
||||
title: resource.title,
|
||||
id: resource.id,
|
||||
mime: resource.mimeType,
|
||||
}));
|
||||
|
||||
await this.execApiCommand_('POST', '/resources', resourceForm);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteFolder(id: string) {
|
||||
logger.info('Delete folder', id, 'in', this.label);
|
||||
await this.tracker_.deleteFolder(id);
|
||||
@@ -698,8 +865,8 @@ class Client implements ActionableClient {
|
||||
}, {
|
||||
count: 2,
|
||||
delayOnFailure: count => count * Second,
|
||||
onFail: (error)=>{
|
||||
logger.warn('Share failed:', error);
|
||||
onFail: ({ error, willRetry })=>{
|
||||
logger.warn('Share failed:', error, willRetry ? 'Retrying...' : '');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -755,6 +922,24 @@ class Client implements ActionableClient {
|
||||
await this.execCliCommand_('mv', itemId, movingToRoot ? 'root' : newParentId);
|
||||
}
|
||||
|
||||
public async listResources() {
|
||||
const params = {
|
||||
fields: 'id,title,mime',
|
||||
include_deleted: '1',
|
||||
include_conflicts: '1',
|
||||
};
|
||||
return await this.execPagedApiCommand_(
|
||||
'GET',
|
||||
'/resources',
|
||||
params,
|
||||
(item): ResourceData => ({
|
||||
id: getStringProperty(item, 'id'),
|
||||
title: getStringProperty(item, 'title'),
|
||||
mimeType: getStringProperty(item, 'mime'),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async listNotes() {
|
||||
const params = {
|
||||
fields: 'id,parent_id,body,title,is_conflict,conflict_original_id,share_id,is_shared',
|
||||
@@ -812,10 +997,14 @@ class Client implements ActionableClient {
|
||||
return this.tracker_.itemById(itemId);
|
||||
}
|
||||
|
||||
public itemExists(itemId: ItemId) {
|
||||
return this.tracker_.itemExists(itemId);
|
||||
}
|
||||
|
||||
public async checkState() {
|
||||
logger.info('Check state', this.label);
|
||||
|
||||
type ItemSlice = { id: string };
|
||||
type ItemSlice = { id: string; title: string };
|
||||
const compare = (a: ItemSlice, b: ItemSlice) => {
|
||||
if (a.id === b.id) return 0;
|
||||
return a.id < b.id ? -1 : 1;
|
||||
@@ -833,32 +1022,46 @@ class Client implements ActionableClient {
|
||||
}
|
||||
};
|
||||
|
||||
const assertSameIds = (actualSorted: ItemSlice[], expectedSorted: ItemSlice[], testLabel: string) => {
|
||||
const idLogs = (ids: ItemId[], items: ItemSlice[]) => {
|
||||
const itemTitle = (id: ItemId) => {
|
||||
const itemTitle = items.find(item => item.id === id)?.title;
|
||||
return itemTitle ? JSON.stringify(substrWithEllipsis(itemTitle, 0, 28)) : 'Unknown';
|
||||
};
|
||||
|
||||
const output = [];
|
||||
for (const id of ids) {
|
||||
const log = this.globalActionTracker_.getActionLog(id);
|
||||
|
||||
output.push(`id: ${id} (${itemTitle(id)})`);
|
||||
if (log.length > 0) {
|
||||
output.push(
|
||||
log
|
||||
.map(item => `\t${item.source}: ${item.action}`)
|
||||
.join('\n'),
|
||||
);
|
||||
} else {
|
||||
output.push('\tNo history found');
|
||||
}
|
||||
}
|
||||
return output.join('\n');
|
||||
};
|
||||
|
||||
const assertSameIds = async (actualSorted: ItemSlice[], expectedSorted: ItemSlice[], assertionLabel: 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',
|
||||
const message = [
|
||||
`${assertionLabel}: IDs were different:`,
|
||||
missing.length && `Expected ${JSON.stringify(missing)} to be present, but were missing.`,
|
||||
unexpected.length && `Present but should not have been: ${JSON.stringify(unexpected)}`,
|
||||
'Logs:',
|
||||
idLogs(missing),
|
||||
idLogs(unexpected),
|
||||
].filter(line => !!line).join('\n'));
|
||||
idLogs(missing, expectedSorted),
|
||||
idLogs(unexpected, actualSorted),
|
||||
].filter(line => !!line).join('\n');
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -871,7 +1074,7 @@ class Client implements ActionableClient {
|
||||
|
||||
assertNoAdjacentEqualIds(notes, 'notes');
|
||||
assertNoAdjacentEqualIds(expectedNotes, 'expectedNotes');
|
||||
assertSameIds(notes, expectedNotes, 'should have the same note IDs');
|
||||
await assertSameIds(notes, expectedNotes, 'Note IDs should match');
|
||||
assert.deepEqual(notes, expectedNotes, 'should have the same notes as the expected state');
|
||||
};
|
||||
|
||||
@@ -884,12 +1087,49 @@ class Client implements ActionableClient {
|
||||
|
||||
assertNoAdjacentEqualIds(folders, 'folders');
|
||||
assertNoAdjacentEqualIds(expectedFolders, 'expectedFolders');
|
||||
assertSameIds(folders, expectedFolders, 'should have the same folder IDs');
|
||||
await assertSameIds(folders, expectedFolders, 'Folder IDs should match');
|
||||
assert.deepEqual(folders, expectedFolders, 'should have the same folders as the expected state');
|
||||
};
|
||||
|
||||
await checkNoteState();
|
||||
await checkFolderState();
|
||||
const checkResourceState = async () => {
|
||||
const actualResources = [...await this.listResources()];
|
||||
const actualResourceIds = new Set(actualResources.map(r => r.id));
|
||||
const expectedResources = [...await this.tracker_.listResources()];
|
||||
|
||||
const missingResources = [];
|
||||
for (const resource of expectedResources) {
|
||||
if (!actualResourceIds.has(resource.id)) {
|
||||
missingResources.push(resource.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingResources.length > 0) {
|
||||
const log = idLogs(missingResources, expectedResources);
|
||||
|
||||
throw new Error(`Missing resource(s): All expected resources should exist on the client. Resource(s) with ID(s) ${JSON.stringify(missingResources)} were not found (total resource count: ${actualResourceIds.size}).\nResource action history:\n${log}`);
|
||||
}
|
||||
};
|
||||
|
||||
const errors: Error[] = [];
|
||||
const runCheck = async (check: ()=> Promise<void>) => {
|
||||
try {
|
||||
await check();
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
};
|
||||
|
||||
await runCheck(checkResourceState);
|
||||
await runCheck(checkNoteState);
|
||||
await runCheck(checkFolderState);
|
||||
|
||||
if (errors.length) {
|
||||
const errorList = errors
|
||||
.map((error, index) => `Error ${index + 1} of ${errors.length}: ${error}`)
|
||||
.map(message => hangingIndent(message))
|
||||
.join('\n');
|
||||
throw new Error(`Incorrect state in client: ${this.clientLabel_}:\n${errorList}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ const validateId = (id: string) => {
|
||||
};
|
||||
|
||||
export default class FolderRecord implements FolderData {
|
||||
public readonly parentId: string;
|
||||
public readonly id: string;
|
||||
public readonly parentId: ItemId;
|
||||
public readonly id: ItemId;
|
||||
public readonly title: string;
|
||||
public readonly ownedByEmail: string;
|
||||
public readonly childIds: ItemId[];
|
||||
|
||||
49
packages/tools/fuzzer/model/ResourceRecord.ts
Normal file
49
packages/tools/fuzzer/model/ResourceRecord.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ItemId, ResourceData } from '../types';
|
||||
|
||||
interface InitializationOptions extends ResourceData {
|
||||
referencedBy: ItemId[];
|
||||
}
|
||||
|
||||
export default class ResourceRecord implements ResourceData {
|
||||
public readonly parentId: undefined;
|
||||
public readonly id: ItemId;
|
||||
public readonly title: string;
|
||||
public readonly mimeType: string;
|
||||
public readonly referencedBy: readonly ItemId[] = [];
|
||||
|
||||
public constructor(options: InitializationOptions) {
|
||||
this.id = options.id;
|
||||
this.title = options.title;
|
||||
this.mimeType = options.mimeType;
|
||||
this.referencedBy = [...options.referencedBy];
|
||||
}
|
||||
|
||||
public get referenceCount() {
|
||||
return this.referencedBy.length;
|
||||
}
|
||||
|
||||
public withReference(noteId: ItemId) {
|
||||
if (this.referencedBy.includes(noteId)) {
|
||||
return this;
|
||||
}
|
||||
return new ResourceRecord({
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
mimeType: this.mimeType,
|
||||
referencedBy: [...this.referencedBy, noteId],
|
||||
});
|
||||
}
|
||||
|
||||
public withoutReference(noteId: ItemId) {
|
||||
if (this.referencedBy.includes(noteId)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return new ResourceRecord({
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
mimeType: this.mimeType,
|
||||
referencedBy: this.referencedBy.filter(ref => ref !== noteId),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { packagesDir } from './constants';
|
||||
import ActionRunner, { ActionSpec } from './ActionRunner';
|
||||
import randomString from './utils/randomString';
|
||||
import { readFile } from 'fs/promises';
|
||||
import randomId from './utils/randomId';
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node');
|
||||
|
||||
const globalLogger = new Logger();
|
||||
@@ -55,9 +56,10 @@ interface Options {
|
||||
|
||||
const createContext = (options: Options, server: Server, profilesDirectory: string) => {
|
||||
const random = new SeededRandom(options.seed);
|
||||
// Use a separate random number generator for strings. This prevents
|
||||
// Use a separate random number generator for strings and IDs. This prevents
|
||||
// the random strings setting from affecting the other output.
|
||||
const stringRandom = new SeededRandom(random.next());
|
||||
const idRandom = new SeededRandom(random.next());
|
||||
|
||||
if (options.isJoplinCloud) {
|
||||
logger.info('Sync target: Joplin Cloud');
|
||||
@@ -71,6 +73,7 @@ const createContext = (options: Options, server: Server, profilesDirectory: stri
|
||||
return (_targetLength: number) => `Placeholder (x${stringCount++})`;
|
||||
}
|
||||
})();
|
||||
const randomIdGenerator = randomId((min, max) => idRandom.nextInRange(min, max));
|
||||
|
||||
const fuzzContext: FuzzContext = {
|
||||
serverUrl: server.url,
|
||||
@@ -82,6 +85,7 @@ const createContext = (options: Options, server: Server, profilesDirectory: stri
|
||||
randInt: (a, b) => random.nextInRange(a, b),
|
||||
randomFrom: (data) => data[random.nextInRange(0, data.length)],
|
||||
randomString: randomStringGenerator,
|
||||
randomId: randomIdGenerator,
|
||||
keepAccounts: options.keepAccountsOnClose,
|
||||
};
|
||||
return fuzzContext;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type Client from './Client';
|
||||
import type FolderRecord from './model/FolderRecord';
|
||||
import ResourceRecord from './model/ResourceRecord';
|
||||
|
||||
export type Json = string|number|Json[]|{ [key: string]: Json };
|
||||
|
||||
@@ -25,12 +26,26 @@ export interface DetailedFolderData extends FolderData {
|
||||
isShared: boolean;
|
||||
}
|
||||
|
||||
export type TreeItem = NoteData|FolderRecord;
|
||||
export interface ResourceData {
|
||||
id: ItemId;
|
||||
title: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export type TreeItem = NoteData|FolderRecord|ResourceRecord;
|
||||
|
||||
export const isFolder = (item: TreeItem): item is FolderRecord => {
|
||||
return 'childIds' in item;
|
||||
};
|
||||
|
||||
export const isResource = (item: TreeItem): item is ResourceRecord => {
|
||||
return 'mimeType' in item;
|
||||
};
|
||||
|
||||
export const isNote = (item: TreeItem): item is NoteData => {
|
||||
return !isFolder(item) && !isResource(item);
|
||||
};
|
||||
|
||||
// Typescript type assertions require type definitions on the left for arrow functions.
|
||||
// See https://github.com/microsoft/TypeScript/issues/53450.
|
||||
export const assertIsFolder: (item: TreeItem)=> asserts item is FolderRecord = item => {
|
||||
@@ -57,6 +72,7 @@ export interface FuzzContext {
|
||||
execApi: (method: HttpMethod, route: string, debugAction: Json)=> Promise<Json>;
|
||||
randInt: (low: number, high: number)=> number;
|
||||
randomString: (targetLength: number)=> string;
|
||||
randomId: ()=> string;
|
||||
randomFrom: <T> (data: T[])=> T;
|
||||
}
|
||||
|
||||
@@ -82,6 +98,8 @@ export interface ActionableClient {
|
||||
deleteNote(id: ItemId): Promise<void>;
|
||||
createNote(data: NoteData): Promise<void>;
|
||||
updateNote(data: NoteData): Promise<void>;
|
||||
attachResource(note: NoteData, resource: ResourceData): Promise<NoteData>;
|
||||
createResource(resource: ResourceData): Promise<void>;
|
||||
moveItem(itemId: ItemId, newParentId: ItemId): Promise<void>;
|
||||
publishNote(id: ItemId): Promise<void>;
|
||||
unpublishNote(id: ItemId): Promise<void>;
|
||||
@@ -89,10 +107,12 @@ export interface ActionableClient {
|
||||
|
||||
listNotes(): Promise<NoteData[]>;
|
||||
listFolders(): Promise<DetailedFolderData[]>;
|
||||
listResources(): Promise<ResourceData[]>;
|
||||
allFolderDescendants(parentId: ItemId): Promise<ItemId[]>;
|
||||
randomFolder(options: RandomFolderOptions): Promise<FolderRecord>;
|
||||
randomNote(options: RandomNoteOptions): Promise<NoteData>;
|
||||
itemById(id: ItemId): TreeItem;
|
||||
itemExists(id: ItemId): boolean;
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
|
||||
7
packages/tools/fuzzer/utils/extractResourceIds.ts
Normal file
7
packages/tools/fuzzer/utils/extractResourceIds.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { extractResourceUrls } from '@joplin/lib/urlUtils';
|
||||
|
||||
const extractResourceIds = (text: string) => {
|
||||
return extractResourceUrls(text).map(item => item.itemId);
|
||||
};
|
||||
|
||||
export default extractResourceIds;
|
||||
@@ -5,7 +5,9 @@ const getProperty = (object: unknown, propertyName: string) => {
|
||||
}
|
||||
|
||||
if (!(propertyName in object)) {
|
||||
throw new Error(`No such property ${JSON.stringify(propertyName)} in object`);
|
||||
throw new Error(
|
||||
`No such property ${JSON.stringify(propertyName)} in object. Available keys: (${JSON.stringify(Object.keys(object))})`,
|
||||
);
|
||||
}
|
||||
|
||||
return object[propertyName as keyof object];
|
||||
|
||||
7
packages/tools/fuzzer/utils/hangingIndent.ts
Normal file
7
packages/tools/fuzzer/utils/hangingIndent.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
// Hanging indent: Indents all lines after the first
|
||||
const hangingIndent = (text: string, indentation = ' ') => {
|
||||
return text.replace(/\n/g, `\n${indentation}`);
|
||||
};
|
||||
|
||||
export default hangingIndent;
|
||||
12
packages/tools/fuzzer/utils/randomId.test.ts
Normal file
12
packages/tools/fuzzer/utils/randomId.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import randomId from './randomId';
|
||||
|
||||
describe('randomId', () => {
|
||||
test('should generate a 32-character alphanumeric ID', () => {
|
||||
expect(
|
||||
randomId((_low, high) => high - 1)(),
|
||||
).toBe('ffffffffffffffffffffffffffffffff');
|
||||
expect(
|
||||
randomId((low, _high) => low)(),
|
||||
).toBe('00000000000000000000000000000000');
|
||||
});
|
||||
});
|
||||
16
packages/tools/fuzzer/utils/randomId.ts
Normal file
16
packages/tools/fuzzer/utils/randomId.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
type OnNextRandom = (lowInclusive: number, highExclusive: number)=> number;
|
||||
|
||||
const randomId = (nextRandomInteger: OnNextRandom)=> () => {
|
||||
const bytes = [];
|
||||
for (let i = 0; i < 16; i++) {
|
||||
bytes.push(nextRandomInteger(0, 256));
|
||||
}
|
||||
|
||||
return Buffer.from(bytes)
|
||||
.toString('hex')
|
||||
.toLowerCase()
|
||||
.padStart(32, '0');
|
||||
};
|
||||
|
||||
export default randomId;
|
||||
@@ -3,10 +3,15 @@ import { msleep } from '@joplin/utils/time';
|
||||
|
||||
const logger = Logger.create('retryWithCount');
|
||||
|
||||
interface FailureEvent {
|
||||
error: Error;
|
||||
willRetry: boolean;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
count: number;
|
||||
delayOnFailure?: (retryCount: number)=> number;
|
||||
onFail: (error: Error)=> void|Promise<void>;
|
||||
onFail: (event: FailureEvent)=> void|Promise<void>;
|
||||
}
|
||||
|
||||
const retryWithCount = async (task: ()=> Promise<void>, { count, delayOnFailure, onFail }: Options) => {
|
||||
@@ -15,10 +20,11 @@ const retryWithCount = async (task: ()=> Promise<void>, { count, delayOnFailure,
|
||||
try {
|
||||
return await task();
|
||||
} catch (error) {
|
||||
await onFail(error);
|
||||
lastError = error;
|
||||
|
||||
const willRetry = retry + 1 < count;
|
||||
await onFail({ error, willRetry });
|
||||
|
||||
const delay = willRetry && delayOnFailure ? delayOnFailure(retry + 1) : 0;
|
||||
if (delay) {
|
||||
logger.info(`Retrying after ${delay}ms...`);
|
||||
|
||||
@@ -7,7 +7,9 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: Christoph Eder\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Sebastian Aust <code@ryanthara.de>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -331,7 +333,7 @@ msgstr "A5"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1121
|
||||
msgid "ABC musical notation: Options"
|
||||
msgstr ""
|
||||
msgstr "ABC-Notenschrift: Optionen"
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx:62
|
||||
#: packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx:240
|
||||
@@ -821,7 +823,7 @@ msgstr "Beta"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:123
|
||||
msgid "Block code"
|
||||
msgstr ""
|
||||
msgstr "Blockcode"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:55
|
||||
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:80
|
||||
@@ -939,9 +941,8 @@ msgid "Cannot change encrypted item"
|
||||
msgstr "Kann verschlüsseltes Element nicht ändern"
|
||||
|
||||
#: packages/lib/commands/convertNoteToMarkdown.ts:42
|
||||
#, fuzzy
|
||||
msgid "Cannot convert read-only item: \"%s\""
|
||||
msgstr "Konnte neue Notiz nicht erstellen: %s"
|
||||
msgstr "Schreibgeschütztes Element kann nicht konvertiert werden: „%s“"
|
||||
|
||||
#: packages/lib/models/Note.ts:622
|
||||
msgid "Cannot copy note to \"%s\" notebook"
|
||||
@@ -1103,7 +1104,7 @@ msgstr "Überprüfen… Bitte warten."
|
||||
|
||||
#: packages/app-desktop/gui/NoteContentPropertiesDialog.tsx:114
|
||||
msgid "Chinese/Japanese/Korean characters"
|
||||
msgstr ""
|
||||
msgstr "Chinesische/Japanische/Koreanische Schriftzeichen"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note/commands/attachFile.ts:98
|
||||
msgid "Choose an option"
|
||||
@@ -1270,7 +1271,7 @@ msgstr "Befehl"
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:30
|
||||
msgid "COMMAND"
|
||||
msgstr ""
|
||||
msgstr "COMMAND"
|
||||
|
||||
#: packages/app-desktop/plugins/GotoAnything.tsx:783
|
||||
msgid "Command palette"
|
||||
@@ -1334,9 +1335,8 @@ msgid "Configuration"
|
||||
msgstr "Konfiguration"
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:24
|
||||
#, fuzzy
|
||||
msgid "Configured keyboard shortcuts:"
|
||||
msgstr "Tastaturkürzel"
|
||||
msgstr "Konfigurierte Tastaturkürzel:"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1296
|
||||
msgid "Configures the size of scrollbars used in the app."
|
||||
@@ -1412,9 +1412,8 @@ msgid "Convert it"
|
||||
msgstr "Umwandeln"
|
||||
|
||||
#: packages/lib/commands/convertNoteToMarkdown.ts:18
|
||||
#, fuzzy
|
||||
msgid "Convert to Markdown"
|
||||
msgstr "Notiz in Markdown umwandeln"
|
||||
msgstr "In Markdown umwandeln"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note/Note.tsx:1350
|
||||
msgid "Convert to note"
|
||||
@@ -1521,9 +1520,8 @@ msgid "Could not connect to plugin repository."
|
||||
msgstr "Konnte keine Verbindung zum Plugin-Repository herstellen."
|
||||
|
||||
#: packages/lib/commands/convertNoteToMarkdown.ts:70
|
||||
#, fuzzy
|
||||
msgid "Could not convert notes to Markdown: %s"
|
||||
msgstr "Konnte Notiz nicht in Markdown umwandeln: %s"
|
||||
msgstr "Notizen konnten nicht in Markdown konvertiert werden: %s"
|
||||
|
||||
#: packages/app-desktop/InteropServiceHelper.ts:235
|
||||
msgid "Could not export notes: %s"
|
||||
@@ -1837,13 +1835,14 @@ msgid "Delete profile \"%s\""
|
||||
msgstr "Lösche Profil „%s“"
|
||||
|
||||
#: 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 ""
|
||||
"Alle Daten, inklusive Notizen, Notizbücher und Schlagwörter werden dauerhaft "
|
||||
"Profil „%s“ löschen?\n"
|
||||
"\n"
|
||||
"Alle Daten, einschließlich Notizen, Notizbücher und Tags, werden dauerhaft "
|
||||
"gelöscht."
|
||||
|
||||
#: packages/editor/ProseMirror/plugins/tablePlugin.ts:36
|
||||
@@ -1861,6 +1860,10 @@ msgid ""
|
||||
"All notes associated with this tag will remain, but the tag will be removed "
|
||||
"from all notes."
|
||||
msgstr ""
|
||||
"Schlagworte „%s“ löschen?\n"
|
||||
"\n"
|
||||
"Alle mit diesem Tag verbundenen Notizen bleiben erhalten, aber das Tag wird "
|
||||
"aus allen Notizen entfernt."
|
||||
|
||||
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.ts:38
|
||||
#: packages/app-mobile/components/side-menu-content.tsx:414
|
||||
@@ -2045,7 +2048,7 @@ msgstr "Zeigt alle Informationen über die Notiz an."
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:14
|
||||
msgid "Displays the configured keyboard shortcuts."
|
||||
msgstr ""
|
||||
msgstr "Zeigt die konfigurierten Tastaturkürzel an."
|
||||
|
||||
#: packages/app-cli/app/command-cat.ts:14
|
||||
msgid "Displays the given note."
|
||||
@@ -2269,9 +2272,8 @@ msgid "Edit profile configuration..."
|
||||
msgstr "Profilkonfiguration bearbeiten..."
|
||||
|
||||
#: packages/app-mobile/components/screens/tags.tsx:64
|
||||
#, fuzzy
|
||||
msgid "Edit tag"
|
||||
msgstr "Notiz bearbeiten."
|
||||
msgstr "Schlagwort bearbeiten"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen.tsx:129
|
||||
#: packages/app-desktop/gui/NoteContentPropertiesDialog.tsx:151
|
||||
@@ -2382,9 +2384,8 @@ msgid "Enable abbreviation syntax"
|
||||
msgstr "Abkürzungssyntax aktivieren"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1093
|
||||
#, fuzzy
|
||||
msgid "Enable ABC musical notation support"
|
||||
msgstr "Fountain-Syntaxunterstützung aktivieren"
|
||||
msgstr "Unterstützung für ABC-Notenschrift aktivieren"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1095
|
||||
msgid "Enable audio player"
|
||||
@@ -3167,7 +3168,6 @@ msgid "Import or export your data"
|
||||
msgstr "Importiere oder Exportiere deine Daten"
|
||||
|
||||
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.ts:20
|
||||
#, fuzzy
|
||||
msgid "Import..."
|
||||
msgstr "Importieren..."
|
||||
|
||||
@@ -3569,7 +3569,7 @@ msgstr "Unterstützter Schlüsselbund: %s"
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:30
|
||||
msgid "KEYS"
|
||||
msgstr ""
|
||||
msgstr "KEYS"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:74
|
||||
msgid "Keys that need upgrading"
|
||||
@@ -3800,6 +3800,8 @@ msgid ""
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile "
|
||||
"cannot be deleted."
|
||||
msgstr ""
|
||||
"Verwalte deine Profile. Du kannst Profile umbenennen oder löschen. Das "
|
||||
"aktive Profil kann nicht gelöscht werden."
|
||||
|
||||
#. `generate-ppk`
|
||||
#: packages/app-cli/app/command-e2ee.ts:19
|
||||
@@ -3962,17 +3964,16 @@ msgstr[0] "%d Notiz in das Notizbuch „%s“ verschieben?"
|
||||
msgstr[1] "%d Notizen in das Notizbuch „%s“ verschieben?"
|
||||
|
||||
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.ts:34
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Move %d notebooks to the trash?\n"
|
||||
"\n"
|
||||
"All notes and sub-notebooks within these notebooks will also be moved to the "
|
||||
"trash."
|
||||
msgstr ""
|
||||
"Notizbuch „%s“ in den Papierkorb bewegen?\n"
|
||||
"%d Notizbücher in den Papierkorb verschieben?\n"
|
||||
"\n"
|
||||
"Alle Notizen und Unter-Notizbücher in diesem Notizbuch werden ebenfalls in "
|
||||
"den Papierkorb bewegt."
|
||||
"Alle Notizen und Unternotizbücher in diesen Notizbüchern werden ebenfalls in "
|
||||
"den Papierkorb verschoben."
|
||||
|
||||
#: packages/app-desktop/gui/ResizableLayout/MoveButtons.tsx:73
|
||||
msgid "Move down"
|
||||
@@ -4215,7 +4216,7 @@ msgstr "Keine Updates verfügbar"
|
||||
|
||||
#: packages/lib/components/shared/SamlShared.ts:12
|
||||
msgid "No URL for SAML authentication set."
|
||||
msgstr ""
|
||||
msgstr "Keine URL für SAML-Authentifizierung festgelegt."
|
||||
|
||||
#: packages/app-cli/app/command-share.ts:188
|
||||
#: packages/app-cli/app/command-share.ts:208
|
||||
@@ -4565,7 +4566,7 @@ msgstr "Sync-Assistent öffnen..."
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx:635
|
||||
msgid "Open-source licences"
|
||||
msgstr ""
|
||||
msgstr "Open-Source-Lizenzen"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:87
|
||||
msgid "Open..."
|
||||
@@ -4604,6 +4605,9 @@ 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 ""
|
||||
"Optionen, die beim Rendern von ABC-Code verwendet werden sollten. Es muss "
|
||||
"sich um ein JSON5-Objekt handeln. Die vollständige Liste der Optionen "
|
||||
"findest du unter: %s"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:103
|
||||
msgid "Ordered list"
|
||||
@@ -4979,9 +4983,8 @@ msgid "Profile name"
|
||||
msgstr "Profilname"
|
||||
|
||||
#: packages/app-desktop/gui/ProfileEditor.tsx:120
|
||||
#, fuzzy
|
||||
msgid "Profile name cannot be empty"
|
||||
msgstr "Bestätigungs-Passwort darf nicht leer sein"
|
||||
msgstr "Profilname darf nicht leer sein"
|
||||
|
||||
#: packages/app-desktop/gui/ProfileEditor.tsx:116
|
||||
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.ts:18
|
||||
@@ -5176,11 +5179,10 @@ msgid "Remove"
|
||||
msgstr "Entfernen"
|
||||
|
||||
#: packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx:134
|
||||
#, fuzzy
|
||||
msgid "Remove %d tags from all notes? This cannot be undone."
|
||||
msgstr ""
|
||||
"Modell löschen und neu herunterladen?\n"
|
||||
"Dies kann nicht rückgängig gemacht werden."
|
||||
"%d Schlagworte aus allen Notizen entfernen? Dieser Vorgang kann nicht "
|
||||
"rückgängig gemacht werden."
|
||||
|
||||
#: packages/app-mobile/components/TagEditor.tsx:136
|
||||
msgid "Remove %s"
|
||||
@@ -5598,7 +5600,7 @@ msgstr "Eltern-Notizbuch auswählen"
|
||||
|
||||
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.ts:42
|
||||
msgid "Select the type of file to be imported:"
|
||||
msgstr ""
|
||||
msgstr "Wähle den Typ der zu importierenden Datei aus:"
|
||||
|
||||
#: packages/app-mobile/components/ComboBox.tsx:378
|
||||
msgid "Selected: %s"
|
||||
@@ -5927,7 +5929,7 @@ msgstr "Quelle: "
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:35
|
||||
msgid "SPACE"
|
||||
msgstr ""
|
||||
msgstr "SPACE"
|
||||
|
||||
#: packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx:456
|
||||
msgid "Spacer"
|
||||
@@ -6241,18 +6243,16 @@ msgid "Tab moves focus"
|
||||
msgstr "Tab verschiebt Fokus"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:118
|
||||
#, fuzzy
|
||||
msgid "Table"
|
||||
msgstr "Aktivieren"
|
||||
msgstr "Tabelle"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1440
|
||||
msgid "Tabloid"
|
||||
msgstr "Tabloid"
|
||||
|
||||
#: packages/app-mobile/components/screens/tags.tsx:206
|
||||
#, fuzzy
|
||||
msgid "Tag: %s"
|
||||
msgstr "Nutzung: %s"
|
||||
msgstr "Schlagwort: %s"
|
||||
|
||||
#: packages/app-cli/app/command-import.ts:58
|
||||
#: packages/app-desktop/gui/ImportScreen.tsx:94
|
||||
@@ -6485,7 +6485,6 @@ msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
|
||||
msgstr "Die Notiz „%s“ wurde erfolgreich im Notizbuch „%s“ wiederhergestellt."
|
||||
|
||||
#: 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"
|
||||
@@ -6493,11 +6492,11 @@ msgid_plural ""
|
||||
"The notes have been converted to Markdown and the original notes have been "
|
||||
"moved to the trash"
|
||||
msgstr[0] ""
|
||||
"Die Notiz wurde in Markdown umgewandelt und die Originalnotiz in den "
|
||||
"Papierkorb verschoben"
|
||||
"Die Notiz wurde in Markdown konvertiert und die ursprüngliche Notiz wurde in "
|
||||
"den Papierkorb verschoben"
|
||||
msgstr[1] ""
|
||||
"Die Notiz wurde in Markdown umgewandelt und die Originalnotiz in den "
|
||||
"Papierkorb verschoben"
|
||||
"Die Notizen wurden in Markdown konvertiert und die ursprünglichen Notizen "
|
||||
"wurden in den Papierkorb verschoben"
|
||||
|
||||
#: packages/app-desktop/gui/TrashNotification/TrashNotification.tsx:45
|
||||
msgid "The note was successfully moved to the trash."
|
||||
@@ -7093,7 +7092,7 @@ msgstr "Jetzt testen"
|
||||
|
||||
#: packages/app-cli/app/command-keymap.ts:30
|
||||
msgid "TYPE"
|
||||
msgstr ""
|
||||
msgstr "TYPE"
|
||||
|
||||
#: packages/app-cli/app/command-help.ts:72
|
||||
msgid ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -960,7 +960,7 @@ msgstr "Det går inte att hitta \"%s\"."
|
||||
|
||||
#: packages/app-cli/app/command-mkbook.ts:28
|
||||
msgid "Cannot find: \"%s\""
|
||||
msgstr "Det går inte att hitta: \"%s\"."
|
||||
msgstr "Det går inte att hitta: \"%s\""
|
||||
|
||||
#: packages/app-cli/app/command-sync.ts:203
|
||||
msgid "Cannot initialise synchroniser."
|
||||
@@ -2357,7 +2357,7 @@ msgstr "Aktivera förkortningssyntax"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1093
|
||||
msgid "Enable ABC musical notation support"
|
||||
msgstr "Aktivera stöd för ABC musiknotation."
|
||||
msgstr "Aktivera stöd för ABC musiknotation"
|
||||
|
||||
#: packages/lib/models/settings/builtInMetadata.ts:1095
|
||||
msgid "Enable audio player"
|
||||
@@ -2741,7 +2741,7 @@ msgstr ""
|
||||
#: packages/app-desktop/gui/SsoLoginScreen/SsoLoginScreen.tsx:29
|
||||
#: packages/app-mobile/components/screens/SsoLoginScreen.tsx:54
|
||||
msgid "Failed to connect to your account. Please try again."
|
||||
msgstr "Det gick inte att ansluta till ditt konto. Vänligen försök igen."
|
||||
msgstr "Det gick inte att ansluta till ditt konto. Försök igen."
|
||||
|
||||
#: packages/app-cli/app/main.js:107
|
||||
msgid "Fatal error:"
|
||||
@@ -4750,7 +4750,7 @@ msgstr ""
|
||||
|
||||
#: packages/server/src/utils/saml.ts:58
|
||||
msgid "Please wait while we load your organisation sign-in page..."
|
||||
msgstr "Vänligen vänta medan vi laddar din organisations inloggningssida..."
|
||||
msgstr "Vänta medan vi laddar din organisations inloggningssida..."
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.tsx:118
|
||||
#: packages/app-desktop/gui/ResourceScreen.tsx:315
|
||||
@@ -6801,7 +6801,7 @@ msgid ""
|
||||
"To allow Joplin to synchronise with Joplin Cloud, please login using this "
|
||||
"URL:"
|
||||
msgstr ""
|
||||
"För att tillåta Joplin att synkronisera med Joplin Cloud, vänligen logga in "
|
||||
"För att tillåta Joplin att synkronisera med Joplin Cloud, logga in "
|
||||
"med denna URL:"
|
||||
|
||||
#: packages/app-desktop/gui/SsoLoginScreen/SsoLoginScreen.tsx:36
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@joplin/renderer": "~3.6",
|
||||
"@joplin/utils": "~3.6",
|
||||
"compare-versions": "6.1.1",
|
||||
"dayjs": "1.11.18",
|
||||
"dayjs": "1.11.19",
|
||||
"execa": "4.1.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"gettext-parser": "7.0.1",
|
||||
@@ -54,14 +54,14 @@
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/node-fetch": "2.6.13",
|
||||
"@types/yargs": "17.0.33",
|
||||
"gettext-extractor": "3.8.0",
|
||||
"@types/yargs": "17.0.34",
|
||||
"gettext-extractor": "4.0.1",
|
||||
"gulp": "4.0.2",
|
||||
"html-entities": "1.4.0",
|
||||
"jest": "29.7.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"rss": "1.2.2",
|
||||
"sass": "1.93.2",
|
||||
"sass": "1.93.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"style-to-js": "1.1.18",
|
||||
"ts-node": "10.9.2",
|
||||
|
||||
@@ -200,6 +200,7 @@
|
||||
"v3.5.11": true,
|
||||
"v3.5.12": true,
|
||||
"v3.6.1": true,
|
||||
"v3.6.2": 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,7 +329,7 @@ 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`;
|
||||
|
||||
|
||||
@@ -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,9 +14,9 @@
|
||||
"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",
|
||||
"@rollup/plugin-replace": "6.0.3",
|
||||
"browserify": "14.5.0",
|
||||
"rollup": "4.2.0",
|
||||
"standard": "17.1.2",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"dependencies": {
|
||||
"@joplin/fork-htmlparser2": "^4.1.60",
|
||||
"async-mutex": "0.5.0",
|
||||
"dayjs": "1.11.18",
|
||||
"dayjs": "1.11.19",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.3.2",
|
||||
"glob": "11.0.3",
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
updated: 2026-01-01T02:02:32Z
|
||||
updated: 2026-02-01T02:35:07Z
|
||||
---
|
||||
|
||||
# Joplin statistics
|
||||
|
||||
| Name | Value |
|
||||
| ----- | ----- |
|
||||
| Total Windows downloads | 7,465,188 |
|
||||
| Total macOs downloads | 2,073,797 |
|
||||
| Total Linux downloads | 1,698,890 |
|
||||
| Windows % | 66% |
|
||||
| Total Windows downloads | 7,693,776 |
|
||||
| Total macOs downloads | 2,092,639 |
|
||||
| Total Linux downloads | 1,732,101 |
|
||||
| Windows % | 67% |
|
||||
| macOS % | 18% |
|
||||
| Linux % | 15% |
|
||||
|
||||
@@ -17,237 +17,242 @@ updated: 2026-01-01T02:02:32Z
|
||||
|
||||
| Version | Date | Windows | macOS | Linux | Total |
|
||||
| ----- | ----- | ----- | ----- | ----- | ----- |
|
||||
| [v3.5.9](https://github.com/laurent22/joplin/releases/tag/v3.5.9) (p) | 2025-11-30T19:11:11Z | 4,213 | 765 | 1,266 | 6,244 |
|
||||
| [v3.5.7](https://github.com/laurent22/joplin/releases/tag/v3.5.7) (p) | 2025-11-22T08:35:36Z | 1,816 | 317 | 1,078 | 3,211 |
|
||||
| [v3.5.6](https://github.com/laurent22/joplin/releases/tag/v3.5.6) (p) | 2025-10-29T14:48:46Z | 4,224 | 645 | 1,451 | 6,320 |
|
||||
| [v3.5.5](https://github.com/laurent22/joplin/releases/tag/v3.5.5) (p) | 2025-10-18T10:31:09Z | 2,426 | 367 | 582 | 3,375 |
|
||||
| [v3.5.4](https://github.com/laurent22/joplin/releases/tag/v3.5.4) (p) | 2025-10-10T17:19:58Z | 2,492 | 258 | 331 | 3,081 |
|
||||
| [v3.4.12](https://github.com/laurent22/joplin/releases/tag/v3.4.12) | 2025-09-09T21:35:47Z | 339,960 | 31,383 | 90,374 | 461,717 |
|
||||
| [v3.4.10](https://github.com/laurent22/joplin/releases/tag/v3.4.10) | 2025-09-01T12:25:22Z | 77,250 | 7,829 | 10,369 | 95,448 |
|
||||
| [v3.4.7](https://github.com/laurent22/joplin/releases/tag/v3.4.7) (p) | 2025-08-23T10:49:54Z | 8,025 | 385 | 600 | 9,010 |
|
||||
| [v3.4.6](https://github.com/laurent22/joplin/releases/tag/v3.4.6) (p) | 2025-08-20T20:30:35Z | 42,759 | 5,173 | 2,643 | 50,575 |
|
||||
| [v3.4.5](https://github.com/laurent22/joplin/releases/tag/v3.4.5) (p) | 2025-08-10T12:49:30Z | 1,786 | 399 | 449 | 2,634 |
|
||||
| [v3.6.2](https://github.com/laurent22/joplin/releases/tag/v3.6.2) (p) | 2026-01-18T20:10:43Z | 2,014 | 375 | 652 | 3,041 |
|
||||
| [v3.6.1](https://github.com/laurent22/joplin/releases/tag/v3.6.1) (p) | 2026-01-17T14:17:29Z | 451 | 86 | 90 | 627 |
|
||||
| [v3.5.12](https://github.com/laurent22/joplin/releases/tag/v3.5.12) | 2026-01-17T14:20:33Z | 109,044 | 8,240 | 15,151 | 132,435 |
|
||||
| [v3.5.11](https://github.com/laurent22/joplin/releases/tag/v3.5.11) | 2026-01-12T15:17:25Z | 74,153 | 6,083 | 5,681 | 85,917 |
|
||||
| [v3.5.10](https://github.com/laurent22/joplin/releases/tag/v3.5.10) (p) | 2026-01-08T20:18:15Z | 999 | 167 | 256 | 1,422 |
|
||||
| [v3.5.9](https://github.com/laurent22/joplin/releases/tag/v3.5.9) (p) | 2025-11-30T19:11:11Z | 5,307 | 948 | 2,084 | 8,339 |
|
||||
| [v3.5.7](https://github.com/laurent22/joplin/releases/tag/v3.5.7) (p) | 2025-11-22T08:35:36Z | 1,835 | 329 | 1,116 | 3,280 |
|
||||
| [v3.5.6](https://github.com/laurent22/joplin/releases/tag/v3.5.6) (p) | 2025-10-29T14:48:46Z | 4,250 | 675 | 1,462 | 6,387 |
|
||||
| [v3.5.5](https://github.com/laurent22/joplin/releases/tag/v3.5.5) (p) | 2025-10-18T10:31:09Z | 2,436 | 374 | 587 | 3,397 |
|
||||
| [v3.5.4](https://github.com/laurent22/joplin/releases/tag/v3.5.4) (p) | 2025-10-10T17:19:58Z | 2,521 | 264 | 336 | 3,121 |
|
||||
| [v3.4.12](https://github.com/laurent22/joplin/releases/tag/v3.4.12) | 2025-09-09T21:35:47Z | 367,244 | 34,261 | 100,031 | 501,536 |
|
||||
| [v3.4.10](https://github.com/laurent22/joplin/releases/tag/v3.4.10) | 2025-09-01T12:25:22Z | 77,546 | 7,864 | 10,380 | 95,790 |
|
||||
| [v3.4.7](https://github.com/laurent22/joplin/releases/tag/v3.4.7) (p) | 2025-08-23T10:49:54Z | 8,074 | 385 | 603 | 9,062 |
|
||||
| [v3.4.6](https://github.com/laurent22/joplin/releases/tag/v3.4.6) (p) | 2025-08-20T20:30:35Z | 42,918 | 5,175 | 2,645 | 50,738 |
|
||||
| [v3.4.5](https://github.com/laurent22/joplin/releases/tag/v3.4.5) (p) | 2025-08-10T12:49:30Z | 1,788 | 399 | 450 | 2,637 |
|
||||
| [v3.4.4](https://github.com/laurent22/joplin/releases/tag/v3.4.4) (p) | 2025-08-13T16:46:39Z | 113 | 60 | 42 | 215 |
|
||||
| [v3.4.3](https://github.com/laurent22/joplin/releases/tag/v3.4.3) (p) | 2025-07-25T19:49:44Z | 2,004 | 505 | 569 | 3,078 |
|
||||
| [v3.4.2](https://github.com/laurent22/joplin/releases/tag/v3.4.2) (p) | 2025-07-24T10:43:55Z | 644 | 161 | 130 | 935 |
|
||||
| [v3.3.13](https://github.com/laurent22/joplin/releases/tag/v3.3.13) | 2025-06-09T20:13:30Z | 265,307 | 27,199 | 60,703 | 353,209 |
|
||||
| [v3.4.3](https://github.com/laurent22/joplin/releases/tag/v3.4.3) (p) | 2025-07-25T19:49:44Z | 2,005 | 505 | 569 | 3,079 |
|
||||
| [v3.4.2](https://github.com/laurent22/joplin/releases/tag/v3.4.2) (p) | 2025-07-24T10:43:55Z | 645 | 161 | 130 | 936 |
|
||||
| [v3.3.13](https://github.com/laurent22/joplin/releases/tag/v3.3.13) | 2025-06-09T20:13:30Z | 266,081 | 27,230 | 61,031 | 354,342 |
|
||||
| [v3.4.1](https://github.com/laurent22/joplin/releases/tag/v3.4.1) (p) | 2025-05-20T09:59:39Z | 3,967 | 1,163 | 1,332 | 6,462 |
|
||||
| [v3.3.12](https://github.com/laurent22/joplin/releases/tag/v3.3.12) | 2025-05-04T18:12:23Z | 164,423 | 21,586 | 28,239 | 214,248 |
|
||||
| [v3.3.10](https://github.com/laurent22/joplin/releases/tag/v3.3.10) | 2025-05-02T19:46:15Z | 31,260 | 5,164 | 1,846 | 38,270 |
|
||||
| [v3.3.9](https://github.com/laurent22/joplin/releases/tag/v3.3.9) | 2025-05-01T21:02:12Z | 30,100 | 6,155 | 1,203 | 37,458 |
|
||||
| [v3.3.12](https://github.com/laurent22/joplin/releases/tag/v3.3.12) | 2025-05-04T18:12:23Z | 165,618 | 21,592 | 28,244 | 215,454 |
|
||||
| [v3.3.10](https://github.com/laurent22/joplin/releases/tag/v3.3.10) | 2025-05-02T19:46:15Z | 32,250 | 5,165 | 1,847 | 39,262 |
|
||||
| [v3.3.9](https://github.com/laurent22/joplin/releases/tag/v3.3.9) | 2025-05-01T21:02:12Z | 31,181 | 6,157 | 1,203 | 38,541 |
|
||||
| [v3.3.7](https://github.com/laurent22/joplin/releases/tag/v3.3.7) (p) | 2025-04-29T13:47:19Z | 771 | 0 | 182 | 953 |
|
||||
| [v3.3.6](https://github.com/laurent22/joplin/releases/tag/v3.3.6) (p) | 2025-04-24T12:27:20Z | 1,071 | 303 | 262 | 1,636 |
|
||||
| [v3.3.5](https://github.com/laurent22/joplin/releases/tag/v3.3.5) (p) | 2025-04-17T13:40:31Z | 1,402 | 319 | 316 | 2,037 |
|
||||
| [v3.3.4](https://github.com/laurent22/joplin/releases/tag/v3.3.4) (p) | 2025-04-07T20:23:35Z | 1,644 | 427 | 382 | 2,453 |
|
||||
| [v3.3.3](https://github.com/laurent22/joplin/releases/tag/v3.3.3) (p) | 2025-03-16T11:52:33Z | 2,717 | 844 | 990 | 4,551 |
|
||||
| [v3.2.13](https://github.com/laurent22/joplin/releases/tag/v3.2.13) | 2025-02-28T14:38:21Z | 224,510 | 33,017 | 44,677 | 302,204 |
|
||||
| [v3.3.3](https://github.com/laurent22/joplin/releases/tag/v3.3.3) (p) | 2025-03-16T11:52:33Z | 2,717 | 844 | 991 | 4,552 |
|
||||
| [v3.2.13](https://github.com/laurent22/joplin/releases/tag/v3.2.13) | 2025-02-28T14:38:21Z | 224,897 | 33,047 | 44,720 | 302,664 |
|
||||
| [v3.3.2](https://github.com/laurent22/joplin/releases/tag/v3.3.2) (p) | 2025-02-19T17:34:26Z | 2,375 | 595 | 665 | 3,635 |
|
||||
| [v3.3.1](https://github.com/laurent22/joplin/releases/tag/v3.3.1) (p) | 2025-02-16T17:06:26Z | 870 | 215 | 186 | 1,271 |
|
||||
| [v3.2.12](https://github.com/laurent22/joplin/releases/tag/v3.2.12) | 2025-01-23T23:52:04Z | 153,563 | 25,384 | 28,029 | 206,976 |
|
||||
| [v3.2.11](https://github.com/laurent22/joplin/releases/tag/v3.2.11) | 2025-01-13T17:48:21Z | 67,519 | 14,881 | 6,902 | 89,302 |
|
||||
| [v3.2.10](https://github.com/laurent22/joplin/releases/tag/v3.2.10) (p) | 2025-01-10T10:17:28Z | 2,926 | 195 | 205 | 3,326 |
|
||||
| [v3.3.1](https://github.com/laurent22/joplin/releases/tag/v3.3.1) (p) | 2025-02-16T17:06:26Z | 871 | 217 | 186 | 1,274 |
|
||||
| [v3.2.12](https://github.com/laurent22/joplin/releases/tag/v3.2.12) | 2025-01-23T23:52:04Z | 153,757 | 25,408 | 28,064 | 207,229 |
|
||||
| [v3.2.11](https://github.com/laurent22/joplin/releases/tag/v3.2.11) | 2025-01-13T17:48:21Z | 67,721 | 14,884 | 6,903 | 89,508 |
|
||||
| [v3.2.10](https://github.com/laurent22/joplin/releases/tag/v3.2.10) (p) | 2025-01-10T10:17:28Z | 2,987 | 195 | 205 | 3,387 |
|
||||
| [v3.2.9](https://github.com/laurent22/joplin/releases/tag/v3.2.9) (p) | 2025-01-09T22:58:42Z | 394 | 120 | 70 | 584 |
|
||||
| [v3.2.7](https://github.com/laurent22/joplin/releases/tag/v3.2.7) (p) | 2025-01-06T16:35:41Z | 909 | 185 | 906 | 2,000 |
|
||||
| [v3.2.7](https://github.com/laurent22/joplin/releases/tag/v3.2.7) (p) | 2025-01-06T16:35:41Z | 909 | 185 | 907 | 2,001 |
|
||||
| [v3.2.6](https://github.com/laurent22/joplin/releases/tag/v3.2.6) (p) | 2024-12-23T21:54:40Z | 1,789 | 364 | 494 | 2,647 |
|
||||
| [v3.2.5](https://github.com/laurent22/joplin/releases/tag/v3.2.5) (p) | 2024-12-18T10:41:13Z | 1,030 | 229 | 238 | 1,497 |
|
||||
| [v3.2.4](https://github.com/laurent22/joplin/releases/tag/v3.2.4) (p) | 2024-12-12T17:59:52Z | 1,042 | 182 | 253 | 1,477 |
|
||||
| [v3.2.3](https://github.com/laurent22/joplin/releases/tag/v3.2.3) (p) | 2024-11-18T00:09:05Z | 2,735 | 585 | 912 | 4,232 |
|
||||
| [v3.2.1](https://github.com/laurent22/joplin/releases/tag/v3.2.1) (p) | 2024-11-10T16:16:27Z | 1,285 | 267 | 365 | 1,917 |
|
||||
| [v3.1.24](https://github.com/laurent22/joplin/releases/tag/v3.1.24) | 2024-11-09T15:08:29Z | 209,928 | 33,564 | 43,916 | 287,408 |
|
||||
| [v3.1.23](https://github.com/laurent22/joplin/releases/tag/v3.1.23) | 2024-11-07T10:56:45Z | 28,517 | 6,766 | 1,561 | 36,844 |
|
||||
| [v3.1.22](https://github.com/laurent22/joplin/releases/tag/v3.1.22) | 2024-11-05T08:59:32Z | 30,426 | 8,732 | 1,240 | 40,398 |
|
||||
| [v3.1.20](https://github.com/laurent22/joplin/releases/tag/v3.1.20) | 2024-10-22T12:21:32Z | 96,787 | 19,412 | 13,949 | 130,148 |
|
||||
| [v3.2.3](https://github.com/laurent22/joplin/releases/tag/v3.2.3) (p) | 2024-11-18T00:09:05Z | 2,736 | 585 | 912 | 4,233 |
|
||||
| [v3.2.1](https://github.com/laurent22/joplin/releases/tag/v3.2.1) (p) | 2024-11-10T16:16:27Z | 1,285 | 268 | 366 | 1,919 |
|
||||
| [v3.1.24](https://github.com/laurent22/joplin/releases/tag/v3.1.24) | 2024-11-09T15:08:29Z | 210,156 | 33,577 | 43,939 | 287,672 |
|
||||
| [v3.1.23](https://github.com/laurent22/joplin/releases/tag/v3.1.23) | 2024-11-07T10:56:45Z | 28,642 | 6,768 | 1,563 | 36,973 |
|
||||
| [v3.1.22](https://github.com/laurent22/joplin/releases/tag/v3.1.22) | 2024-11-05T08:59:32Z | 30,552 | 8,732 | 1,240 | 40,524 |
|
||||
| [v3.1.20](https://github.com/laurent22/joplin/releases/tag/v3.1.20) | 2024-10-22T12:21:32Z | 96,993 | 19,413 | 13,951 | 130,357 |
|
||||
| [v3.1.18](https://github.com/laurent22/joplin/releases/tag/v3.1.18) (p) | 2024-10-11T23:27:10Z | 1,541 | 311 | 605 | 2,457 |
|
||||
| [v3.1.17](https://github.com/laurent22/joplin/releases/tag/v3.1.17) (p) | 2024-09-26T11:57:54Z | 1,732 | 384 | 548 | 2,664 |
|
||||
| [v3.1.15](https://github.com/laurent22/joplin/releases/tag/v3.1.15) (p) | 2024-09-17T09:15:10Z | 1,216 | 255 | 512 | 1,983 |
|
||||
| [v3.1.15](https://github.com/laurent22/joplin/releases/tag/v3.1.15) (p) | 2024-09-17T09:15:10Z | 1,216 | 255 | 513 | 1,984 |
|
||||
| [v3.1.8](https://github.com/laurent22/joplin/releases/tag/v3.1.8) (p) | 2024-09-08T20:32:44Z | 1,256 | 288 | 351 | 1,895 |
|
||||
| [v3.1.6](https://github.com/laurent22/joplin/releases/tag/v3.1.6) (p) | 2024-09-02T13:19:40Z | 996 | 260 | 442 | 1,698 |
|
||||
| [v3.1.4](https://github.com/laurent22/joplin/releases/tag/v3.1.4) (p) | 2024-08-27T17:46:38Z | 979 | 207 | 266 | 1,452 |
|
||||
| [v3.0.15](https://github.com/laurent22/joplin/releases/tag/v3.0.15) | 2024-08-21T09:19:58Z | 205,225 | 37,882 | 45,218 | 288,325 |
|
||||
| [v3.0.15](https://github.com/laurent22/joplin/releases/tag/v3.0.15) | 2024-08-21T09:19:58Z | 205,455 | 37,888 | 45,228 | 288,571 |
|
||||
| [v3.1.3](https://github.com/laurent22/joplin/releases/tag/v3.1.3) (p) | 2024-08-17T13:08:21Z | 1,272 | 317 | 504 | 2,093 |
|
||||
| [v3.1.2](https://github.com/laurent22/joplin/releases/tag/v3.1.2) (p) | 2024-08-16T09:00:59Z | 483 | 147 | 106 | 736 |
|
||||
| [v3.1.2](https://github.com/laurent22/joplin/releases/tag/v3.1.2) (p) | 2024-08-16T09:00:59Z | 484 | 147 | 107 | 738 |
|
||||
| [v3.1.1](https://github.com/laurent22/joplin/releases/tag/v3.1.1) (p) | 2024-08-10T11:36:02Z | 1,116 | 229 | 282 | 1,627 |
|
||||
| [v2.14.23](https://github.com/laurent22/joplin/releases/tag/v2.14.23) | 2024-08-07T11:15:25Z | 10,878 | 2,781 | 654 | 14,313 |
|
||||
| [v3.0.14](https://github.com/laurent22/joplin/releases/tag/v3.0.14) | 2024-07-28T13:55:50Z | 91,107 | 18,598 | 18,873 | 128,578 |
|
||||
| [v3.0.12](https://github.com/laurent22/joplin/releases/tag/v3.0.12) | 2024-07-02T17:11:14Z | 46,087 | 12,892 | 7,395 | 66,374 |
|
||||
| [v3.0.11](https://github.com/laurent22/joplin/releases/tag/v3.0.11) (p) | 2024-06-29T10:20:02Z | 875 | 180 | 283 | 1,338 |
|
||||
| [v2.14.23](https://github.com/laurent22/joplin/releases/tag/v2.14.23) | 2024-08-07T11:15:25Z | 10,883 | 2,794 | 656 | 14,333 |
|
||||
| [v3.0.14](https://github.com/laurent22/joplin/releases/tag/v3.0.14) | 2024-07-28T13:55:50Z | 91,293 | 18,600 | 18,874 | 128,767 |
|
||||
| [v3.0.12](https://github.com/laurent22/joplin/releases/tag/v3.0.12) | 2024-07-02T17:11:14Z | 46,275 | 12,907 | 7,403 | 66,585 |
|
||||
| [v3.0.11](https://github.com/laurent22/joplin/releases/tag/v3.0.11) (p) | 2024-06-29T10:20:02Z | 875 | 181 | 284 | 1,340 |
|
||||
| [v3.0.10](https://github.com/laurent22/joplin/releases/tag/v3.0.10) (p) | 2024-06-19T15:24:07Z | 1,651 | 307 | 577 | 2,535 |
|
||||
| [v3.0.9](https://github.com/laurent22/joplin/releases/tag/v3.0.9) (p) | 2024-06-12T19:07:50Z | 1,303 | 276 | 394 | 1,973 |
|
||||
| [v3.0.8](https://github.com/laurent22/joplin/releases/tag/v3.0.8) (p) | 2024-05-22T14:20:45Z | 2,696 | 0 | 954 | 3,650 |
|
||||
| [v2.14.22](https://github.com/laurent22/joplin/releases/tag/v2.14.22) | 2024-05-22T19:19:02Z | 144,451 | 30,928 | 25,554 | 200,933 |
|
||||
| [v3.0.6](https://github.com/laurent22/joplin/releases/tag/v3.0.6) (p) | 2024-04-27T13:16:04Z | 3,040 | 704 | 878 | 4,622 |
|
||||
| [v3.0.3](https://github.com/laurent22/joplin/releases/tag/v3.0.3) (p) | 2024-04-18T15:41:38Z | 1,518 | 337 | 347 | 2,202 |
|
||||
| [v3.0.2](https://github.com/laurent22/joplin/releases/tag/v3.0.2) (p) | 2024-03-21T18:18:49Z | 2,909 | 761 | 1,125 | 4,795 |
|
||||
| [v2.14.20](https://github.com/laurent22/joplin/releases/tag/v2.14.20) | 2024-03-18T17:05:17Z | 194,689 | 39,624 | 38,386 | 272,699 |
|
||||
| [v2.14.19](https://github.com/laurent22/joplin/releases/tag/v2.14.19) | 2024-03-08T10:45:16Z | 65,998 | 18,495 | 8,580 | 93,073 |
|
||||
| [v2.14.17](https://github.com/laurent22/joplin/releases/tag/v2.14.17) | 2024-03-01T18:10:26Z | 64,480 | 18,659 | 7,644 | 90,783 |
|
||||
| [v3.0.9](https://github.com/laurent22/joplin/releases/tag/v3.0.9) (p) | 2024-06-12T19:07:50Z | 1,303 | 277 | 395 | 1,975 |
|
||||
| [v3.0.8](https://github.com/laurent22/joplin/releases/tag/v3.0.8) (p) | 2024-05-22T14:20:45Z | 2,696 | 0 | 955 | 3,651 |
|
||||
| [v2.14.22](https://github.com/laurent22/joplin/releases/tag/v2.14.22) | 2024-05-22T19:19:02Z | 144,593 | 30,950 | 25,579 | 201,122 |
|
||||
| [v3.0.6](https://github.com/laurent22/joplin/releases/tag/v3.0.6) (p) | 2024-04-27T13:16:04Z | 3,040 | 705 | 879 | 4,624 |
|
||||
| [v3.0.3](https://github.com/laurent22/joplin/releases/tag/v3.0.3) (p) | 2024-04-18T15:41:38Z | 1,518 | 339 | 348 | 2,205 |
|
||||
| [v3.0.2](https://github.com/laurent22/joplin/releases/tag/v3.0.2) (p) | 2024-03-21T18:18:49Z | 2,909 | 762 | 1,125 | 4,796 |
|
||||
| [v2.14.20](https://github.com/laurent22/joplin/releases/tag/v2.14.20) | 2024-03-18T17:05:17Z | 194,812 | 39,645 | 38,415 | 272,872 |
|
||||
| [v2.14.19](https://github.com/laurent22/joplin/releases/tag/v2.14.19) | 2024-03-08T10:45:16Z | 66,070 | 18,495 | 8,581 | 93,146 |
|
||||
| [v2.14.17](https://github.com/laurent22/joplin/releases/tag/v2.14.17) | 2024-03-01T18:10:26Z | 64,523 | 18,659 | 7,646 | 90,828 |
|
||||
| [v2.14.16](https://github.com/laurent22/joplin/releases/tag/v2.14.16) (p) | 2024-02-22T22:49:10Z | 1,383 | 302 | 394 | 2,079 |
|
||||
| [v2.14.15](https://github.com/laurent22/joplin/releases/tag/v2.14.15) (p) | 2024-02-19T11:24:57Z | 886 | 197 | 208 | 1,291 |
|
||||
| [v2.14.14](https://github.com/laurent22/joplin/releases/tag/v2.14.14) (p) | 2024-02-10T16:03:08Z | 1,305 | 259 | 398 | 1,962 |
|
||||
| [v2.14.14](https://github.com/laurent22/joplin/releases/tag/v2.14.14) (p) | 2024-02-10T16:03:08Z | 1,305 | 259 | 399 | 1,963 |
|
||||
| [v2.14.13](https://github.com/laurent22/joplin/releases/tag/v2.14.13) (p) | 2024-02-09T16:31:54Z | 457 | 135 | 106 | 698 |
|
||||
| [v2.14.12](https://github.com/laurent22/joplin/releases/tag/v2.14.12) (p) | 2024-02-03T12:11:47Z | 1,013 | 233 | 266 | 1,512 |
|
||||
| [v2.14.11](https://github.com/laurent22/joplin/releases/tag/v2.14.11) (p) | 2024-01-26T11:53:05Z | 1,307 | 275 | 492 | 2,074 |
|
||||
| [v2.14.10](https://github.com/laurent22/joplin/releases/tag/v2.14.10) (p) | 2024-01-18T22:45:04Z | 2,130 | 286 | 389 | 2,805 |
|
||||
| [v2.13.15](https://github.com/laurent22/joplin/releases/tag/v2.13.15) | 2024-01-15T13:01:19Z | 151,102 | 34,188 | 29,294 | 214,584 |
|
||||
| [v2.13.14](https://github.com/laurent22/joplin/releases/tag/v2.13.14) | 2024-01-13T19:11:04Z | 20,829 | 7,846 | 2,383 | 31,058 |
|
||||
| [v2.14.12](https://github.com/laurent22/joplin/releases/tag/v2.14.12) (p) | 2024-02-03T12:11:47Z | 1,014 | 233 | 268 | 1,515 |
|
||||
| [v2.14.11](https://github.com/laurent22/joplin/releases/tag/v2.14.11) (p) | 2024-01-26T11:53:05Z | 1,307 | 275 | 493 | 2,075 |
|
||||
| [v2.14.10](https://github.com/laurent22/joplin/releases/tag/v2.14.10) (p) | 2024-01-18T22:45:04Z | 2,130 | 286 | 390 | 2,806 |
|
||||
| [v2.13.15](https://github.com/laurent22/joplin/releases/tag/v2.13.15) | 2024-01-15T13:01:19Z | 151,241 | 34,191 | 29,307 | 214,739 |
|
||||
| [v2.13.14](https://github.com/laurent22/joplin/releases/tag/v2.13.14) | 2024-01-13T19:11:04Z | 20,878 | 7,846 | 2,383 | 31,107 |
|
||||
| [v2.14.9](https://github.com/laurent22/joplin/releases/tag/v2.14.9) (p) | 2024-01-11T22:17:59Z | 1,092 | 0 | 261 | 1,353 |
|
||||
| [v2.14.8](https://github.com/laurent22/joplin/releases/tag/v2.14.8) (p) | 2024-01-09T22:57:07Z | 791 | 253 | 194 | 1,238 |
|
||||
| [v2.14.7](https://github.com/laurent22/joplin/releases/tag/v2.14.7) (p) | 2024-01-08T11:51:49Z | 653 | 144 | 193 | 990 |
|
||||
| [v2.14.8](https://github.com/laurent22/joplin/releases/tag/v2.14.8) (p) | 2024-01-09T22:57:07Z | 791 | 253 | 199 | 1,243 |
|
||||
| [v2.14.7](https://github.com/laurent22/joplin/releases/tag/v2.14.7) (p) | 2024-01-08T11:51:49Z | 653 | 144 | 194 | 991 |
|
||||
| [v2.14.6](https://github.com/laurent22/joplin/releases/tag/v2.14.6) (p) | 2024-01-06T16:38:32Z | 761 | 166 | 168 | 1,095 |
|
||||
| [v2.13.13](https://github.com/laurent22/joplin/releases/tag/v2.13.13) | 2024-01-06T13:33:11Z | 55,042 | 15,954 | 6,368 | 77,364 |
|
||||
| [v2.13.12](https://github.com/laurent22/joplin/releases/tag/v2.13.12) | 2023-12-31T16:08:02Z | 46,194 | 14,316 | 5,109 | 65,619 |
|
||||
| [v2.13.11](https://github.com/laurent22/joplin/releases/tag/v2.13.11) | 2023-12-24T12:58:53Z | 46,983 | 13,282 | 6,019 | 66,284 |
|
||||
| [v2.13.10](https://github.com/laurent22/joplin/releases/tag/v2.13.10) | 2023-12-22T10:11:08Z | 20,385 | 8,747 | 1,581 | 30,713 |
|
||||
| [v2.13.9](https://github.com/laurent22/joplin/releases/tag/v2.13.9) | 2023-12-09T17:18:58Z | 69,164 | 21,942 | 8,648 | 99,754 |
|
||||
| [v2.13.8](https://github.com/laurent22/joplin/releases/tag/v2.13.8) | 2023-12-03T12:07:08Z | 51,606 | 17,922 | 5,236 | 74,764 |
|
||||
| [v2.13.6](https://github.com/laurent22/joplin/releases/tag/v2.13.6) (p) | 2023-11-17T19:24:03Z | 2,192 | 463 | 596 | 3,251 |
|
||||
| [v2.13.5](https://github.com/laurent22/joplin/releases/tag/v2.13.5) (p) | 2023-11-09T20:24:09Z | 1,504 | 356 | 458 | 2,318 |
|
||||
| [v2.13.13](https://github.com/laurent22/joplin/releases/tag/v2.13.13) | 2024-01-06T13:33:11Z | 55,100 | 15,954 | 6,368 | 77,422 |
|
||||
| [v2.13.12](https://github.com/laurent22/joplin/releases/tag/v2.13.12) | 2023-12-31T16:08:02Z | 46,242 | 14,316 | 5,111 | 65,669 |
|
||||
| [v2.13.11](https://github.com/laurent22/joplin/releases/tag/v2.13.11) | 2023-12-24T12:58:53Z | 47,035 | 13,283 | 6,023 | 66,341 |
|
||||
| [v2.13.10](https://github.com/laurent22/joplin/releases/tag/v2.13.10) | 2023-12-22T10:11:08Z | 20,446 | 8,784 | 1,612 | 30,842 |
|
||||
| [v2.13.9](https://github.com/laurent22/joplin/releases/tag/v2.13.9) | 2023-12-09T17:18:58Z | 69,242 | 21,942 | 8,654 | 99,838 |
|
||||
| [v2.13.8](https://github.com/laurent22/joplin/releases/tag/v2.13.8) | 2023-12-03T12:07:08Z | 51,644 | 17,923 | 5,238 | 74,805 |
|
||||
| [v2.13.6](https://github.com/laurent22/joplin/releases/tag/v2.13.6) (p) | 2023-11-17T19:24:03Z | 2,192 | 463 | 598 | 3,253 |
|
||||
| [v2.13.5](https://github.com/laurent22/joplin/releases/tag/v2.13.5) (p) | 2023-11-09T20:24:09Z | 1,504 | 356 | 459 | 2,319 |
|
||||
| [v2.13.4](https://github.com/laurent22/joplin/releases/tag/v2.13.4) (p) | 2023-10-31T00:01:00Z | 1,581 | 388 | 505 | 2,474 |
|
||||
| [v2.13.3](https://github.com/laurent22/joplin/releases/tag/v2.13.3) (p) | 2023-10-24T09:25:33Z | 1,315 | 285 | 304 | 1,904 |
|
||||
| [v2.12.19](https://github.com/laurent22/joplin/releases/tag/v2.12.19) | 2023-10-21T09:39:18Z | 168,213 | 43,676 | 27,903 | 239,792 |
|
||||
| [v2.13.3](https://github.com/laurent22/joplin/releases/tag/v2.13.3) (p) | 2023-10-24T09:25:33Z | 1,315 | 285 | 305 | 1,905 |
|
||||
| [v2.12.19](https://github.com/laurent22/joplin/releases/tag/v2.12.19) | 2023-10-21T09:39:18Z | 168,293 | 43,680 | 27,916 | 239,889 |
|
||||
| [v2.13.2](https://github.com/laurent22/joplin/releases/tag/v2.13.2) (p) | 2023-10-06T17:00:07Z | 2,052 | 505 | 709 | 3,266 |
|
||||
| [v2.12.18](https://github.com/laurent22/joplin/releases/tag/v2.12.18) | 2023-09-22T14:37:24Z | 110,324 | 36,533 | 18,731 | 165,588 |
|
||||
| [v2.12.17](https://github.com/laurent22/joplin/releases/tag/v2.12.17) | 2023-09-14T21:54:52Z | 48,176 | 21,044 | 6,654 | 75,874 |
|
||||
| [v2.13.1](https://github.com/laurent22/joplin/releases/tag/v2.13.1) (p) | 2023-09-13T09:31:50Z | 1,396 | 430 | 690 | 2,516 |
|
||||
| [v2.12.16](https://github.com/laurent22/joplin/releases/tag/v2.12.16) | 2023-09-11T22:33:37Z | 28,829 | 14,678 | 2,471 | 45,978 |
|
||||
| [v2.12.15](https://github.com/laurent22/joplin/releases/tag/v2.12.15) | 2023-08-27T11:35:39Z | 65,725 | 28,184 | 8,496 | 102,405 |
|
||||
| [v2.12.12](https://github.com/laurent22/joplin/releases/tag/v2.12.12) (p) | 2023-08-19T22:44:56Z | 3,403 | 399 | 431 | 4,233 |
|
||||
| [v2.12.10](https://github.com/laurent22/joplin/releases/tag/v2.12.10) (p) | 2023-07-30T18:25:58Z | 8,044 | 3,821 | 918 | 12,783 |
|
||||
| [v2.12.9](https://github.com/laurent22/joplin/releases/tag/v2.12.9) (p) | 2023-07-25T16:06:08Z | 2,854 | 375 | 324 | 3,553 |
|
||||
| [v2.12.7](https://github.com/laurent22/joplin/releases/tag/v2.12.7) (p) | 2023-07-13T12:55:31Z | 2,205 | 669 | 596 | 3,470 |
|
||||
| [v2.12.5](https://github.com/laurent22/joplin/releases/tag/v2.12.5) (p) | 2023-07-12T15:03:46Z | 2,134 | 172 | 164 | 2,470 |
|
||||
| [v2.12.4](https://github.com/laurent22/joplin/releases/tag/v2.12.4) (p) | 2023-07-07T22:36:53Z | 2,531 | 1,842 | 226 | 4,599 |
|
||||
| [v2.12.18](https://github.com/laurent22/joplin/releases/tag/v2.12.18) | 2023-09-22T14:37:24Z | 110,388 | 36,535 | 18,735 | 165,658 |
|
||||
| [v2.12.17](https://github.com/laurent22/joplin/releases/tag/v2.12.17) | 2023-09-14T21:54:52Z | 48,185 | 21,045 | 6,654 | 75,884 |
|
||||
| [v2.13.1](https://github.com/laurent22/joplin/releases/tag/v2.13.1) (p) | 2023-09-13T09:31:50Z | 1,396 | 431 | 690 | 2,517 |
|
||||
| [v2.12.16](https://github.com/laurent22/joplin/releases/tag/v2.12.16) | 2023-09-11T22:33:37Z | 28,829 | 14,678 | 2,472 | 45,979 |
|
||||
| [v2.12.15](https://github.com/laurent22/joplin/releases/tag/v2.12.15) | 2023-08-27T11:35:39Z | 65,826 | 28,205 | 8,501 | 102,532 |
|
||||
| [v2.12.12](https://github.com/laurent22/joplin/releases/tag/v2.12.12) (p) | 2023-08-19T22:44:56Z | 3,449 | 400 | 431 | 4,280 |
|
||||
| [v2.12.10](https://github.com/laurent22/joplin/releases/tag/v2.12.10) (p) | 2023-07-30T18:25:58Z | 8,097 | 3,821 | 918 | 12,836 |
|
||||
| [v2.12.9](https://github.com/laurent22/joplin/releases/tag/v2.12.9) (p) | 2023-07-25T16:06:08Z | 2,897 | 375 | 324 | 3,596 |
|
||||
| [v2.12.7](https://github.com/laurent22/joplin/releases/tag/v2.12.7) (p) | 2023-07-13T12:55:31Z | 2,209 | 669 | 596 | 3,474 |
|
||||
| [v2.12.5](https://github.com/laurent22/joplin/releases/tag/v2.12.5) (p) | 2023-07-12T15:03:46Z | 2,170 | 174 | 165 | 2,509 |
|
||||
| [v2.12.4](https://github.com/laurent22/joplin/releases/tag/v2.12.4) (p) | 2023-07-07T22:36:53Z | 2,820 | 2,136 | 226 | 5,182 |
|
||||
| [v2.12.3](https://github.com/laurent22/joplin/releases/tag/v2.12.3) (p) | 2023-07-07T10:16:55Z | 429 | 213 | 104 | 746 |
|
||||
| [v2.11.11](https://github.com/laurent22/joplin/releases/tag/v2.11.11) | 2023-06-23T15:16:37Z | 190,720 | 67,262 | 38,943 | 296,925 |
|
||||
| [v2.11.11](https://github.com/laurent22/joplin/releases/tag/v2.11.11) | 2023-06-23T15:16:37Z | 190,946 | 67,271 | 38,964 | 297,181 |
|
||||
| [v2.11.9](https://github.com/laurent22/joplin/releases/tag/v2.11.9) (p) | 2023-06-06T16:23:27Z | 2,321 | 583 | 754 | 3,658 |
|
||||
| [v2.11.6](https://github.com/laurent22/joplin/releases/tag/v2.11.6) (p) | 2023-05-31T20:13:08Z | 1,184 | 443 | 353 | 1,980 |
|
||||
| [v2.11.5](https://github.com/laurent22/joplin/releases/tag/v2.11.5) (p) | 2023-05-28T00:41:40Z | 1,045 | 315 | 291 | 1,651 |
|
||||
| [v2.10.19](https://github.com/laurent22/joplin/releases/tag/v2.10.19) | 2023-05-17T12:25:41Z | 125,768 | 48,356 | 22,501 | 196,625 |
|
||||
| [v2.10.19](https://github.com/laurent22/joplin/releases/tag/v2.10.19) | 2023-05-17T12:25:41Z | 125,850 | 48,358 | 22,505 | 196,713 |
|
||||
| [v2.11.4](https://github.com/laurent22/joplin/releases/tag/v2.11.4) (p) | 2023-05-16T10:02:21Z | 1,098 | 477 | 424 | 1,999 |
|
||||
| [v2.11.3](https://github.com/laurent22/joplin/releases/tag/v2.11.3) (p) | 2023-05-16T09:09:57Z | 157 | 49 | 46 | 252 |
|
||||
| [v2.10.18](https://github.com/laurent22/joplin/releases/tag/v2.10.18) | 2023-05-09T13:27:43Z | 57,464 | 24,272 | 6,818 | 88,554 |
|
||||
| [v2.10.17](https://github.com/laurent22/joplin/releases/tag/v2.10.17) | 2023-05-08T17:27:28Z | 19,826 | 11,518 | 900 | 32,244 |
|
||||
| [v2.10.16](https://github.com/laurent22/joplin/releases/tag/v2.10.16) | 2023-04-27T09:27:45Z | 10,413 | 4,265 | 791 | 15,469 |
|
||||
| [v2.11.3](https://github.com/laurent22/joplin/releases/tag/v2.11.3) (p) | 2023-05-16T09:09:57Z | 158 | 49 | 46 | 253 |
|
||||
| [v2.10.18](https://github.com/laurent22/joplin/releases/tag/v2.10.18) | 2023-05-09T13:27:43Z | 57,557 | 24,272 | 6,818 | 88,647 |
|
||||
| [v2.10.17](https://github.com/laurent22/joplin/releases/tag/v2.10.17) | 2023-05-08T17:27:28Z | 19,888 | 11,518 | 900 | 32,306 |
|
||||
| [v2.10.16](https://github.com/laurent22/joplin/releases/tag/v2.10.16) | 2023-04-27T09:27:45Z | 10,497 | 4,265 | 792 | 15,554 |
|
||||
| [v2.10.15](https://github.com/laurent22/joplin/releases/tag/v2.10.15) (p) | 2023-04-26T22:02:16Z | 392 | 149 | 65 | 606 |
|
||||
| [v2.10.13](https://github.com/laurent22/joplin/releases/tag/v2.10.13) (p) | 2023-04-03T16:53:46Z | 5,169 | 831 | 1,081 | 7,081 |
|
||||
| [v2.10.12](https://github.com/laurent22/joplin/releases/tag/v2.10.12) (p) | 2023-03-23T12:17:13Z | 4,133 | 521 | 607 | 5,261 |
|
||||
| [v2.10.11](https://github.com/laurent22/joplin/releases/tag/v2.10.11) (p) | 2023-03-17T10:54:02Z | 3,504 | 386 | 409 | 4,299 |
|
||||
| [v2.10.10](https://github.com/laurent22/joplin/releases/tag/v2.10.10) (p) | 2023-03-13T23:16:37Z | 2,998 | 286 | 258 | 3,542 |
|
||||
| [v2.10.9](https://github.com/laurent22/joplin/releases/tag/v2.10.9) (p) | 2023-03-12T16:16:45Z | 2,544 | 216 | 299 | 3,059 |
|
||||
| [v2.10.8](https://github.com/laurent22/joplin/releases/tag/v2.10.8) (p) | 2023-02-26T12:53:55Z | 5,023 | 574 | 875 | 6,472 |
|
||||
| [v2.10.7](https://github.com/laurent22/joplin/releases/tag/v2.10.7) (p) | 2023-02-24T10:56:20Z | 2,699 | 193 | 282 | 3,174 |
|
||||
| [v2.10.6](https://github.com/laurent22/joplin/releases/tag/v2.10.6) (p) | 2023-02-20T14:00:05Z | 3,477 | 344 | 292 | 4,113 |
|
||||
| [v2.10.5](https://github.com/laurent22/joplin/releases/tag/v2.10.5) | 2023-01-16T15:00:53Z | 385 | 107 | 329 | 821 |
|
||||
| [v2.10.4](https://github.com/laurent22/joplin/releases/tag/v2.10.4) (p) | 2023-01-05T13:09:20Z | 8,701 | 1,306 | 1,815 | 11,822 |
|
||||
| [v2.10.3](https://github.com/laurent22/joplin/releases/tag/v2.10.3) (p) | 2022-12-31T15:53:23Z | 3,268 | 316 | 421 | 4,005 |
|
||||
| [v2.10.2](https://github.com/laurent22/joplin/releases/tag/v2.10.2) (p) | 2022-12-18T18:05:08Z | 4,702 | 594 | 643 | 5,939 |
|
||||
| [v2.9.17](https://github.com/laurent22/joplin/releases/tag/v2.9.17) | 2022-11-15T10:28:37Z | 336,575 | 108,809 | 83,411 | 528,795 |
|
||||
| [v2.9.12](https://github.com/laurent22/joplin/releases/tag/v2.9.12) (p) | 2022-11-01T17:06:05Z | 11,888 | 614 | 548 | 13,050 |
|
||||
| [v2.9.11](https://github.com/laurent22/joplin/releases/tag/v2.9.11) (p) | 2022-10-23T16:09:58Z | 4,029 | 533 | 764 | 5,326 |
|
||||
| [v2.9.4](https://github.com/laurent22/joplin/releases/tag/v2.9.4) (p) | 2022-08-18T16:52:26Z | 9,071 | 1,870 | 2,203 | 13,144 |
|
||||
| [v2.9.3](https://github.com/laurent22/joplin/releases/tag/v2.9.3) (p) | 2022-08-18T13:11:09Z | 368 | 95 | 278 | 741 |
|
||||
| [v2.10.13](https://github.com/laurent22/joplin/releases/tag/v2.10.13) (p) | 2023-04-03T16:53:46Z | 5,207 | 831 | 1,081 | 7,119 |
|
||||
| [v2.10.12](https://github.com/laurent22/joplin/releases/tag/v2.10.12) (p) | 2023-03-23T12:17:13Z | 4,177 | 521 | 607 | 5,305 |
|
||||
| [v2.10.11](https://github.com/laurent22/joplin/releases/tag/v2.10.11) (p) | 2023-03-17T10:54:02Z | 3,548 | 386 | 409 | 4,343 |
|
||||
| [v2.10.10](https://github.com/laurent22/joplin/releases/tag/v2.10.10) (p) | 2023-03-13T23:16:37Z | 3,039 | 286 | 258 | 3,583 |
|
||||
| [v2.10.9](https://github.com/laurent22/joplin/releases/tag/v2.10.9) (p) | 2023-03-12T16:16:45Z | 2,590 | 216 | 299 | 3,105 |
|
||||
| [v2.10.8](https://github.com/laurent22/joplin/releases/tag/v2.10.8) (p) | 2023-02-26T12:53:55Z | 5,070 | 574 | 875 | 6,519 |
|
||||
| [v2.10.7](https://github.com/laurent22/joplin/releases/tag/v2.10.7) (p) | 2023-02-24T10:56:20Z | 2,745 | 193 | 282 | 3,220 |
|
||||
| [v2.10.6](https://github.com/laurent22/joplin/releases/tag/v2.10.6) (p) | 2023-02-20T14:00:05Z | 3,531 | 344 | 292 | 4,167 |
|
||||
| [v2.10.5](https://github.com/laurent22/joplin/releases/tag/v2.10.5) | 2023-01-16T15:00:53Z | 388 | 108 | 330 | 826 |
|
||||
| [v2.10.4](https://github.com/laurent22/joplin/releases/tag/v2.10.4) (p) | 2023-01-05T13:09:20Z | 8,754 | 1,308 | 1,815 | 11,877 |
|
||||
| [v2.10.3](https://github.com/laurent22/joplin/releases/tag/v2.10.3) (p) | 2022-12-31T15:53:23Z | 3,323 | 316 | 421 | 4,060 |
|
||||
| [v2.10.2](https://github.com/laurent22/joplin/releases/tag/v2.10.2) (p) | 2022-12-18T18:05:08Z | 4,747 | 594 | 643 | 5,984 |
|
||||
| [v2.9.17](https://github.com/laurent22/joplin/releases/tag/v2.9.17) | 2022-11-15T10:28:37Z | 336,728 | 108,813 | 83,415 | 528,956 |
|
||||
| [v2.9.12](https://github.com/laurent22/joplin/releases/tag/v2.9.12) (p) | 2022-11-01T17:06:05Z | 11,926 | 614 | 548 | 13,088 |
|
||||
| [v2.9.11](https://github.com/laurent22/joplin/releases/tag/v2.9.11) (p) | 2022-10-23T16:09:58Z | 4,071 | 533 | 764 | 5,368 |
|
||||
| [v2.9.4](https://github.com/laurent22/joplin/releases/tag/v2.9.4) (p) | 2022-08-18T16:52:26Z | 9,112 | 1,871 | 2,203 | 13,186 |
|
||||
| [v2.9.3](https://github.com/laurent22/joplin/releases/tag/v2.9.3) (p) | 2022-08-18T13:11:09Z | 368 | 95 | 279 | 742 |
|
||||
| [v2.9.2](https://github.com/laurent22/joplin/releases/tag/v2.9.2) (p) | 2022-08-12T18:12:12Z | 1,539 | 450 | 0 | 1,989 |
|
||||
| [v2.9.1](https://github.com/laurent22/joplin/releases/tag/v2.9.1) (p) | 2022-07-11T09:59:32Z | 8,516 | 1,345 | 1,415 | 11,276 |
|
||||
| [v2.8.8](https://github.com/laurent22/joplin/releases/tag/v2.8.8) | 2022-05-17T14:48:06Z | 352,683 | 114,436 | 113,630 | 580,749 |
|
||||
| [v2.8.7](https://github.com/laurent22/joplin/releases/tag/v2.8.7) (p) | 2022-05-06T11:34:27Z | 5,028 | 368 | 432 | 5,828 |
|
||||
| [v2.8.6](https://github.com/laurent22/joplin/releases/tag/v2.8.6) (p) | 2022-05-03T10:08:25Z | 4,644 | 405 | 336 | 5,385 |
|
||||
| [v2.8.5](https://github.com/laurent22/joplin/releases/tag/v2.8.5) (p) | 2022-04-27T13:51:50Z | 4,746 | 373 | 360 | 5,479 |
|
||||
| [v2.8.4](https://github.com/laurent22/joplin/releases/tag/v2.8.4) (p) | 2022-04-19T18:00:09Z | 5,219 | 592 | 336 | 6,147 |
|
||||
| [v2.8.2](https://github.com/laurent22/joplin/releases/tag/v2.8.2) (p) | 2022-04-14T11:35:45Z | 4,641 | 282 | 284 | 5,207 |
|
||||
| [v2.7.15](https://github.com/laurent22/joplin/releases/tag/v2.7.15) | 2022-03-17T13:03:23Z | 156,991 | 56,788 | 51,299 | 265,078 |
|
||||
| [v2.7.14](https://github.com/laurent22/joplin/releases/tag/v2.7.14) | 2022-02-27T11:30:53Z | 35,231 | 16,786 | 4,817 | 56,834 |
|
||||
| [v2.7.13](https://github.com/laurent22/joplin/releases/tag/v2.7.13) | 2022-02-24T17:42:12Z | 55,666 | 25,730 | 11,732 | 93,128 |
|
||||
| [v2.7.12](https://github.com/laurent22/joplin/releases/tag/v2.7.12) (p) | 2022-02-14T15:06:14Z | 5,413 | 467 | 507 | 6,387 |
|
||||
| [v2.7.11](https://github.com/laurent22/joplin/releases/tag/v2.7.11) (p) | 2022-02-12T13:00:02Z | 4,569 | 198 | 177 | 4,944 |
|
||||
| [v2.7.10](https://github.com/laurent22/joplin/releases/tag/v2.7.10) (p) | 2022-02-11T18:19:09Z | 4,065 | 129 | 98 | 4,292 |
|
||||
| [v2.7.8](https://github.com/laurent22/joplin/releases/tag/v2.7.8) (p) | 2022-01-19T09:35:27Z | 6,301 | 773 | 832 | 7,906 |
|
||||
| [v2.7.7](https://github.com/laurent22/joplin/releases/tag/v2.7.7) (p) | 2022-01-18T14:05:07Z | 4,400 | 159 | 146 | 4,705 |
|
||||
| [v2.7.6](https://github.com/laurent22/joplin/releases/tag/v2.7.6) (p) | 2022-01-17T17:08:28Z | 4,324 | 186 | 123 | 4,633 |
|
||||
| [v2.6.10](https://github.com/laurent22/joplin/releases/tag/v2.6.10) | 2021-12-19T11:31:16Z | 137,195 | 51,218 | 49,336 | 237,749 |
|
||||
| [v2.6.9](https://github.com/laurent22/joplin/releases/tag/v2.6.9) | 2021-12-17T11:57:32Z | 20,058 | 9,505 | 3,200 | 32,763 |
|
||||
| [v2.6.7](https://github.com/laurent22/joplin/releases/tag/v2.6.7) (p) | 2021-12-16T10:47:23Z | 4,538 | 182 | 114 | 4,834 |
|
||||
| [v2.6.6](https://github.com/laurent22/joplin/releases/tag/v2.6.6) (p) | 2021-12-13T12:31:43Z | 4,536 | 259 | 177 | 4,972 |
|
||||
| [v2.6.5](https://github.com/laurent22/joplin/releases/tag/v2.6.5) (p) | 2021-12-13T10:07:04Z | 3,857 | 54 | 39 | 3,950 |
|
||||
| [v2.6.4](https://github.com/laurent22/joplin/releases/tag/v2.6.4) (p) | 2021-12-09T19:53:43Z | 4,611 | 293 | 209 | 5,113 |
|
||||
| [v2.6.2](https://github.com/laurent22/joplin/releases/tag/v2.6.2) (p) | 2021-11-18T12:19:12Z | 6,381 | 795 | 704 | 7,880 |
|
||||
| [v2.5.12](https://github.com/laurent22/joplin/releases/tag/v2.5.12) | 2021-11-08T11:07:11Z | 83,868 | 32,511 | 25,244 | 141,623 |
|
||||
| [v2.5.10](https://github.com/laurent22/joplin/releases/tag/v2.5.10) | 2021-11-01T08:22:42Z | 47,903 | 19,054 | 10,102 | 77,059 |
|
||||
| [v2.5.8](https://github.com/laurent22/joplin/releases/tag/v2.5.8) | 2021-10-31T11:38:03Z | 16,607 | 6,584 | 2,336 | 25,527 |
|
||||
| [v2.5.7](https://github.com/laurent22/joplin/releases/tag/v2.5.7) (p) | 2021-10-29T14:47:33Z | 4,118 | 206 | 170 | 4,494 |
|
||||
| [v2.5.6](https://github.com/laurent22/joplin/releases/tag/v2.5.6) (p) | 2021-10-28T22:03:09Z | 4,174 | 180 | 110 | 4,464 |
|
||||
| [v2.5.4](https://github.com/laurent22/joplin/releases/tag/v2.5.4) (p) | 2021-10-19T10:10:54Z | 5,664 | 570 | 589 | 6,823 |
|
||||
| [v2.4.12](https://github.com/laurent22/joplin/releases/tag/v2.4.12) | 2021-10-13T17:24:34Z | 48,464 | 19,992 | 9,793 | 78,249 |
|
||||
| [v2.5.1](https://github.com/laurent22/joplin/releases/tag/v2.5.1) (p) | 2021-10-02T09:51:58Z | 6,757 | 909 | 949 | 8,615 |
|
||||
| [v2.4.9](https://github.com/laurent22/joplin/releases/tag/v2.4.9) | 2021-09-29T19:08:58Z | 60,151 | 23,269 | 15,937 | 99,357 |
|
||||
| [v2.4.8](https://github.com/laurent22/joplin/releases/tag/v2.4.8) (p) | 2021-09-22T19:01:46Z | 10,518 | 1,776 | 537 | 12,831 |
|
||||
| [v2.4.7](https://github.com/laurent22/joplin/releases/tag/v2.4.7) (p) | 2021-09-19T12:53:22Z | 4,674 | 261 | 213 | 5,148 |
|
||||
| [v2.4.6](https://github.com/laurent22/joplin/releases/tag/v2.4.6) (p) | 2021-09-09T18:57:17Z | 5,434 | 463 | 522 | 6,419 |
|
||||
| [v2.4.5](https://github.com/laurent22/joplin/releases/tag/v2.4.5) (p) | 2021-09-06T18:03:28Z | 4,677 | 279 | 227 | 5,183 |
|
||||
| [v2.4.4](https://github.com/laurent22/joplin/releases/tag/v2.4.4) (p) | 2021-08-30T16:02:51Z | 4,899 | 382 | 377 | 5,658 |
|
||||
| [v2.4.3](https://github.com/laurent22/joplin/releases/tag/v2.4.3) (p) | 2021-08-28T15:27:32Z | 4,377 | 209 | 184 | 4,770 |
|
||||
| [v2.4.2](https://github.com/laurent22/joplin/releases/tag/v2.4.2) (p) | 2021-08-27T17:13:21Z | 4,166 | 153 | 94 | 4,413 |
|
||||
| [v2.4.1](https://github.com/laurent22/joplin/releases/tag/v2.4.1) (p) | 2021-08-21T11:52:30Z | 4,971 | 374 | 339 | 5,684 |
|
||||
| [v2.3.5](https://github.com/laurent22/joplin/releases/tag/v2.3.5) | 2021-08-17T06:43:30Z | 86,297 | 31,466 | 33,171 | 150,934 |
|
||||
| [v2.3.3](https://github.com/laurent22/joplin/releases/tag/v2.3.3) | 2021-08-14T09:19:40Z | 19,360 | 6,889 | 4,065 | 30,314 |
|
||||
| [v2.2.7](https://github.com/laurent22/joplin/releases/tag/v2.2.7) | 2021-08-11T11:03:26Z | 19,255 | 7,527 | 2,610 | 29,392 |
|
||||
| [v2.2.6](https://github.com/laurent22/joplin/releases/tag/v2.2.6) (p) | 2021-08-09T19:29:20Z | 11,397 | 4,621 | 960 | 16,978 |
|
||||
| [v2.2.5](https://github.com/laurent22/joplin/releases/tag/v2.2.5) (p) | 2021-08-07T10:35:24Z | 4,718 | 280 | 210 | 5,208 |
|
||||
| [v2.2.4](https://github.com/laurent22/joplin/releases/tag/v2.2.4) (p) | 2021-08-05T16:42:48Z | 4,366 | 210 | 135 | 4,711 |
|
||||
| [v2.2.2](https://github.com/laurent22/joplin/releases/tag/v2.2.2) (p) | 2021-07-19T10:28:35Z | 6,324 | 739 | 650 | 7,713 |
|
||||
| [v2.1.9](https://github.com/laurent22/joplin/releases/tag/v2.1.9) | 2021-07-19T10:28:43Z | 50,903 | 18,970 | 16,825 | 86,698 |
|
||||
| [v2.2.1](https://github.com/laurent22/joplin/releases/tag/v2.2.1) (p) | 2021-07-09T17:38:25Z | 5,937 | 419 | 397 | 6,753 |
|
||||
| [v2.1.8](https://github.com/laurent22/joplin/releases/tag/v2.1.8) | 2021-07-03T08:25:16Z | 34,205 | 12,211 | 12,741 | 59,157 |
|
||||
| [v2.1.7](https://github.com/laurent22/joplin/releases/tag/v2.1.7) | 2021-06-26T19:48:55Z | 17,490 | 6,414 | 3,644 | 27,548 |
|
||||
| [v2.1.5](https://github.com/laurent22/joplin/releases/tag/v2.1.5) (p) | 2021-06-23T15:08:52Z | 4,811 | 256 | 204 | 5,271 |
|
||||
| [v2.1.3](https://github.com/laurent22/joplin/releases/tag/v2.1.3) (p) | 2021-06-19T16:32:51Z | 4,890 | 314 | 219 | 5,423 |
|
||||
| [v2.0.11](https://github.com/laurent22/joplin/releases/tag/v2.0.11) | 2021-06-16T17:55:49Z | 27,308 | 9,285 | 9,917 | 46,510 |
|
||||
| [v2.0.10](https://github.com/laurent22/joplin/releases/tag/v2.0.10) | 2021-06-16T07:58:29Z | 6,435 | 950 | 402 | 7,787 |
|
||||
| [v2.0.9](https://github.com/laurent22/joplin/releases/tag/v2.0.9) (p) | 2021-06-12T09:30:30Z | 4,900 | 310 | 903 | 6,113 |
|
||||
| [v2.0.8](https://github.com/laurent22/joplin/releases/tag/v2.0.8) (p) | 2021-06-10T16:15:08Z | 4,313 | 249 | 602 | 5,164 |
|
||||
| [v2.9.1](https://github.com/laurent22/joplin/releases/tag/v2.9.1) (p) | 2022-07-11T09:59:32Z | 8,557 | 1,345 | 1,415 | 11,317 |
|
||||
| [v2.8.8](https://github.com/laurent22/joplin/releases/tag/v2.8.8) | 2022-05-17T14:48:06Z | 352,778 | 114,442 | 113,633 | 580,853 |
|
||||
| [v2.8.7](https://github.com/laurent22/joplin/releases/tag/v2.8.7) (p) | 2022-05-06T11:34:27Z | 5,070 | 368 | 432 | 5,870 |
|
||||
| [v2.8.6](https://github.com/laurent22/joplin/releases/tag/v2.8.6) (p) | 2022-05-03T10:08:25Z | 4,686 | 405 | 336 | 5,427 |
|
||||
| [v2.8.5](https://github.com/laurent22/joplin/releases/tag/v2.8.5) (p) | 2022-04-27T13:51:50Z | 4,794 | 373 | 360 | 5,527 |
|
||||
| [v2.8.4](https://github.com/laurent22/joplin/releases/tag/v2.8.4) (p) | 2022-04-19T18:00:09Z | 5,263 | 592 | 336 | 6,191 |
|
||||
| [v2.8.2](https://github.com/laurent22/joplin/releases/tag/v2.8.2) (p) | 2022-04-14T11:35:45Z | 4,688 | 282 | 284 | 5,254 |
|
||||
| [v2.7.15](https://github.com/laurent22/joplin/releases/tag/v2.7.15) | 2022-03-17T13:03:23Z | 157,038 | 56,788 | 51,302 | 265,128 |
|
||||
| [v2.7.14](https://github.com/laurent22/joplin/releases/tag/v2.7.14) | 2022-02-27T11:30:53Z | 35,278 | 16,787 | 4,817 | 56,882 |
|
||||
| [v2.7.13](https://github.com/laurent22/joplin/releases/tag/v2.7.13) | 2022-02-24T17:42:12Z | 55,718 | 25,731 | 11,732 | 93,181 |
|
||||
| [v2.7.12](https://github.com/laurent22/joplin/releases/tag/v2.7.12) (p) | 2022-02-14T15:06:14Z | 5,452 | 467 | 507 | 6,426 |
|
||||
| [v2.7.11](https://github.com/laurent22/joplin/releases/tag/v2.7.11) (p) | 2022-02-12T13:00:02Z | 4,606 | 198 | 177 | 4,981 |
|
||||
| [v2.7.10](https://github.com/laurent22/joplin/releases/tag/v2.7.10) (p) | 2022-02-11T18:19:09Z | 4,108 | 129 | 98 | 4,335 |
|
||||
| [v2.7.8](https://github.com/laurent22/joplin/releases/tag/v2.7.8) (p) | 2022-01-19T09:35:27Z | 6,348 | 773 | 832 | 7,953 |
|
||||
| [v2.7.7](https://github.com/laurent22/joplin/releases/tag/v2.7.7) (p) | 2022-01-18T14:05:07Z | 4,439 | 159 | 146 | 4,744 |
|
||||
| [v2.7.6](https://github.com/laurent22/joplin/releases/tag/v2.7.6) (p) | 2022-01-17T17:08:28Z | 4,366 | 186 | 123 | 4,675 |
|
||||
| [v2.6.10](https://github.com/laurent22/joplin/releases/tag/v2.6.10) | 2021-12-19T11:31:16Z | 137,262 | 51,219 | 49,338 | 237,819 |
|
||||
| [v2.6.9](https://github.com/laurent22/joplin/releases/tag/v2.6.9) | 2021-12-17T11:57:32Z | 20,109 | 9,505 | 3,200 | 32,814 |
|
||||
| [v2.6.7](https://github.com/laurent22/joplin/releases/tag/v2.6.7) (p) | 2021-12-16T10:47:23Z | 4,579 | 182 | 115 | 4,876 |
|
||||
| [v2.6.6](https://github.com/laurent22/joplin/releases/tag/v2.6.6) (p) | 2021-12-13T12:31:43Z | 4,584 | 259 | 177 | 5,020 |
|
||||
| [v2.6.5](https://github.com/laurent22/joplin/releases/tag/v2.6.5) (p) | 2021-12-13T10:07:04Z | 3,897 | 54 | 39 | 3,990 |
|
||||
| [v2.6.4](https://github.com/laurent22/joplin/releases/tag/v2.6.4) (p) | 2021-12-09T19:53:43Z | 4,658 | 293 | 209 | 5,160 |
|
||||
| [v2.6.2](https://github.com/laurent22/joplin/releases/tag/v2.6.2) (p) | 2021-11-18T12:19:12Z | 6,428 | 795 | 704 | 7,927 |
|
||||
| [v2.5.12](https://github.com/laurent22/joplin/releases/tag/v2.5.12) | 2021-11-08T11:07:11Z | 83,917 | 32,512 | 25,245 | 141,674 |
|
||||
| [v2.5.10](https://github.com/laurent22/joplin/releases/tag/v2.5.10) | 2021-11-01T08:22:42Z | 47,957 | 19,054 | 10,103 | 77,114 |
|
||||
| [v2.5.8](https://github.com/laurent22/joplin/releases/tag/v2.5.8) | 2021-10-31T11:38:03Z | 16,658 | 6,584 | 2,337 | 25,579 |
|
||||
| [v2.5.7](https://github.com/laurent22/joplin/releases/tag/v2.5.7) (p) | 2021-10-29T14:47:33Z | 4,160 | 206 | 170 | 4,536 |
|
||||
| [v2.5.6](https://github.com/laurent22/joplin/releases/tag/v2.5.6) (p) | 2021-10-28T22:03:09Z | 4,217 | 180 | 110 | 4,507 |
|
||||
| [v2.5.4](https://github.com/laurent22/joplin/releases/tag/v2.5.4) (p) | 2021-10-19T10:10:54Z | 5,710 | 570 | 591 | 6,871 |
|
||||
| [v2.4.12](https://github.com/laurent22/joplin/releases/tag/v2.4.12) | 2021-10-13T17:24:34Z | 48,525 | 19,992 | 9,793 | 78,310 |
|
||||
| [v2.5.1](https://github.com/laurent22/joplin/releases/tag/v2.5.1) (p) | 2021-10-02T09:51:58Z | 6,804 | 909 | 949 | 8,662 |
|
||||
| [v2.4.9](https://github.com/laurent22/joplin/releases/tag/v2.4.9) | 2021-09-29T19:08:58Z | 60,201 | 23,269 | 15,944 | 99,414 |
|
||||
| [v2.4.8](https://github.com/laurent22/joplin/releases/tag/v2.4.8) (p) | 2021-09-22T19:01:46Z | 10,571 | 1,776 | 537 | 12,884 |
|
||||
| [v2.4.7](https://github.com/laurent22/joplin/releases/tag/v2.4.7) (p) | 2021-09-19T12:53:22Z | 4,725 | 261 | 213 | 5,199 |
|
||||
| [v2.4.6](https://github.com/laurent22/joplin/releases/tag/v2.4.6) (p) | 2021-09-09T18:57:17Z | 5,481 | 463 | 522 | 6,466 |
|
||||
| [v2.4.5](https://github.com/laurent22/joplin/releases/tag/v2.4.5) (p) | 2021-09-06T18:03:28Z | 4,720 | 279 | 227 | 5,226 |
|
||||
| [v2.4.4](https://github.com/laurent22/joplin/releases/tag/v2.4.4) (p) | 2021-08-30T16:02:51Z | 4,941 | 382 | 377 | 5,700 |
|
||||
| [v2.4.3](https://github.com/laurent22/joplin/releases/tag/v2.4.3) (p) | 2021-08-28T15:27:32Z | 4,426 | 209 | 184 | 4,819 |
|
||||
| [v2.4.2](https://github.com/laurent22/joplin/releases/tag/v2.4.2) (p) | 2021-08-27T17:13:21Z | 4,208 | 153 | 94 | 4,455 |
|
||||
| [v2.4.1](https://github.com/laurent22/joplin/releases/tag/v2.4.1) (p) | 2021-08-21T11:52:30Z | 5,014 | 374 | 339 | 5,727 |
|
||||
| [v2.3.5](https://github.com/laurent22/joplin/releases/tag/v2.3.5) | 2021-08-17T06:43:30Z | 86,356 | 31,472 | 33,172 | 151,000 |
|
||||
| [v2.3.3](https://github.com/laurent22/joplin/releases/tag/v2.3.3) | 2021-08-14T09:19:40Z | 19,419 | 6,889 | 4,065 | 30,373 |
|
||||
| [v2.2.7](https://github.com/laurent22/joplin/releases/tag/v2.2.7) | 2021-08-11T11:03:26Z | 19,302 | 7,527 | 2,610 | 29,439 |
|
||||
| [v2.2.6](https://github.com/laurent22/joplin/releases/tag/v2.2.6) (p) | 2021-08-09T19:29:20Z | 11,445 | 4,621 | 960 | 17,026 |
|
||||
| [v2.2.5](https://github.com/laurent22/joplin/releases/tag/v2.2.5) (p) | 2021-08-07T10:35:24Z | 4,759 | 280 | 210 | 5,249 |
|
||||
| [v2.2.4](https://github.com/laurent22/joplin/releases/tag/v2.2.4) (p) | 2021-08-05T16:42:48Z | 4,419 | 210 | 135 | 4,764 |
|
||||
| [v2.2.2](https://github.com/laurent22/joplin/releases/tag/v2.2.2) (p) | 2021-07-19T10:28:35Z | 6,366 | 739 | 650 | 7,755 |
|
||||
| [v2.1.9](https://github.com/laurent22/joplin/releases/tag/v2.1.9) | 2021-07-19T10:28:43Z | 50,975 | 18,974 | 16,829 | 86,778 |
|
||||
| [v2.2.1](https://github.com/laurent22/joplin/releases/tag/v2.2.1) (p) | 2021-07-09T17:38:25Z | 5,977 | 419 | 397 | 6,793 |
|
||||
| [v2.1.8](https://github.com/laurent22/joplin/releases/tag/v2.1.8) | 2021-07-03T08:25:16Z | 34,252 | 12,211 | 12,742 | 59,205 |
|
||||
| [v2.1.7](https://github.com/laurent22/joplin/releases/tag/v2.1.7) | 2021-06-26T19:48:55Z | 17,534 | 6,414 | 3,644 | 27,592 |
|
||||
| [v2.1.5](https://github.com/laurent22/joplin/releases/tag/v2.1.5) (p) | 2021-06-23T15:08:52Z | 4,853 | 256 | 204 | 5,313 |
|
||||
| [v2.1.3](https://github.com/laurent22/joplin/releases/tag/v2.1.3) (p) | 2021-06-19T16:32:51Z | 4,933 | 314 | 219 | 5,466 |
|
||||
| [v2.0.11](https://github.com/laurent22/joplin/releases/tag/v2.0.11) | 2021-06-16T17:55:49Z | 27,362 | 9,285 | 9,917 | 46,564 |
|
||||
| [v2.0.10](https://github.com/laurent22/joplin/releases/tag/v2.0.10) | 2021-06-16T07:58:29Z | 6,482 | 950 | 402 | 7,834 |
|
||||
| [v2.0.9](https://github.com/laurent22/joplin/releases/tag/v2.0.9) (p) | 2021-06-12T09:30:30Z | 4,956 | 312 | 904 | 6,172 |
|
||||
| [v2.0.8](https://github.com/laurent22/joplin/releases/tag/v2.0.8) (p) | 2021-06-10T16:15:08Z | 4,360 | 250 | 604 | 5,214 |
|
||||
| [v2.0.4](https://github.com/laurent22/joplin/releases/tag/v2.0.4) (p) | 2021-06-02T12:54:17Z | 1,647 | 406 | 395 | 2,448 |
|
||||
| [v2.0.2](https://github.com/laurent22/joplin/releases/tag/v2.0.2) (p) | 2021-05-21T18:07:48Z | 6,393 | 505 | 1,683 | 8,581 |
|
||||
| [v2.0.2](https://github.com/laurent22/joplin/releases/tag/v2.0.2) (p) | 2021-05-21T18:07:48Z | 6,441 | 505 | 1,683 | 8,629 |
|
||||
| [v2.0.1](https://github.com/laurent22/joplin/releases/tag/v2.0.1) (p) | 2021-05-15T13:22:58Z | 959 | 290 | 1,042 | 2,291 |
|
||||
| [v1.8.5](https://github.com/laurent22/joplin/releases/tag/v1.8.5) | 2021-05-10T11:58:14Z | 42,519 | 16,319 | 19,450 | 78,288 |
|
||||
| [v1.8.4](https://github.com/laurent22/joplin/releases/tag/v1.8.4) (p) | 2021-05-09T18:05:05Z | 4,250 | 154 | 475 | 4,879 |
|
||||
| [v1.8.3](https://github.com/laurent22/joplin/releases/tag/v1.8.3) (p) | 2021-05-04T10:38:16Z | 5,267 | 322 | 953 | 6,542 |
|
||||
| [v1.8.2](https://github.com/laurent22/joplin/releases/tag/v1.8.2) (p) | 2021-04-25T10:50:51Z | 5,549 | 453 | 1,304 | 7,306 |
|
||||
| [v1.8.1](https://github.com/laurent22/joplin/releases/tag/v1.8.1) (p) | 2021-03-29T10:46:41Z | 6,556 | 842 | 2,468 | 9,866 |
|
||||
| [v1.7.11](https://github.com/laurent22/joplin/releases/tag/v1.7.11) | 2021-02-03T12:50:01Z | 121,744 | 43,031 | 64,477 | 229,252 |
|
||||
| [v1.7.10](https://github.com/laurent22/joplin/releases/tag/v1.7.10) | 2021-01-30T13:25:29Z | 14,646 | 4,887 | 4,534 | 24,067 |
|
||||
| [v1.8.5](https://github.com/laurent22/joplin/releases/tag/v1.8.5) | 2021-05-10T11:58:14Z | 42,589 | 16,321 | 19,451 | 78,361 |
|
||||
| [v1.8.4](https://github.com/laurent22/joplin/releases/tag/v1.8.4) (p) | 2021-05-09T18:05:05Z | 4,306 | 154 | 475 | 4,935 |
|
||||
| [v1.8.3](https://github.com/laurent22/joplin/releases/tag/v1.8.3) (p) | 2021-05-04T10:38:16Z | 5,315 | 322 | 953 | 6,590 |
|
||||
| [v1.8.2](https://github.com/laurent22/joplin/releases/tag/v1.8.2) (p) | 2021-04-25T10:50:51Z | 5,594 | 453 | 1,304 | 7,351 |
|
||||
| [v1.8.1](https://github.com/laurent22/joplin/releases/tag/v1.8.1) (p) | 2021-03-29T10:46:41Z | 6,603 | 842 | 2,468 | 9,913 |
|
||||
| [v1.7.11](https://github.com/laurent22/joplin/releases/tag/v1.7.11) | 2021-02-03T12:50:01Z | 121,812 | 43,035 | 64,480 | 229,327 |
|
||||
| [v1.7.10](https://github.com/laurent22/joplin/releases/tag/v1.7.10) | 2021-01-30T13:25:29Z | 14,646 | 4,889 | 4,537 | 24,072 |
|
||||
| [v1.7.9](https://github.com/laurent22/joplin/releases/tag/v1.7.9) (p) | 2021-01-28T09:50:21Z | 536 | 150 | 516 | 1,202 |
|
||||
| [v1.7.6](https://github.com/laurent22/joplin/releases/tag/v1.7.6) (p) | 2021-01-27T10:36:05Z | 351 | 110 | 306 | 767 |
|
||||
| [v1.7.5](https://github.com/laurent22/joplin/releases/tag/v1.7.5) (p) | 2021-01-26T09:53:05Z | 444 | 221 | 470 | 1,135 |
|
||||
| [v1.7.4](https://github.com/laurent22/joplin/releases/tag/v1.7.4) (p) | 2021-01-22T17:58:38Z | 734 | 221 | 643 | 1,598 |
|
||||
| [v1.6.8](https://github.com/laurent22/joplin/releases/tag/v1.6.8) | 2021-01-20T18:11:34Z | 24,194 | 7,735 | 7,640 | 39,569 |
|
||||
| [v1.6.8](https://github.com/laurent22/joplin/releases/tag/v1.6.8) | 2021-01-20T18:11:34Z | 24,257 | 7,735 | 7,640 | 39,632 |
|
||||
| [v1.7.3](https://github.com/laurent22/joplin/releases/tag/v1.7.3) (p) | 2021-01-20T11:23:50Z | 388 | 97 | 461 | 946 |
|
||||
| [v1.6.7](https://github.com/laurent22/joplin/releases/tag/v1.6.7) | 2021-01-11T23:20:33Z | 15,564 | 4,672 | 4,576 | 24,812 |
|
||||
| [v1.6.6](https://github.com/laurent22/joplin/releases/tag/v1.6.6) | 2021-01-09T16:15:31Z | 12,872 | 3,444 | 4,823 | 21,139 |
|
||||
| [v1.6.5](https://github.com/laurent22/joplin/releases/tag/v1.6.5) (p) | 2021-01-09T01:24:32Z | 4,496 | 93 | 326 | 4,915 |
|
||||
| [v1.6.4](https://github.com/laurent22/joplin/releases/tag/v1.6.4) (p) | 2021-01-07T19:11:32Z | 427 | 95 | 222 | 744 |
|
||||
| [v1.6.7](https://github.com/laurent22/joplin/releases/tag/v1.6.7) | 2021-01-11T23:20:33Z | 15,628 | 4,672 | 4,576 | 24,876 |
|
||||
| [v1.6.6](https://github.com/laurent22/joplin/releases/tag/v1.6.6) | 2021-01-09T16:15:31Z | 12,872 | 3,445 | 4,825 | 21,142 |
|
||||
| [v1.6.5](https://github.com/laurent22/joplin/releases/tag/v1.6.5) (p) | 2021-01-09T01:24:32Z | 4,548 | 93 | 326 | 4,967 |
|
||||
| [v1.6.4](https://github.com/laurent22/joplin/releases/tag/v1.6.4) (p) | 2021-01-07T19:11:32Z | 428 | 95 | 222 | 745 |
|
||||
| [v1.6.2](https://github.com/laurent22/joplin/releases/tag/v1.6.2) (p) | 2021-01-04T22:34:55Z | 710 | 244 | 608 | 1,562 |
|
||||
| [v1.5.14](https://github.com/laurent22/joplin/releases/tag/v1.5.14) | 2020-12-30T01:48:46Z | 16,059 | 5,236 | 5,554 | 26,849 |
|
||||
| [v1.5.14](https://github.com/laurent22/joplin/releases/tag/v1.5.14) | 2020-12-30T01:48:46Z | 16,115 | 5,236 | 5,554 | 26,905 |
|
||||
| [v1.6.1](https://github.com/laurent22/joplin/releases/tag/v1.6.1) (p) | 2020-12-29T19:37:45Z | 207 | 55 | 188 | 450 |
|
||||
| [v1.5.13](https://github.com/laurent22/joplin/releases/tag/v1.5.13) | 2020-12-29T18:29:15Z | 793 | 245 | 231 | 1,269 |
|
||||
| [v1.5.12](https://github.com/laurent22/joplin/releases/tag/v1.5.12) | 2020-12-28T15:14:08Z | 2,576 | 1,799 | 946 | 5,321 |
|
||||
| [v1.5.11](https://github.com/laurent22/joplin/releases/tag/v1.5.11) | 2020-12-27T19:54:07Z | 14,426 | 4,665 | 4,321 | 23,412 |
|
||||
| [v1.5.13](https://github.com/laurent22/joplin/releases/tag/v1.5.13) | 2020-12-29T18:29:15Z | 793 | 245 | 232 | 1,270 |
|
||||
| [v1.5.12](https://github.com/laurent22/joplin/releases/tag/v1.5.12) | 2020-12-28T15:14:08Z | 2,576 | 1,800 | 946 | 5,322 |
|
||||
| [v1.5.11](https://github.com/laurent22/joplin/releases/tag/v1.5.11) | 2020-12-27T19:54:07Z | 14,428 | 4,665 | 4,321 | 23,414 |
|
||||
| [v1.5.10](https://github.com/laurent22/joplin/releases/tag/v1.5.10) (p) | 2020-12-26T12:35:36Z | 329 | 126 | 288 | 743 |
|
||||
| [v1.5.9](https://github.com/laurent22/joplin/releases/tag/v1.5.9) (p) | 2020-12-23T18:01:08Z | 363 | 388 | 429 | 1,180 |
|
||||
| [v1.5.8](https://github.com/laurent22/joplin/releases/tag/v1.5.8) (p) | 2020-12-20T09:45:19Z | 599 | 181 | 662 | 1,442 |
|
||||
| [v1.5.7](https://github.com/laurent22/joplin/releases/tag/v1.5.7) (p) | 2020-12-10T12:58:33Z | 925 | 271 | 1,012 | 2,208 |
|
||||
| [v1.5.4](https://github.com/laurent22/joplin/releases/tag/v1.5.4) (p) | 2020-12-05T12:07:49Z | 737 | 184 | 654 | 1,575 |
|
||||
| [v1.4.19](https://github.com/laurent22/joplin/releases/tag/v1.4.19) | 2020-12-01T11:11:16Z | 30,417 | 13,639 | 11,719 | 55,775 |
|
||||
| [v1.4.19](https://github.com/laurent22/joplin/releases/tag/v1.4.19) | 2020-12-01T11:11:16Z | 30,477 | 13,641 | 11,719 | 55,837 |
|
||||
| [v1.4.18](https://github.com/laurent22/joplin/releases/tag/v1.4.18) | 2020-11-28T12:21:41Z | 11,766 | 3,913 | 3,166 | 18,845 |
|
||||
| [v1.4.16](https://github.com/laurent22/joplin/releases/tag/v1.4.16) | 2020-11-27T19:40:16Z | 1,655 | 864 | 624 | 3,143 |
|
||||
| [v1.4.15](https://github.com/laurent22/joplin/releases/tag/v1.4.15) | 2020-11-27T13:25:43Z | 1,045 | 516 | 299 | 1,860 |
|
||||
| [v1.4.12](https://github.com/laurent22/joplin/releases/tag/v1.4.12) | 2020-11-23T18:58:07Z | 3,226 | 1,381 | 1,331 | 5,938 |
|
||||
| [v1.4.11](https://github.com/laurent22/joplin/releases/tag/v1.4.11) (p) | 2020-11-19T23:06:51Z | 5,309 | 195 | 613 | 6,117 |
|
||||
| [v1.4.11](https://github.com/laurent22/joplin/releases/tag/v1.4.11) (p) | 2020-11-19T23:06:51Z | 5,352 | 195 | 613 | 6,160 |
|
||||
| [v1.4.10](https://github.com/laurent22/joplin/releases/tag/v1.4.10) (p) | 2020-11-14T09:53:15Z | 701 | 235 | 705 | 1,641 |
|
||||
| [v1.4.9](https://github.com/laurent22/joplin/releases/tag/v1.4.9) (p) | 2020-11-11T14:23:17Z | 878 | 180 | 422 | 1,480 |
|
||||
| [v1.4.7](https://github.com/laurent22/joplin/releases/tag/v1.4.7) (p) | 2020-11-07T18:23:29Z | 564 | 211 | 536 | 1,311 |
|
||||
| [v1.3.18](https://github.com/laurent22/joplin/releases/tag/v1.3.18) | 2020-11-06T12:07:02Z | 36,018 | 11,393 | 10,551 | 57,962 |
|
||||
| [v1.3.18](https://github.com/laurent22/joplin/releases/tag/v1.3.18) | 2020-11-06T12:07:02Z | 36,082 | 11,393 | 10,551 | 58,026 |
|
||||
| [v1.3.17](https://github.com/laurent22/joplin/releases/tag/v1.3.17) (p) | 2020-11-06T11:35:15Z | 91 | 67 | 45 | 203 |
|
||||
| [v1.4.6](https://github.com/laurent22/joplin/releases/tag/v1.4.6) (p) | 2020-11-05T22:44:12Z | 783 | 131 | 72 | 986 |
|
||||
| [v1.3.15](https://github.com/laurent22/joplin/releases/tag/v1.3.15) | 2020-11-04T12:22:50Z | 2,835 | 1,349 | 874 | 5,058 |
|
||||
| [v1.3.15](https://github.com/laurent22/joplin/releases/tag/v1.3.15) | 2020-11-04T12:22:50Z | 2,839 | 1,349 | 874 | 5,062 |
|
||||
| [v1.3.11](https://github.com/laurent22/joplin/releases/tag/v1.3.11) (p) | 2020-10-31T13:22:20Z | 745 | 223 | 501 | 1,469 |
|
||||
| [v1.3.10](https://github.com/laurent22/joplin/releases/tag/v1.3.10) (p) | 2020-10-29T13:27:14Z | 418 | 155 | 336 | 909 |
|
||||
| [v1.3.9](https://github.com/laurent22/joplin/releases/tag/v1.3.9) (p) | 2020-10-23T16:04:26Z | 934 | 281 | 654 | 1,869 |
|
||||
@@ -257,113 +262,113 @@ updated: 2026-01-01T02:02:32Z
|
||||
| [v1.3.3](https://github.com/laurent22/joplin/releases/tag/v1.3.3) (p) | 2020-10-17T10:56:57Z | 161 | 84 | 54 | 299 |
|
||||
| [v1.3.2](https://github.com/laurent22/joplin/releases/tag/v1.3.2) (p) | 2020-10-11T20:39:49Z | 716 | 220 | 589 | 1,525 |
|
||||
| [v1.3.1](https://github.com/laurent22/joplin/releases/tag/v1.3.1) (p) | 2020-10-11T15:10:18Z | 126 | 90 | 65 | 281 |
|
||||
| [v1.2.6](https://github.com/laurent22/joplin/releases/tag/v1.2.6) | 2020-10-09T13:56:59Z | 50,028 | 17,797 | 14,097 | 81,922 |
|
||||
| [v1.2.6](https://github.com/laurent22/joplin/releases/tag/v1.2.6) | 2020-10-09T13:56:59Z | 50,092 | 17,797 | 14,097 | 81,986 |
|
||||
| [v1.2.4](https://github.com/laurent22/joplin/releases/tag/v1.2.4) (p) | 2020-09-30T07:34:29Z | 866 | 292 | 823 | 1,981 |
|
||||
| [v1.2.3](https://github.com/laurent22/joplin/releases/tag/v1.2.3) (p) | 2020-09-29T15:13:02Z | 263 | 103 | 105 | 471 |
|
||||
| [v1.2.2](https://github.com/laurent22/joplin/releases/tag/v1.2.2) (p) | 2020-09-22T20:31:55Z | 1,161 | 246 | 660 | 2,067 |
|
||||
| [v1.1.4](https://github.com/laurent22/joplin/releases/tag/v1.1.4) | 2020-09-21T11:20:09Z | 28,240 | 13,561 | 7,791 | 49,592 |
|
||||
| [v1.1.4](https://github.com/laurent22/joplin/releases/tag/v1.1.4) | 2020-09-21T11:20:09Z | 28,241 | 13,561 | 7,791 | 49,593 |
|
||||
| [v1.1.3](https://github.com/laurent22/joplin/releases/tag/v1.1.3) (p) | 2020-09-17T10:30:37Z | 631 | 192 | 486 | 1,309 |
|
||||
| [v1.1.2](https://github.com/laurent22/joplin/releases/tag/v1.1.2) (p) | 2020-09-15T12:58:38Z | 425 | 155 | 273 | 853 |
|
||||
| [v1.1.1](https://github.com/laurent22/joplin/releases/tag/v1.1.1) (p) | 2020-09-11T23:32:47Z | 602 | 235 | 373 | 1,210 |
|
||||
| [v1.0.245](https://github.com/laurent22/joplin/releases/tag/v1.0.245) | 2020-09-09T12:56:10Z | 23,073 | 10,070 | 5,678 | 38,821 |
|
||||
| [v1.0.245](https://github.com/laurent22/joplin/releases/tag/v1.0.245) | 2020-09-09T12:56:10Z | 23,079 | 10,070 | 5,678 | 38,827 |
|
||||
| [v1.0.242](https://github.com/laurent22/joplin/releases/tag/v1.0.242) | 2020-09-04T22:00:34Z | 12,912 | 6,467 | 3,049 | 22,428 |
|
||||
| [v1.0.241](https://github.com/laurent22/joplin/releases/tag/v1.0.241) | 2020-09-04T18:06:00Z | 26,693 | 5,992 | 5,154 | 37,839 |
|
||||
| [v1.0.241](https://github.com/laurent22/joplin/releases/tag/v1.0.241) | 2020-09-04T18:06:00Z | 26,696 | 5,994 | 5,157 | 37,847 |
|
||||
| [v1.0.239](https://github.com/laurent22/joplin/releases/tag/v1.0.239) (p) | 2020-09-01T21:56:36Z | 1,016 | 270 | 426 | 1,712 |
|
||||
| [v1.0.237](https://github.com/laurent22/joplin/releases/tag/v1.0.237) (p) | 2020-08-29T15:38:04Z | 639 | 965 | 361 | 1,965 |
|
||||
| [v1.0.236](https://github.com/laurent22/joplin/releases/tag/v1.0.236) (p) | 2020-08-28T09:16:54Z | 364 | 153 | 127 | 644 |
|
||||
| [v1.0.235](https://github.com/laurent22/joplin/releases/tag/v1.0.235) (p) | 2020-08-18T22:08:01Z | 2,089 | 530 | 945 | 3,564 |
|
||||
| [v1.0.234](https://github.com/laurent22/joplin/releases/tag/v1.0.234) (p) | 2020-08-17T23:13:02Z | 722 | 166 | 124 | 1,012 |
|
||||
| [v1.0.233](https://github.com/laurent22/joplin/releases/tag/v1.0.233) | 2020-08-01T14:51:15Z | 48,651 | 18,251 | 12,392 | 79,294 |
|
||||
| [v1.0.233](https://github.com/laurent22/joplin/releases/tag/v1.0.233) | 2020-08-01T14:51:15Z | 48,711 | 18,253 | 12,392 | 79,356 |
|
||||
| [v1.0.232](https://github.com/laurent22/joplin/releases/tag/v1.0.232) (p) | 2020-07-28T22:34:40Z | 705 | 265 | 202 | 1,172 |
|
||||
| [v1.0.227](https://github.com/laurent22/joplin/releases/tag/v1.0.227) | 2020-07-07T20:44:54Z | 42,020 | 15,328 | 9,680 | 67,028 |
|
||||
| [v1.0.227](https://github.com/laurent22/joplin/releases/tag/v1.0.227) | 2020-07-07T20:44:54Z | 42,024 | 15,328 | 9,680 | 67,032 |
|
||||
| [v1.0.226](https://github.com/laurent22/joplin/releases/tag/v1.0.226) (p) | 2020-07-04T10:21:26Z | 4,980 | 2,295 | 711 | 7,986 |
|
||||
| [v1.0.224](https://github.com/laurent22/joplin/releases/tag/v1.0.224) | 2020-06-20T22:26:08Z | 25,301 | 11,053 | 6,035 | 42,389 |
|
||||
| [v1.0.223](https://github.com/laurent22/joplin/releases/tag/v1.0.223) (p) | 2020-06-20T11:51:27Z | 233 | 154 | 102 | 489 |
|
||||
| [v1.0.221](https://github.com/laurent22/joplin/releases/tag/v1.0.221) (p) | 2020-06-20T01:44:20Z | 903 | 249 | 236 | 1,388 |
|
||||
| [v1.0.220](https://github.com/laurent22/joplin/releases/tag/v1.0.220) | 2020-06-13T18:26:22Z | 32,835 | 9,974 | 6,450 | 49,259 |
|
||||
| [v1.0.220](https://github.com/laurent22/joplin/releases/tag/v1.0.220) | 2020-06-13T18:26:22Z | 32,840 | 9,975 | 6,452 | 49,267 |
|
||||
| [v1.0.218](https://github.com/laurent22/joplin/releases/tag/v1.0.218) | 2020-06-07T10:43:34Z | 14,646 | 7,022 | 3,156 | 24,824 |
|
||||
| [v1.0.217](https://github.com/laurent22/joplin/releases/tag/v1.0.217) (p) | 2020-06-06T15:17:27Z | 283 | 140 | 86 | 509 |
|
||||
| [v1.0.216](https://github.com/laurent22/joplin/releases/tag/v1.0.216) | 2020-05-24T14:21:01Z | 42,409 | 14,364 | 10,229 | 67,002 |
|
||||
| [v1.0.214](https://github.com/laurent22/joplin/releases/tag/v1.0.214) (p) | 2020-05-21T17:15:15Z | 7,109 | 3,515 | 790 | 11,414 |
|
||||
| [v1.0.217](https://github.com/laurent22/joplin/releases/tag/v1.0.217) (p) | 2020-06-06T15:17:27Z | 283 | 141 | 87 | 511 |
|
||||
| [v1.0.216](https://github.com/laurent22/joplin/releases/tag/v1.0.216) | 2020-05-24T14:21:01Z | 42,470 | 14,365 | 10,230 | 67,065 |
|
||||
| [v1.0.214](https://github.com/laurent22/joplin/releases/tag/v1.0.214) (p) | 2020-05-21T17:15:15Z | 7,109 | 3,516 | 791 | 11,416 |
|
||||
| [v1.0.212](https://github.com/laurent22/joplin/releases/tag/v1.0.212) (p) | 2020-05-21T07:48:39Z | 257 | 114 | 73 | 444 |
|
||||
| [v1.0.211](https://github.com/laurent22/joplin/releases/tag/v1.0.211) (p) | 2020-05-20T08:59:16Z | 356 | 177 | 114 | 647 |
|
||||
| [v1.0.209](https://github.com/laurent22/joplin/releases/tag/v1.0.209) (p) | 2020-05-17T18:32:51Z | 1,451 | 896 | 174 | 2,521 |
|
||||
| [v1.0.207](https://github.com/laurent22/joplin/releases/tag/v1.0.207) (p) | 2020-05-10T16:37:35Z | 1,254 | 307 | 1,046 | 2,607 |
|
||||
| [v1.0.201](https://github.com/laurent22/joplin/releases/tag/v1.0.201) | 2020-04-15T22:55:13Z | 54,431 | 20,109 | 18,227 | 92,767 |
|
||||
| [v1.0.200](https://github.com/laurent22/joplin/releases/tag/v1.0.200) | 2020-04-12T12:17:46Z | 9,641 | 4,941 | 1,934 | 16,516 |
|
||||
| [v1.0.199](https://github.com/laurent22/joplin/releases/tag/v1.0.199) | 2020-04-10T18:41:58Z | 19,962 | 5,937 | 3,842 | 29,741 |
|
||||
| [v1.0.197](https://github.com/laurent22/joplin/releases/tag/v1.0.197) | 2020-03-30T17:21:22Z | 23,528 | 9,876 | 6,706 | 40,110 |
|
||||
| [v1.0.195](https://github.com/laurent22/joplin/releases/tag/v1.0.195) | 2020-03-22T19:56:12Z | 19,185 | 8,006 | 4,538 | 31,729 |
|
||||
| [v1.0.194](https://github.com/laurent22/joplin/releases/tag/v1.0.194) (p) | 2020-03-14T00:00:32Z | 1,344 | 1,434 | 544 | 3,322 |
|
||||
| [v1.0.193](https://github.com/laurent22/joplin/releases/tag/v1.0.193) | 2020-03-08T08:58:53Z | 28,852 | 10,974 | 7,453 | 47,279 |
|
||||
| [v1.0.192](https://github.com/laurent22/joplin/releases/tag/v1.0.192) (p) | 2020-03-06T23:27:52Z | 594 | 167 | 115 | 876 |
|
||||
| [v1.0.190](https://github.com/laurent22/joplin/releases/tag/v1.0.190) (p) | 2020-03-06T01:22:22Z | 469 | 136 | 111 | 716 |
|
||||
| [v1.0.189](https://github.com/laurent22/joplin/releases/tag/v1.0.189) (p) | 2020-03-04T17:27:15Z | 462 | 139 | 124 | 725 |
|
||||
| [v1.0.187](https://github.com/laurent22/joplin/releases/tag/v1.0.187) (p) | 2020-03-01T12:31:06Z | 985 | 281 | 299 | 1,565 |
|
||||
| [v1.0.179](https://github.com/laurent22/joplin/releases/tag/v1.0.179) | 2020-01-24T22:42:41Z | 71,846 | 29,142 | 22,606 | 123,594 |
|
||||
| [v1.0.178](https://github.com/laurent22/joplin/releases/tag/v1.0.178) | 2020-01-20T19:06:45Z | 17,682 | 6,014 | 2,623 | 26,319 |
|
||||
| [v1.0.177](https://github.com/laurent22/joplin/releases/tag/v1.0.177) (p) | 2019-12-30T14:40:40Z | 2,015 | 481 | 758 | 3,254 |
|
||||
| [v1.0.176](https://github.com/laurent22/joplin/releases/tag/v1.0.176) (p) | 2019-12-14T10:36:44Z | 3,179 | 2,582 | 497 | 6,258 |
|
||||
| [v1.0.175](https://github.com/laurent22/joplin/releases/tag/v1.0.175) | 2019-12-08T11:48:47Z | 73,813 | 17,029 | 16,633 | 107,475 |
|
||||
| [v1.0.174](https://github.com/laurent22/joplin/releases/tag/v1.0.174) | 2019-11-12T18:20:58Z | 30,786 | 11,812 | 8,258 | 50,856 |
|
||||
| [v1.0.173](https://github.com/laurent22/joplin/releases/tag/v1.0.173) | 2019-11-11T08:33:35Z | 5,159 | 2,128 | 776 | 8,063 |
|
||||
| [v1.0.170](https://github.com/laurent22/joplin/releases/tag/v1.0.170) | 2019-10-13T22:13:04Z | 27,922 | 8,837 | 7,721 | 44,480 |
|
||||
| [v1.0.169](https://github.com/laurent22/joplin/releases/tag/v1.0.169) | 2019-09-27T18:35:13Z | 17,300 | 5,977 | 3,787 | 27,064 |
|
||||
| [v1.0.168](https://github.com/laurent22/joplin/releases/tag/v1.0.168) | 2019-09-25T21:21:38Z | 5,400 | 2,320 | 749 | 8,469 |
|
||||
| [v1.0.167](https://github.com/laurent22/joplin/releases/tag/v1.0.167) | 2019-09-10T08:48:37Z | 17,028 | 5,751 | 3,745 | 26,524 |
|
||||
| [v1.0.211](https://github.com/laurent22/joplin/releases/tag/v1.0.211) (p) | 2020-05-20T08:59:16Z | 356 | 178 | 115 | 649 |
|
||||
| [v1.0.209](https://github.com/laurent22/joplin/releases/tag/v1.0.209) (p) | 2020-05-17T18:32:51Z | 1,451 | 897 | 175 | 2,523 |
|
||||
| [v1.0.207](https://github.com/laurent22/joplin/releases/tag/v1.0.207) (p) | 2020-05-10T16:37:35Z | 1,254 | 308 | 1,047 | 2,609 |
|
||||
| [v1.0.201](https://github.com/laurent22/joplin/releases/tag/v1.0.201) | 2020-04-15T22:55:13Z | 54,431 | 20,112 | 18,229 | 92,772 |
|
||||
| [v1.0.200](https://github.com/laurent22/joplin/releases/tag/v1.0.200) | 2020-04-12T12:17:46Z | 9,641 | 4,943 | 1,935 | 16,519 |
|
||||
| [v1.0.199](https://github.com/laurent22/joplin/releases/tag/v1.0.199) | 2020-04-10T18:41:58Z | 19,973 | 5,938 | 3,847 | 29,758 |
|
||||
| [v1.0.197](https://github.com/laurent22/joplin/releases/tag/v1.0.197) | 2020-03-30T17:21:22Z | 23,529 | 9,877 | 6,725 | 40,131 |
|
||||
| [v1.0.195](https://github.com/laurent22/joplin/releases/tag/v1.0.195) | 2020-03-22T19:56:12Z | 19,185 | 8,010 | 4,541 | 31,736 |
|
||||
| [v1.0.194](https://github.com/laurent22/joplin/releases/tag/v1.0.194) (p) | 2020-03-14T00:00:32Z | 1,344 | 1,435 | 545 | 3,324 |
|
||||
| [v1.0.193](https://github.com/laurent22/joplin/releases/tag/v1.0.193) | 2020-03-08T08:58:53Z | 28,852 | 10,986 | 7,466 | 47,304 |
|
||||
| [v1.0.192](https://github.com/laurent22/joplin/releases/tag/v1.0.192) (p) | 2020-03-06T23:27:52Z | 596 | 168 | 117 | 881 |
|
||||
| [v1.0.190](https://github.com/laurent22/joplin/releases/tag/v1.0.190) (p) | 2020-03-06T01:22:22Z | 471 | 136 | 111 | 718 |
|
||||
| [v1.0.189](https://github.com/laurent22/joplin/releases/tag/v1.0.189) (p) | 2020-03-04T17:27:15Z | 464 | 140 | 125 | 729 |
|
||||
| [v1.0.187](https://github.com/laurent22/joplin/releases/tag/v1.0.187) (p) | 2020-03-01T12:31:06Z | 985 | 282 | 300 | 1,567 |
|
||||
| [v1.0.179](https://github.com/laurent22/joplin/releases/tag/v1.0.179) | 2020-01-24T22:42:41Z | 71,852 | 29,156 | 22,619 | 123,627 |
|
||||
| [v1.0.178](https://github.com/laurent22/joplin/releases/tag/v1.0.178) | 2020-01-20T19:06:45Z | 17,682 | 6,015 | 2,625 | 26,322 |
|
||||
| [v1.0.177](https://github.com/laurent22/joplin/releases/tag/v1.0.177) (p) | 2019-12-30T14:40:40Z | 2,015 | 482 | 761 | 3,258 |
|
||||
| [v1.0.176](https://github.com/laurent22/joplin/releases/tag/v1.0.176) (p) | 2019-12-14T10:36:44Z | 3,179 | 2,582 | 498 | 6,259 |
|
||||
| [v1.0.175](https://github.com/laurent22/joplin/releases/tag/v1.0.175) | 2019-12-08T11:48:47Z | 73,815 | 17,030 | 16,635 | 107,480 |
|
||||
| [v1.0.174](https://github.com/laurent22/joplin/releases/tag/v1.0.174) | 2019-11-12T18:20:58Z | 30,786 | 11,813 | 8,259 | 50,858 |
|
||||
| [v1.0.173](https://github.com/laurent22/joplin/releases/tag/v1.0.173) | 2019-11-11T08:33:35Z | 5,160 | 2,128 | 776 | 8,064 |
|
||||
| [v1.0.170](https://github.com/laurent22/joplin/releases/tag/v1.0.170) | 2019-10-13T22:13:04Z | 27,924 | 8,839 | 7,722 | 44,485 |
|
||||
| [v1.0.169](https://github.com/laurent22/joplin/releases/tag/v1.0.169) | 2019-09-27T18:35:13Z | 17,300 | 5,979 | 3,789 | 27,068 |
|
||||
| [v1.0.168](https://github.com/laurent22/joplin/releases/tag/v1.0.168) | 2019-09-25T21:21:38Z | 5,400 | 2,321 | 750 | 8,471 |
|
||||
| [v1.0.167](https://github.com/laurent22/joplin/releases/tag/v1.0.167) | 2019-09-10T08:48:37Z | 17,030 | 5,754 | 3,747 | 26,531 |
|
||||
| [v1.0.166](https://github.com/laurent22/joplin/releases/tag/v1.0.166) | 2019-09-09T17:35:54Z | 2,010 | 607 | 263 | 2,880 |
|
||||
| [v1.0.165](https://github.com/laurent22/joplin/releases/tag/v1.0.165) | 2019-08-14T21:46:29Z | 19,128 | 7,026 | 5,494 | 31,648 |
|
||||
| [v1.0.161](https://github.com/laurent22/joplin/releases/tag/v1.0.161) | 2019-07-13T18:30:00Z | 19,461 | 6,395 | 4,163 | 30,019 |
|
||||
| [v1.0.160](https://github.com/laurent22/joplin/releases/tag/v1.0.160) | 2019-06-15T00:21:40Z | 30,893 | 7,801 | 8,139 | 46,833 |
|
||||
| [v1.0.159](https://github.com/laurent22/joplin/releases/tag/v1.0.159) | 2019-06-08T00:00:19Z | 5,298 | 2,224 | 1,323 | 8,845 |
|
||||
| [v1.0.158](https://github.com/laurent22/joplin/releases/tag/v1.0.158) | 2019-05-27T19:01:18Z | 9,895 | 3,595 | 1,964 | 15,454 |
|
||||
| [v1.0.157](https://github.com/laurent22/joplin/releases/tag/v1.0.157) | 2019-05-26T17:55:53Z | 2,254 | 885 | 316 | 3,455 |
|
||||
| [v1.0.153](https://github.com/laurent22/joplin/releases/tag/v1.0.153) (p) | 2019-05-15T06:27:29Z | 918 | 145 | 133 | 1,196 |
|
||||
| [v1.0.165](https://github.com/laurent22/joplin/releases/tag/v1.0.165) | 2019-08-14T21:46:29Z | 19,129 | 7,028 | 5,495 | 31,652 |
|
||||
| [v1.0.161](https://github.com/laurent22/joplin/releases/tag/v1.0.161) | 2019-07-13T18:30:00Z | 19,463 | 6,396 | 4,164 | 30,023 |
|
||||
| [v1.0.160](https://github.com/laurent22/joplin/releases/tag/v1.0.160) | 2019-06-15T00:21:40Z | 30,895 | 7,802 | 8,140 | 46,837 |
|
||||
| [v1.0.159](https://github.com/laurent22/joplin/releases/tag/v1.0.159) | 2019-06-08T00:00:19Z | 5,299 | 2,225 | 1,324 | 8,848 |
|
||||
| [v1.0.158](https://github.com/laurent22/joplin/releases/tag/v1.0.158) | 2019-05-27T19:01:18Z | 9,895 | 3,596 | 1,965 | 15,456 |
|
||||
| [v1.0.157](https://github.com/laurent22/joplin/releases/tag/v1.0.157) | 2019-05-26T17:55:53Z | 2,254 | 886 | 317 | 3,457 |
|
||||
| [v1.0.153](https://github.com/laurent22/joplin/releases/tag/v1.0.153) (p) | 2019-05-15T06:27:29Z | 918 | 146 | 134 | 1,198 |
|
||||
| [v1.0.152](https://github.com/laurent22/joplin/releases/tag/v1.0.152) | 2019-05-13T09:08:07Z | 13,954 | 4,482 | 4,096 | 22,532 |
|
||||
| [v1.0.151](https://github.com/laurent22/joplin/releases/tag/v1.0.151) | 2019-05-12T15:14:32Z | 2,007 | 579 | 987 | 3,573 |
|
||||
| [v1.0.150](https://github.com/laurent22/joplin/releases/tag/v1.0.150) | 2019-05-12T11:27:48Z | 500 | 179 | 98 | 777 |
|
||||
| [v1.0.148](https://github.com/laurent22/joplin/releases/tag/v1.0.148) (p) | 2019-05-08T19:12:24Z | 188 | 95 | 121 | 404 |
|
||||
| [v1.0.145](https://github.com/laurent22/joplin/releases/tag/v1.0.145) | 2019-05-03T09:16:53Z | 7,087 | 2,905 | 1,468 | 11,460 |
|
||||
| [v1.0.143](https://github.com/laurent22/joplin/releases/tag/v1.0.143) | 2019-04-22T10:51:38Z | 11,998 | 3,597 | 2,813 | 18,408 |
|
||||
| [v1.0.142](https://github.com/laurent22/joplin/releases/tag/v1.0.142) | 2019-04-02T16:44:51Z | 14,873 | 4,612 | 4,762 | 24,247 |
|
||||
| [v1.0.140](https://github.com/laurent22/joplin/releases/tag/v1.0.140) | 2019-03-10T20:59:58Z | 13,757 | 4,223 | 3,410 | 21,390 |
|
||||
| [v1.0.139](https://github.com/laurent22/joplin/releases/tag/v1.0.139) (p) | 2019-03-09T10:06:48Z | 190 | 106 | 70 | 366 |
|
||||
| [v1.0.138](https://github.com/laurent22/joplin/releases/tag/v1.0.138) (p) | 2019-03-03T17:23:00Z | 262 | 131 | 108 | 501 |
|
||||
| [v1.0.137](https://github.com/laurent22/joplin/releases/tag/v1.0.137) (p) | 2019-03-03T01:12:51Z | 652 | 98 | 108 | 858 |
|
||||
| [v1.0.135](https://github.com/laurent22/joplin/releases/tag/v1.0.135) | 2019-02-27T23:36:57Z | 12,708 | 4,004 | 4,112 | 20,824 |
|
||||
| [v1.0.134](https://github.com/laurent22/joplin/releases/tag/v1.0.134) | 2019-02-27T10:21:44Z | 1,516 | 614 | 246 | 2,376 |
|
||||
| [v1.0.132](https://github.com/laurent22/joplin/releases/tag/v1.0.132) | 2019-02-26T23:02:05Z | 1,154 | 496 | 121 | 1,771 |
|
||||
| [v1.0.127](https://github.com/laurent22/joplin/releases/tag/v1.0.127) | 2019-02-14T23:12:48Z | 10,040 | 3,219 | 2,963 | 16,222 |
|
||||
| [v1.0.126](https://github.com/laurent22/joplin/releases/tag/v1.0.126) (p) | 2019-02-09T19:46:16Z | 1,003 | 118 | 139 | 1,260 |
|
||||
| [v1.0.125](https://github.com/laurent22/joplin/releases/tag/v1.0.125) | 2019-01-26T18:14:33Z | 10,452 | 3,603 | 1,728 | 15,783 |
|
||||
| [v1.0.120](https://github.com/laurent22/joplin/releases/tag/v1.0.120) | 2019-01-10T21:42:53Z | 15,710 | 5,258 | 6,551 | 27,519 |
|
||||
| [v1.0.119](https://github.com/laurent22/joplin/releases/tag/v1.0.119) | 2018-12-18T12:40:22Z | 9,043 | 3,306 | 2,045 | 14,394 |
|
||||
| [v1.0.150](https://github.com/laurent22/joplin/releases/tag/v1.0.150) | 2019-05-12T11:27:48Z | 500 | 180 | 99 | 779 |
|
||||
| [v1.0.148](https://github.com/laurent22/joplin/releases/tag/v1.0.148) (p) | 2019-05-08T19:12:24Z | 191 | 98 | 123 | 412 |
|
||||
| [v1.0.145](https://github.com/laurent22/joplin/releases/tag/v1.0.145) | 2019-05-03T09:16:53Z | 7,087 | 2,906 | 1,469 | 11,462 |
|
||||
| [v1.0.143](https://github.com/laurent22/joplin/releases/tag/v1.0.143) | 2019-04-22T10:51:38Z | 11,998 | 3,598 | 2,814 | 18,410 |
|
||||
| [v1.0.142](https://github.com/laurent22/joplin/releases/tag/v1.0.142) | 2019-04-02T16:44:51Z | 14,873 | 4,613 | 4,764 | 24,250 |
|
||||
| [v1.0.140](https://github.com/laurent22/joplin/releases/tag/v1.0.140) | 2019-03-10T20:59:58Z | 13,757 | 4,225 | 3,411 | 21,393 |
|
||||
| [v1.0.139](https://github.com/laurent22/joplin/releases/tag/v1.0.139) (p) | 2019-03-09T10:06:48Z | 190 | 107 | 71 | 368 |
|
||||
| [v1.0.138](https://github.com/laurent22/joplin/releases/tag/v1.0.138) (p) | 2019-03-03T17:23:00Z | 262 | 132 | 110 | 504 |
|
||||
| [v1.0.137](https://github.com/laurent22/joplin/releases/tag/v1.0.137) (p) | 2019-03-03T01:12:51Z | 652 | 99 | 110 | 861 |
|
||||
| [v1.0.135](https://github.com/laurent22/joplin/releases/tag/v1.0.135) | 2019-02-27T23:36:57Z | 12,709 | 4,005 | 4,113 | 20,827 |
|
||||
| [v1.0.134](https://github.com/laurent22/joplin/releases/tag/v1.0.134) | 2019-02-27T10:21:44Z | 1,516 | 615 | 247 | 2,378 |
|
||||
| [v1.0.132](https://github.com/laurent22/joplin/releases/tag/v1.0.132) | 2019-02-26T23:02:05Z | 1,154 | 497 | 122 | 1,773 |
|
||||
| [v1.0.127](https://github.com/laurent22/joplin/releases/tag/v1.0.127) | 2019-02-14T23:12:48Z | 10,044 | 3,220 | 2,965 | 16,229 |
|
||||
| [v1.0.126](https://github.com/laurent22/joplin/releases/tag/v1.0.126) (p) | 2019-02-09T19:46:16Z | 1,003 | 119 | 140 | 1,262 |
|
||||
| [v1.0.125](https://github.com/laurent22/joplin/releases/tag/v1.0.125) | 2019-01-26T18:14:33Z | 10,452 | 3,605 | 1,729 | 15,786 |
|
||||
| [v1.0.120](https://github.com/laurent22/joplin/releases/tag/v1.0.120) | 2019-01-10T21:42:53Z | 15,710 | 5,260 | 6,553 | 27,523 |
|
||||
| [v1.0.119](https://github.com/laurent22/joplin/releases/tag/v1.0.119) | 2018-12-18T12:40:22Z | 9,044 | 3,307 | 2,046 | 14,397 |
|
||||
| [v1.0.118](https://github.com/laurent22/joplin/releases/tag/v1.0.118) | 2019-01-11T08:34:13Z | 769 | 292 | 118 | 1,179 |
|
||||
| [v1.0.117](https://github.com/laurent22/joplin/releases/tag/v1.0.117) | 2018-11-24T12:05:24Z | 16,352 | 4,945 | 6,414 | 27,711 |
|
||||
| [v1.0.116](https://github.com/laurent22/joplin/releases/tag/v1.0.116) | 2018-11-20T19:09:24Z | 4,073 | 1,173 | 745 | 5,991 |
|
||||
| [v1.0.115](https://github.com/laurent22/joplin/releases/tag/v1.0.115) | 2018-11-16T16:52:02Z | 3,727 | 1,345 | 829 | 5,901 |
|
||||
| [v1.0.114](https://github.com/laurent22/joplin/releases/tag/v1.0.114) | 2018-10-24T20:14:10Z | 11,475 | 3,545 | 3,859 | 18,879 |
|
||||
| [v1.0.111](https://github.com/laurent22/joplin/releases/tag/v1.0.111) | 2018-09-30T20:15:09Z | 12,587 | 3,574 | 3,711 | 19,872 |
|
||||
| [v1.0.110](https://github.com/laurent22/joplin/releases/tag/v1.0.110) | 2018-09-29T12:29:21Z | 1,016 | 453 | 145 | 1,614 |
|
||||
| [v1.0.117](https://github.com/laurent22/joplin/releases/tag/v1.0.117) | 2018-11-24T12:05:24Z | 16,352 | 4,946 | 6,415 | 27,713 |
|
||||
| [v1.0.116](https://github.com/laurent22/joplin/releases/tag/v1.0.116) | 2018-11-20T19:09:24Z | 4,073 | 1,174 | 746 | 5,993 |
|
||||
| [v1.0.115](https://github.com/laurent22/joplin/releases/tag/v1.0.115) | 2018-11-16T16:52:02Z | 3,727 | 1,346 | 830 | 5,903 |
|
||||
| [v1.0.114](https://github.com/laurent22/joplin/releases/tag/v1.0.114) | 2018-10-24T20:14:10Z | 11,475 | 3,547 | 3,860 | 18,882 |
|
||||
| [v1.0.111](https://github.com/laurent22/joplin/releases/tag/v1.0.111) | 2018-09-30T20:15:09Z | 12,587 | 3,576 | 3,711 | 19,874 |
|
||||
| [v1.0.110](https://github.com/laurent22/joplin/releases/tag/v1.0.110) | 2018-09-29T12:29:21Z | 1,018 | 453 | 145 | 1,616 |
|
||||
| [v1.0.109](https://github.com/laurent22/joplin/releases/tag/v1.0.109) | 2018-09-27T18:01:41Z | 2,152 | 747 | 360 | 3,259 |
|
||||
| [v1.0.108](https://github.com/laurent22/joplin/releases/tag/v1.0.108) (p) | 2018-09-29T18:49:29Z | 68 | 63 | 39 | 170 |
|
||||
| [v1.0.107](https://github.com/laurent22/joplin/releases/tag/v1.0.107) | 2018-09-16T19:51:07Z | 7,213 | 2,180 | 1,740 | 11,133 |
|
||||
| [v1.0.106](https://github.com/laurent22/joplin/releases/tag/v1.0.106) | 2018-09-08T15:23:40Z | 4,605 | 1,499 | 343 | 6,447 |
|
||||
| [v1.0.105](https://github.com/laurent22/joplin/releases/tag/v1.0.105) | 2018-09-05T11:29:36Z | 4,706 | 1,638 | 1,487 | 7,831 |
|
||||
| [v1.0.104](https://github.com/laurent22/joplin/releases/tag/v1.0.104) | 2018-06-28T20:25:36Z | 15,214 | 4,747 | 7,421 | 27,382 |
|
||||
| [v1.0.104](https://github.com/laurent22/joplin/releases/tag/v1.0.104) | 2018-06-28T20:25:36Z | 15,214 | 4,749 | 7,423 | 27,386 |
|
||||
| [v1.0.103](https://github.com/laurent22/joplin/releases/tag/v1.0.103) | 2018-06-21T19:38:13Z | 2,134 | 930 | 707 | 3,771 |
|
||||
| [v1.0.101](https://github.com/laurent22/joplin/releases/tag/v1.0.101) | 2018-06-17T18:35:11Z | 1,379 | 652 | 438 | 2,469 |
|
||||
| [v1.0.100](https://github.com/laurent22/joplin/releases/tag/v1.0.100) | 2018-06-14T17:41:43Z | 965 | 481 | 276 | 1,722 |
|
||||
| [v1.0.99](https://github.com/laurent22/joplin/releases/tag/v1.0.99) | 2018-06-10T13:18:23Z | 1,336 | 646 | 411 | 2,393 |
|
||||
| [v1.0.97](https://github.com/laurent22/joplin/releases/tag/v1.0.97) | 2018-06-09T19:23:34Z | 362 | 197 | 89 | 648 |
|
||||
| [v1.0.96](https://github.com/laurent22/joplin/releases/tag/v1.0.96) | 2018-05-26T16:36:39Z | 2,805 | 1,271 | 1,756 | 5,832 |
|
||||
| [v1.0.96](https://github.com/laurent22/joplin/releases/tag/v1.0.96) | 2018-05-26T16:36:39Z | 2,806 | 1,271 | 1,756 | 5,833 |
|
||||
| [v1.0.95](https://github.com/laurent22/joplin/releases/tag/v1.0.95) | 2018-05-25T13:04:30Z | 473 | 262 | 168 | 903 |
|
||||
| [v1.0.94](https://github.com/laurent22/joplin/releases/tag/v1.0.94) | 2018-05-21T20:52:59Z | 1,221 | 633 | 450 | 2,304 |
|
||||
| [v1.0.93](https://github.com/laurent22/joplin/releases/tag/v1.0.93) | 2018-05-14T11:36:01Z | 1,886 | 1,385 | 808 | 4,079 |
|
||||
| [v1.0.94](https://github.com/laurent22/joplin/releases/tag/v1.0.94) | 2018-05-21T20:52:59Z | 1,221 | 633 | 451 | 2,305 |
|
||||
| [v1.0.93](https://github.com/laurent22/joplin/releases/tag/v1.0.93) | 2018-05-14T11:36:01Z | 1,886 | 1,391 | 808 | 4,085 |
|
||||
| [v1.0.91](https://github.com/laurent22/joplin/releases/tag/v1.0.91) | 2018-05-10T14:48:04Z | 875 | 598 | 368 | 1,841 |
|
||||
| [v1.0.89](https://github.com/laurent22/joplin/releases/tag/v1.0.89) | 2018-05-09T13:05:05Z | 551 | 290 | 161 | 1,002 |
|
||||
| [v1.0.89](https://github.com/laurent22/joplin/releases/tag/v1.0.89) | 2018-05-09T13:05:05Z | 552 | 291 | 161 | 1,004 |
|
||||
| [v1.0.85](https://github.com/laurent22/joplin/releases/tag/v1.0.85) | 2018-05-01T21:08:24Z | 1,702 | 997 | 681 | 3,380 |
|
||||
| [v1.0.83](https://github.com/laurent22/joplin/releases/tag/v1.0.83) | 2018-04-04T19:43:58Z | 5,647 | 2,581 | 2,708 | 10,936 |
|
||||
| [v1.0.83](https://github.com/laurent22/joplin/releases/tag/v1.0.83) | 2018-04-04T19:43:58Z | 5,649 | 2,581 | 2,708 | 10,938 |
|
||||
| [v1.0.82](https://github.com/laurent22/joplin/releases/tag/v1.0.82) | 2018-03-31T19:16:31Z | 754 | 454 | 171 | 1,379 |
|
||||
| [v1.0.81](https://github.com/laurent22/joplin/releases/tag/v1.0.81) | 2018-03-28T08:13:58Z | 1,042 | 641 | 831 | 2,514 |
|
||||
| [v1.0.79](https://github.com/laurent22/joplin/releases/tag/v1.0.79) | 2018-03-23T18:00:11Z | 974 | 584 | 433 | 1,991 |
|
||||
@@ -372,9 +377,9 @@ updated: 2026-01-01T02:02:32Z
|
||||
| [v1.0.72](https://github.com/laurent22/joplin/releases/tag/v1.0.72) | 2018-03-14T09:44:35Z | 456 | 300 | 102 | 858 |
|
||||
| [v1.0.70](https://github.com/laurent22/joplin/releases/tag/v1.0.70) | 2018-02-28T20:04:30Z | 2,009 | 1,099 | 1,301 | 4,409 |
|
||||
| [v1.0.67](https://github.com/laurent22/joplin/releases/tag/v1.0.67) | 2018-02-19T22:51:08Z | 1,950 | 649 | 0 | 2,599 |
|
||||
| [v1.0.66](https://github.com/laurent22/joplin/releases/tag/v1.0.66) | 2018-02-18T23:09:09Z | 453 | 178 | 111 | 742 |
|
||||
| [v1.0.66](https://github.com/laurent22/joplin/releases/tag/v1.0.66) | 2018-02-18T23:09:09Z | 454 | 178 | 111 | 743 |
|
||||
| [v1.0.65](https://github.com/laurent22/joplin/releases/tag/v1.0.65) | 2018-02-17T20:02:25Z | 352 | 176 | 158 | 686 |
|
||||
| [v1.0.64](https://github.com/laurent22/joplin/releases/tag/v1.0.64) | 2018-02-16T00:58:20Z | 1,200 | 587 | 1,151 | 2,938 |
|
||||
| [v1.0.64](https://github.com/laurent22/joplin/releases/tag/v1.0.64) | 2018-02-16T00:58:20Z | 1,201 | 587 | 1,151 | 2,939 |
|
||||
| [v1.0.63](https://github.com/laurent22/joplin/releases/tag/v1.0.63) | 2018-02-14T19:40:36Z | 416 | 204 | 119 | 739 |
|
||||
| [v1.0.62](https://github.com/laurent22/joplin/releases/tag/v1.0.62) | 2018-02-12T20:19:58Z | 717 | 351 | 403 | 1,471 |
|
||||
| [v0.10.61](https://github.com/laurent22/joplin/releases/tag/v0.10.61) | 2018-02-08T18:27:39Z | 1,130 | 684 | 990 | 2,804 |
|
||||
@@ -383,11 +388,11 @@ updated: 2026-01-01T02:02:32Z
|
||||
| [v0.10.52](https://github.com/laurent22/joplin/releases/tag/v0.10.52) | 2018-01-31T19:25:18Z | 167 | 682 | 45 | 894 |
|
||||
| [v0.10.51](https://github.com/laurent22/joplin/releases/tag/v0.10.51) | 2018-01-28T18:47:02Z | 1,436 | 1,648 | 355 | 3,439 |
|
||||
| [v0.10.48](https://github.com/laurent22/joplin/releases/tag/v0.10.48) | 2018-01-23T11:19:51Z | 2,115 | 1,800 | 59 | 3,974 |
|
||||
| [v0.10.47](https://github.com/laurent22/joplin/releases/tag/v0.10.47) | 2018-01-16T17:27:17Z | 1,337 | 1,321 | 94 | 2,752 |
|
||||
| [v0.10.43](https://github.com/laurent22/joplin/releases/tag/v0.10.43) | 2018-01-08T10:12:10Z | 3,492 | 2,408 | 1,247 | 7,147 |
|
||||
| [v0.10.41](https://github.com/laurent22/joplin/releases/tag/v0.10.41) | 2018-01-05T20:38:12Z | 1,249 | 1,605 | 270 | 3,124 |
|
||||
| [v0.10.47](https://github.com/laurent22/joplin/releases/tag/v0.10.47) | 2018-01-16T17:27:17Z | 1,338 | 1,321 | 94 | 2,753 |
|
||||
| [v0.10.43](https://github.com/laurent22/joplin/releases/tag/v0.10.43) | 2018-01-08T10:12:10Z | 3,496 | 2,411 | 1,248 | 7,155 |
|
||||
| [v0.10.41](https://github.com/laurent22/joplin/releases/tag/v0.10.41) | 2018-01-05T20:38:12Z | 1,249 | 1,605 | 271 | 3,125 |
|
||||
| [v0.10.40](https://github.com/laurent22/joplin/releases/tag/v0.10.40) | 2018-01-02T23:16:57Z | 1,643 | 1,836 | 366 | 3,845 |
|
||||
| [v0.10.39](https://github.com/laurent22/joplin/releases/tag/v0.10.39) | 2017-12-11T21:19:44Z | 6,001 | 4,451 | 3,336 | 13,788 |
|
||||
| [v0.10.39](https://github.com/laurent22/joplin/releases/tag/v0.10.39) | 2017-12-11T21:19:44Z | 6,004 | 4,451 | 3,337 | 13,792 |
|
||||
| [v0.10.38](https://github.com/laurent22/joplin/releases/tag/v0.10.38) | 2017-12-08T10:12:06Z | 1,094 | 1,279 | 332 | 2,705 |
|
||||
| [v0.10.37](https://github.com/laurent22/joplin/releases/tag/v0.10.37) | 2017-12-07T19:38:05Z | 294 | 897 | 113 | 1,304 |
|
||||
| [v0.10.36](https://github.com/laurent22/joplin/releases/tag/v0.10.36) | 2017-12-05T09:34:40Z | 1,057 | 1,409 | 468 | 2,934 |
|
||||
@@ -396,11 +401,11 @@ updated: 2026-01-01T02:02:32Z
|
||||
| [v0.10.33](https://github.com/laurent22/joplin/releases/tag/v0.10.33) | 2017-12-02T13:20:39Z | 100 | 718 | 54 | 872 |
|
||||
| [v0.10.31](https://github.com/laurent22/joplin/releases/tag/v0.10.31) | 2017-12-01T09:56:44Z | 922 | 1,505 | 442 | 2,869 |
|
||||
| [v0.10.30](https://github.com/laurent22/joplin/releases/tag/v0.10.30) | 2017-11-30T20:28:16Z | 849 | 1,434 | 454 | 2,737 |
|
||||
| [v0.10.28](https://github.com/laurent22/joplin/releases/tag/v0.10.28) | 2017-11-30T01:07:46Z | 1,499 | 1,768 | 910 | 4,177 |
|
||||
| [v0.10.28](https://github.com/laurent22/joplin/releases/tag/v0.10.28) | 2017-11-30T01:07:46Z | 1,500 | 1,768 | 910 | 4,178 |
|
||||
| [v0.10.26](https://github.com/laurent22/joplin/releases/tag/v0.10.26) | 2017-11-29T16:02:17Z | 311 | 763 | 294 | 1,368 |
|
||||
| [v0.10.25](https://github.com/laurent22/joplin/releases/tag/v0.10.25) | 2017-11-24T14:27:49Z | 263 | 758 | 6,815 | 7,836 |
|
||||
| [v0.10.25](https://github.com/laurent22/joplin/releases/tag/v0.10.25) | 2017-11-24T14:27:49Z | 265 | 758 | 6,820 | 7,843 |
|
||||
| [v0.10.23](https://github.com/laurent22/joplin/releases/tag/v0.10.23) | 2017-11-21T19:38:41Z | 254 | 720 | 70 | 1,044 |
|
||||
| [v0.10.22](https://github.com/laurent22/joplin/releases/tag/v0.10.22) | 2017-11-20T21:45:57Z | 198 | 707 | 52 | 957 |
|
||||
| [v0.10.21](https://github.com/laurent22/joplin/releases/tag/v0.10.21) | 2017-11-18T00:53:15Z | 174 | 699 | 47 | 920 |
|
||||
| [v0.10.20](https://github.com/laurent22/joplin/releases/tag/v0.10.20) | 2017-11-17T17:18:25Z | 167 | 712 | 60 | 939 |
|
||||
| [v0.10.19](https://github.com/laurent22/joplin/releases/tag/v0.10.19) | 2017-11-20T18:59:48Z | 192 | 714 | 58 | 964 |
|
||||
| [v0.10.19](https://github.com/laurent22/joplin/releases/tag/v0.10.19) | 2017-11-20T18:59:48Z | 194 | 716 | 59 | 969 |
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user