Merge branch 'dev' into sharing_bug_2
@@ -937,7 +937,6 @@ packages/default-plugins/commands/editPatch.js
|
||||
packages/default-plugins/utils/getCurrentCommitHash.js
|
||||
packages/default-plugins/utils/getPathToPatchFileFor.js
|
||||
packages/default-plugins/utils/readRepositoryJson.js
|
||||
packages/default-plugins/utils/waitForCliInput.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
|
||||
@@ -1649,6 +1648,18 @@ packages/tools/checkIgnoredFiles.js
|
||||
packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/types.js
|
||||
packages/tools/fuzzer/utils/SeededRandom.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
packages/tools/fuzzer/utils/retryWithCount.js
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.test.js
|
||||
|
@@ -23,6 +23,7 @@ module.exports = {
|
||||
'FileSystemCreateWritableOptions': 'readonly',
|
||||
'FileSystemHandle': 'readonly',
|
||||
'IDBTransactionMode': 'readonly',
|
||||
'BigInt': 'readonly',
|
||||
'globalThis': 'readonly',
|
||||
|
||||
// ServiceWorker
|
||||
|
13
.gitignore
vendored
@@ -912,7 +912,6 @@ packages/default-plugins/commands/editPatch.js
|
||||
packages/default-plugins/utils/getCurrentCommitHash.js
|
||||
packages/default-plugins/utils/getPathToPatchFileFor.js
|
||||
packages/default-plugins/utils/readRepositoryJson.js
|
||||
packages/default-plugins/utils/waitForCliInput.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
|
||||
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
|
||||
@@ -1624,6 +1623,18 @@ packages/tools/checkIgnoredFiles.js
|
||||
packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/types.js
|
||||
packages/tools/fuzzer/utils/SeededRandom.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
packages/tools/fuzzer/utils/retryWithCount.js
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.test.js
|
||||
|
@@ -1300,4 +1300,9 @@ footer .bottom-links-row p {
|
||||
|
||||
:lang(zh-cn) #plans-section .faq {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.cfa-button {
|
||||
margin-top: 10px;
|
||||
}
|
BIN
Assets/WebsiteAssets/images/joplin_server_business/main.png
Normal file
After Width: | Height: | Size: 430 KiB |
BIN
Assets/WebsiteAssets/images/joplin_server_business/publish.jpg
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
Assets/WebsiteAssets/images/joplin_server_business/self_host.jpg
Normal file
After Width: | Height: | Size: 434 KiB |
BIN
Assets/WebsiteAssets/images/joplin_server_business/share.jpg
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
Assets/WebsiteAssets/images/joplin_server_business/teams.jpg
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/BestEtf.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/Freespinny.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/HomeworkGuy.png
Normal file
After Width: | Height: | Size: 110 KiB |
@@ -1,24 +1,28 @@
|
||||
<div class="col-12 col-lg-4 account-type-{{priceMonthly.accountType}}">
|
||||
<div class="col-12 col-lg-4 account-type-{{priceMonthly.accountType}} hosting-type-{{hostingType}}">
|
||||
<div class="price-container {{#featured}}price-container-blue{{/featured}}">
|
||||
<div class="price-row">
|
||||
<div class="plan-type">
|
||||
<img src="{{imageBaseUrl}}/{{iconName}}.png"/> {{title}}
|
||||
<div class="price-row">
|
||||
<div class="plan-type">
|
||||
<img src="{{imageBaseUrl}}/{{iconName}}.png"/> {{title}}
|
||||
</div>
|
||||
|
||||
{{#priceMonthly.formattedMonthlyAmount}}
|
||||
<div class="plan-price plan-price-monthly">
|
||||
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month"> <span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-yearly">
|
||||
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month"> <span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
{{/priceMonthly.formattedMonthlyAmount}}
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-monthly">
|
||||
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month"> <span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
|
||||
{{#priceYearly.formattedMonthlyAmount}}
|
||||
<div class="plan-price-yearly-per-year">
|
||||
<div>
|
||||
({{priceYearly.formattedAmount}}<sub class="per-year"> <span translate>/year</span></sub>)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-yearly">
|
||||
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month"> <span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plan-price-yearly-per-year">
|
||||
<div>
|
||||
({{priceYearly.formattedAmount}}<sub class="per-year"> <span translate>/year</span></sub>)
|
||||
</div>
|
||||
</div>
|
||||
{{/priceYearly.formattedMonthlyAmount}}
|
||||
|
||||
{{#featureLabelsOn}}
|
||||
<p><i class="fas fa-check feature feature-on"></i>{{.}}</p>
|
||||
@@ -29,7 +33,11 @@
|
||||
{{/featureLabelsOff}}
|
||||
|
||||
<p class="text-center subscribe-wrapper">
|
||||
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton">{{cfaLabel}}</a>
|
||||
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton cfa-button">{{cfaLabel}}</a>
|
||||
|
||||
{{#learnMoreUrl}}
|
||||
<a id="learnMore-{{name}}" href="{{learnMoreUrl}}" class="button-link btn-white learnMoreButton cfa-button">Learn more</a>
|
||||
{{/learnMoreUrl}}
|
||||
</p>
|
||||
|
||||
{{#footnote}}<sub>(*) {{.}}</sub>{{/footnote}}
|
||||
|
@@ -1,23 +1,91 @@
|
||||
<div id="plans-section" class="env-{{env}}">
|
||||
<style>
|
||||
.toggle-container {
|
||||
display: flex;
|
||||
border: 2px solid black;
|
||||
border-radius: 100px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.toggle-option {
|
||||
flex: 1;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.toggle-container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 title-box">
|
||||
<h1 translate class="text-center">
|
||||
Joplin Cloud <span class="frame-bg frame-bg-yellow">plans</span>
|
||||
Our synchronisation and sharing <span class="frame-bg frame-bg-yellow">solutions</span>
|
||||
</h1>
|
||||
<p translate class="text-center sub-title">
|
||||
<a href="https://joplincloud.com">Joplin Cloud</a> allows you to synchronise your notes across devices. It also lets you publish notes, and collaborate on notebooks with your friends, family or colleagues.
|
||||
Synchronise and share your notes with our range of plans.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toggle-container" id="toggle">
|
||||
<div class="toggle-option active toggle-button-managed">Managed hosting</div>
|
||||
<div class="toggle-option inactive toggle-button-self">Self-hosting</div>
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
<div class="alert alert-danger alert-env-dev" role="alert" style='text-align: center; margin-top: 10px;'>
|
||||
To use this page please enable JavaScript!
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin-top: 1.2em">
|
||||
<div class="row hosting-type-managed">
|
||||
<div class="col-12 title-box">
|
||||
<h1 translate class="text-center">
|
||||
Joplin Cloud
|
||||
</h1>
|
||||
<p translate class="text-center sub-title">
|
||||
<a href="https://joplincloud.com">Joplin Cloud</a> allows you to synchronise your notes across devices. It also lets you publish notes, and collaborate on notebooks with your friends, family or colleagues.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row hosting-type-self">
|
||||
<div class="col-12 title-box">
|
||||
<h1 translate class="text-center">
|
||||
Joplin Server Business
|
||||
</h1>
|
||||
<p translate class="text-center sub-title">
|
||||
Joplin Server Business is a synchronisation server that you can install on your own infrastructure, so that your data remains private and secure within your business.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin-top: 1.2em" class="hosting-type-managed">
|
||||
<div class="form-check form-check-inline">
|
||||
<input id="pay-monthly-radio" class="form-check-input" type="radio" name="pay-radio" checked value="monthly">
|
||||
<label translate style="font-weight: bold" class="form-check-label" for="pay-monthly-radio">
|
||||
@@ -46,7 +114,11 @@
|
||||
{{> plan}}
|
||||
{{/plans.teams}}
|
||||
|
||||
<p translate class="joplin-cloud-login-info">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
|
||||
{{#plans.joplinServerBusiness}}
|
||||
{{> plan}}
|
||||
{{/plans.joplinServerBusiness}}
|
||||
|
||||
<p translate class="joplin-cloud-login-info hosting-type-managed">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -148,4 +220,30 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
const setHostingType = (type) => {
|
||||
const other = type === 'managed' ? 'self' : 'managed';
|
||||
$('.toggle-button-' + type).addClass('active');
|
||||
$('.toggle-button-' + type).removeClass('inactive');
|
||||
$('.toggle-button-' + other).addClass('inactive');
|
||||
$('.toggle-button-' + other).removeClass('active');
|
||||
|
||||
$('.hosting-type-' + type).show();
|
||||
$('.hosting-type-' + other).hide();
|
||||
}
|
||||
|
||||
$('.toggle-button-managed').click((event) => {
|
||||
event.preventDefault();
|
||||
setHostingType('managed');
|
||||
});
|
||||
|
||||
$('.toggle-button-self').click((event) => {
|
||||
event.preventDefault();
|
||||
setHostingType('self');
|
||||
});
|
||||
|
||||
setHostingType('managed');
|
||||
</script>
|
||||
</div>
|
||||
|
@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
# Sponsors
|
||||
|
||||
<!-- SPONSORS-ORG -->
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://realgambling.ca/"><img title="RealGambling.ca" width="256" src="https://joplinapp.org/images/sponsors/RealGambling.png" alt="RealGambling.ca"/></a> <a href="https://essaypro.com/"><img title="write an essay online with EssayPro" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="write an essay online with EssayPro"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://www.reddit.com/r/tiktokRise/"><img title="Tiktok Rise" width="256" src="https://joplinapp.org/images/sponsors/TiktokRise.jpg" alt="Tiktok Rise"/></a> <a href="https://essaywriter.pro"><img title="write my essay services by EssayWriter" width="256" src="https://joplinapp.org/images/sponsors/EssayWriterPro.png" alt="write my essay services by EssayWriter"/></a> <a href="https://essayservice.com"><img title="quick and reliable service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="quick and reliable service to write my paper for me"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a>
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://realgambling.ca/"><img title="RealGambling.ca" width="256" src="https://joplinapp.org/images/sponsors/RealGambling.png" alt="RealGambling.ca"/></a> <a href="https://essaypro.com/"><img title="write an essay online with EssayPro" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="write an essay online with EssayPro"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://essaywriter.pro"><img title="write my essay services by EssayWriter" width="256" src="https://joplinapp.org/images/sponsors/EssayWriterPro.png" alt="write my essay services by EssayWriter"/></a> <a href="https://essayservice.com"><img title="quick and reliable service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="quick and reliable service to write my paper for me"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://homeworkguy.org/someone-to-take-my-online-class"><img title="someone to take my online class" width="256" src="https://joplinapp.org/images/sponsors/HomeworkGuy.png" alt="someone to take my online class"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
|
@@ -38,6 +38,7 @@
|
||||
"linter-precommit": "eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"packageJsonLint": "node ./packages/tools/packageJsonLint.js",
|
||||
"syncFuzzer": "node ./packages/tools/fuzzer/sync-fuzzer.js",
|
||||
"postinstall": "husky && gulp build",
|
||||
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
|
||||
"publishAll": "git pull && yarn buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
@@ -83,7 +84,7 @@
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "15.4.3",
|
||||
"lint-staged": "15.5.0",
|
||||
"madge": "8.0.0",
|
||||
"npm-package-json-lint": "8.0.0",
|
||||
"typescript": "5.4.5"
|
||||
|
@@ -419,6 +419,11 @@ class Application extends BaseApplication {
|
||||
|
||||
this.initRedux();
|
||||
|
||||
// Since the settings need to be loaded before the store is created, it will never
|
||||
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
|
||||
// initialised. So we manually call dispatchUpdateAll() to force an update.
|
||||
Setting.dispatchUpdateAll();
|
||||
|
||||
if (!shim.sharpEnabled()) this.logger().warn('Sharp is disabled - certain image-related features will not be available');
|
||||
|
||||
initializeCommandService(this.store(), Setting.value('env') === Env.Dev);
|
||||
@@ -461,11 +466,6 @@ class Application extends BaseApplication {
|
||||
this.gui_.setLogger(this.logger());
|
||||
await this.gui_.start();
|
||||
|
||||
// Since the settings need to be loaded before the store is created, it will never
|
||||
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
|
||||
// initialised. So we manually call dispatchUpdateAll() to force an update.
|
||||
Setting.dispatchUpdateAll();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
await refreshFolders((action: any) => this.store().dispatch(action), '');
|
||||
|
||||
|
@@ -26,6 +26,7 @@ class Command extends BaseCommand {
|
||||
['-v, --verbose', 'More verbose output for the `target-status` command'],
|
||||
['-o, --output <directory>', 'Output directory'],
|
||||
['--retry-failed-items', 'Applies to `decrypt` command - retries decrypting items that previously could not be decrypted.'],
|
||||
['-f, --force', 'Do not ask for input on failure'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -67,7 +68,7 @@ class Command extends BaseCommand {
|
||||
this.stdout(line.join('\n'));
|
||||
break;
|
||||
} catch (error) {
|
||||
if (error.code === 'masterKeyNotLoaded') {
|
||||
if (error.code === 'masterKeyNotLoaded' && !args.options.force) {
|
||||
const ok = await askForMasterKey(error);
|
||||
if (!ok) return;
|
||||
continue;
|
||||
|
@@ -17,6 +17,7 @@ import { pathExists, writeFile } from 'fs-extra';
|
||||
import { checkIfLoginWasSuccessful, generateApplicationConfirmUrl } from '@joplin/lib/services/joplinCloudUtils';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { uuidgen } from '@joplin/lib/uuid';
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
|
||||
const logger = Logger.create('command-sync');
|
||||
|
||||
@@ -230,6 +231,10 @@ class Command extends BaseCommand {
|
||||
return cleanUp();
|
||||
}
|
||||
|
||||
// Refresh share invitations -- if running without a GUI, some of the
|
||||
// maintenance tasks may otherwise be skipped.
|
||||
await ShareService.instance().maintenance();
|
||||
|
||||
this.stdout(_('Starting synchronisation...'));
|
||||
|
||||
const contextKey = `sync.${this.syncTargetId_}.context`;
|
||||
|
1
packages/app-cli/tests/html_to_md/comments_in_style.html
Normal file
@@ -0,0 +1 @@
|
||||
<p><span style="/* Comment */ text-decoration: underline;">Test</span>. In the past, <span style="font-size: auto;/* Test! */">comments</span> in CSS have caused issues.</p>
|
1
packages/app-cli/tests/html_to_md/comments_in_style.md
Normal file
@@ -0,0 +1 @@
|
||||
<ins>Test</ins>. In the past, comments in CSS have caused issues.
|
@@ -343,6 +343,14 @@ export default class ElectronAppWrapper {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const sendWindowFocused = (focusedWebContents: WebContents) => {
|
||||
const joplinId = this.windowIdFromWebContents(focusedWebContents);
|
||||
|
||||
if (joplinId !== null) {
|
||||
this.win_.webContents.send('window-focused', joplinId);
|
||||
}
|
||||
};
|
||||
|
||||
const addWindowEventHandlers = (webContents: WebContents) => {
|
||||
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
|
||||
webContents.on('will-frame-navigate', event => {
|
||||
@@ -376,13 +384,10 @@ export default class ElectronAppWrapper {
|
||||
addWindowEventHandlers(event.webContents);
|
||||
});
|
||||
|
||||
webContents.on('focus', () => {
|
||||
const joplinId = this.windowIdFromWebContents(webContents);
|
||||
|
||||
if (joplinId !== null) {
|
||||
this.win_.webContents.send('window-focused', joplinId);
|
||||
}
|
||||
});
|
||||
const onFocus = () => {
|
||||
sendWindowFocused(webContents);
|
||||
};
|
||||
webContents.on('focus', onFocus);
|
||||
};
|
||||
addWindowEventHandlers(this.win_.webContents);
|
||||
|
||||
@@ -454,6 +459,10 @@ export default class ElectronAppWrapper {
|
||||
this.win_.close();
|
||||
}
|
||||
});
|
||||
|
||||
if (window.isFocused()) {
|
||||
sendWindowFocused(window.webContents);
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import { filename, toForwardSlashes } from '@joplin/utils/path';
|
||||
import * as esbuild from 'esbuild';
|
||||
import { existsSync } from 'fs';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { dirname, join, relative } from 'path';
|
||||
|
||||
const baseDir = dirname(__dirname);
|
||||
const baseNodeModules = join(baseDir, 'node_modules');
|
||||
|
||||
// Note: Roughly based on js-draw's use of esbuild:
|
||||
// https://github.com/personalizedrefrigerator/js-draw/blob/6fe6d6821402a08a8d17f15a8f48d95e5d7b084f/packages/build-tool/src/BundledFile.ts#L64
|
||||
const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSizeStats: boolean) => {
|
||||
@@ -28,8 +31,6 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
|
||||
name: 'joplin--relative-imports-for-externals',
|
||||
setup: build => {
|
||||
const externalRegex = /^(.*\.node|sqlite3|electron|@electron\/remote\/.*|electron\/.*|@mapbox\/node-pre-gyp|jsdom)$/;
|
||||
const baseDir = dirname(__dirname);
|
||||
const baseNodeModules = join(baseDir, 'node_modules');
|
||||
build.onResolve({ filter: externalRegex }, args => {
|
||||
// Electron packages don't need relative requires
|
||||
if (args.path === 'electron' || args.path.startsWith('electron/')) {
|
||||
@@ -66,8 +67,6 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
|
||||
// Rewrite imports to prefer .js files to .ts. Otherwise, certain files are duplicated in the final bundle
|
||||
name: 'joplin--prefer-js-imports',
|
||||
setup: build => {
|
||||
const baseDir = dirname(__dirname);
|
||||
const baseNodeModules = join(baseDir, 'node_modules');
|
||||
// Rewrite all relative imports
|
||||
build.onResolve({ filter: /^\./ }, args => {
|
||||
try {
|
||||
@@ -90,6 +89,31 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'joplin--smaller-source-map-size',
|
||||
setup: build => {
|
||||
// Exclude dependencies from node_modules. This significantly reduces the size of the
|
||||
// source map, improving startup performance.
|
||||
//
|
||||
// See https://github.com/evanw/esbuild/issues/1685#issuecomment-944916409
|
||||
// and https://github.com/evanw/esbuild/issues/4130
|
||||
const emptyMapData = Buffer.from(
|
||||
JSON.stringify({ version: 3, sources: [null], mappings: 'AAAA' }),
|
||||
'utf-8',
|
||||
).toString('base64');
|
||||
const emptyMapUrl = `data:application/json;base64,${emptyMapData}`;
|
||||
|
||||
build.onLoad({ filter: /node_modules.*js$/ }, args => {
|
||||
return {
|
||||
contents: [
|
||||
readFileSync(args.path, 'utf8'),
|
||||
`//# sourceMappingURL=${emptyMapUrl}`,
|
||||
].join('\n'),
|
||||
loader: 'default',
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
@@ -60,6 +60,7 @@ const useCss = (editorTheme: Theme) => {
|
||||
body, html {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide the scrollbar. See scrollbar accessibility concerns
|
||||
|
@@ -533,7 +533,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 141;
|
||||
CURRENT_PROJECT_VERSION = 142;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
@@ -568,7 +568,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 141;
|
||||
CURRENT_PROJECT_VERSION = 142;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
@@ -767,7 +767,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 141;
|
||||
CURRENT_PROJECT_VERSION = 142;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -810,7 +810,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 141;
|
||||
CURRENT_PROJECT_VERSION = 142;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
hash:"e857ce4f63c45b5c1d25eb9a76c2127d", files: {
|
||||
hash:"39ce682c4ff5dd85d571d0e99718648f", files: {
|
||||
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
|
||||
|
@@ -1 +1 @@
|
||||
module.exports = {"hash":"e857ce4f63c45b5c1d25eb9a76c2127d","files":["highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
|
||||
module.exports = {"hash":"39ce682c4ff5dd85d571d0e99718648f","files":["highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
|
@@ -1353,7 +1353,6 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
isOpen={this.props.showSideMenu}
|
||||
disableGestures={disableSideMenuGestures}
|
||||
>
|
||||
<StatusBar barStyle={statusBarStyle} />
|
||||
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: '100%' }}>
|
||||
<SafeAreaView style={{ flex: 0, backgroundColor: theme.backgroundColor2 }}/>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
@@ -1362,11 +1361,6 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
</View>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied */}
|
||||
<DropdownAlert alert={(func: any) => (this.dropdownAlert_ = func)} />
|
||||
{ !shouldShowMainContent && <BiometricPopup
|
||||
dispatch={this.props.dispatch}
|
||||
themeId={this.props.themeId}
|
||||
sensorInfo={this.state.sensorInfo}
|
||||
/> }
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</SideMenu>
|
||||
@@ -1416,12 +1410,21 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
|
||||
},
|
||||
}}>
|
||||
<DialogManager themeId={this.props.themeId}>
|
||||
<StatusBar barStyle={statusBarStyle} />
|
||||
<MenuProvider
|
||||
style={{ flex: 1 }}
|
||||
closeButtonLabel={_('Dismiss')}
|
||||
>
|
||||
<FocusControl.MainAppContent style={{ flex: 1 }}>
|
||||
{mainContent}
|
||||
{shouldShowMainContent ? mainContent : (
|
||||
<SafeAreaView>
|
||||
<BiometricPopup
|
||||
dispatch={this.props.dispatch}
|
||||
themeId={this.props.themeId}
|
||||
sensorInfo={this.state.sensorInfo}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
)}
|
||||
</FocusControl.MainAppContent>
|
||||
</MenuProvider>
|
||||
</DialogManager>
|
||||
|
@@ -8,9 +8,9 @@ import { chdir, cwd } from 'process';
|
||||
import { execCommand } from '@joplin/utils';
|
||||
import { glob } from 'glob';
|
||||
import readRepositoryJson from './utils/readRepositoryJson';
|
||||
import waitForCliInput from './utils/waitForCliInput';
|
||||
import getPathToPatchFileFor from './utils/getPathToPatchFileFor';
|
||||
import getCurrentCommitHash from './utils/getCurrentCommitHash';
|
||||
import { waitForCliInput } from '@joplin/utils/cli';
|
||||
|
||||
interface Options {
|
||||
beforeInstall: (buildDir: string, pluginName: string)=> Promise<void>;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { execCommand } from '@joplin/utils';
|
||||
import waitForCliInput from '../utils/waitForCliInput';
|
||||
import { copy } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { waitForCliInput } from '@joplin/utils/cli';
|
||||
import buildDefaultPlugins from '../buildDefaultPlugins';
|
||||
import getPathToPatchFileFor from '../utils/getPathToPatchFileFor';
|
||||
|
||||
|
@@ -98,7 +98,7 @@ export default class ClipperServer {
|
||||
});
|
||||
}
|
||||
|
||||
public async findAvailablePort() {
|
||||
public async findAvailablePort(): Promise<number> {
|
||||
const tcpPortUsed = require('tcp-port-used');
|
||||
|
||||
let state = null;
|
||||
|
@@ -1,13 +1,21 @@
|
||||
|
||||
const testPathIgnorePatterns = [
|
||||
'<rootDir>/node_modules/',
|
||||
'<rootDir>/rnInjectedJs/',
|
||||
'<rootDir>/vendor/',
|
||||
];
|
||||
|
||||
if (!process.env.IS_CONTINUOUS_INTEGRATION) {
|
||||
// We don't require all developers to have Rust to run the project, so we skip this test if not running in CI
|
||||
testPathIgnorePatterns.push('<rootDir>/services/interop/InteropService_Importer_OneNote.*');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testMatch: [
|
||||
'**/*.test.js',
|
||||
],
|
||||
|
||||
testPathIgnorePatterns: [
|
||||
'<rootDir>/node_modules/',
|
||||
'<rootDir>/rnInjectedJs/',
|
||||
'<rootDir>/vendor/',
|
||||
],
|
||||
testPathIgnorePatterns: testPathIgnorePatterns,
|
||||
|
||||
testEnvironment: 'node',
|
||||
|
||||
|
@@ -16,6 +16,7 @@ export enum MarkdownTableJustify {
|
||||
export interface MarkdownTableHeader {
|
||||
name: string;
|
||||
label: string;
|
||||
labelUrl?: string;
|
||||
filter?: (content: string)=> string;
|
||||
disableEscape?: boolean;
|
||||
disableHtmlEscape?: boolean;
|
||||
@@ -159,7 +160,11 @@ const markdownUtils = {
|
||||
const lineMd = [];
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const h = headers[i];
|
||||
headersMd.push(stringPadding(h.label, minCellWidth, ' ', stringPadding.RIGHT));
|
||||
let label = h.label;
|
||||
if (h.labelUrl) {
|
||||
label = `[${h.label}](${h.labelUrl})`;
|
||||
}
|
||||
headersMd.push(stringPadding(label, minCellWidth, ' ', stringPadding.RIGHT));
|
||||
|
||||
const justify = h.justify ? h.justify : MarkdownTableJustify.Left;
|
||||
|
||||
|
@@ -357,7 +357,7 @@ export default class Folder extends BaseItem {
|
||||
|
||||
if (options && options.includeConflictFolder) {
|
||||
const conflictCount = await Note.conflictedCount();
|
||||
if (conflictCount) output.push(this.conflictFolder());
|
||||
if (conflictCount) output.unshift(this.conflictFolder());
|
||||
}
|
||||
|
||||
return output;
|
||||
|
@@ -22,9 +22,7 @@ const expectWithInstructions = <T>(value: T) => {
|
||||
return expect(value, instructionMessage);
|
||||
};
|
||||
|
||||
// We don't require all developers to have Rust to run the project, so we skip this test if not running in CI
|
||||
const skipIfNotCI = process.env.IS_CONTINUOUS_INTEGRATION ? it : it.skip;
|
||||
|
||||
// This file is ignored if not running in CI. Look at onenote-converter/README.md and jest.config.js for more information
|
||||
describe('InteropService_Importer_OneNote', () => {
|
||||
let tempDir: string;
|
||||
async function importNote(path: string) {
|
||||
@@ -52,7 +50,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
afterEach(async () => {
|
||||
await remove(tempDir);
|
||||
});
|
||||
skipIfNotCI('should import a simple OneNote notebook', async () => {
|
||||
it('should import a simple OneNote notebook', async () => {
|
||||
const notes = await importNote(`${supportDir}/onenote/simple_notebook.zip`);
|
||||
const folders = await Folder.all();
|
||||
|
||||
@@ -69,7 +67,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
expectWithInstructions(mainNote.body).toMatchSnapshot(mainNote.title);
|
||||
});
|
||||
|
||||
skipIfNotCI('should preserve indentation of subpages in Section page', async () => {
|
||||
it('should preserve indentation of subpages in Section page', async () => {
|
||||
const notes = await importNote(`${supportDir}/onenote/subpages.zip`);
|
||||
|
||||
const sectionPage = notes.find(n => n.title === 'Section');
|
||||
@@ -89,7 +87,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
expectWithInstructions(menuLines[7].trim()).toBe(`<li class="l2"><a href=":/${pageTwoB.id}" target="content" title="Page 2-b">${pageTwoB.title}</a>`);
|
||||
});
|
||||
|
||||
skipIfNotCI('should created subsections', async () => {
|
||||
it('should created subsections', async () => {
|
||||
const notes = await importNote(`${supportDir}/onenote/subsections.zip`);
|
||||
const folders = await Folder.all();
|
||||
|
||||
@@ -107,7 +105,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
expectWithInstructions(notesFromParentSection.length).toBe(2);
|
||||
});
|
||||
|
||||
skipIfNotCI('should expect notes to be rendered the same', async () => {
|
||||
it('should expect notes to be rendered the same', async () => {
|
||||
let idx = 0;
|
||||
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
|
||||
const notes = await importNote(`${supportDir}/onenote/complex_notes.zip`);
|
||||
@@ -124,7 +122,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
BaseModel.setIdGenerator(originalIdGenerator);
|
||||
});
|
||||
|
||||
skipIfNotCI('should render the proper tree for notebook with group sections', async () => {
|
||||
it('should render the proper tree for notebook with group sections', async () => {
|
||||
const notes = await importNote(`${supportDir}/onenote/group_sections.zip`);
|
||||
const folders = await Folder.all();
|
||||
|
||||
@@ -152,7 +150,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
expectWithInstructions(notes.filter(n => n.parent_id === sectionD1.id).length).toBe(1);
|
||||
});
|
||||
|
||||
skipIfNotCI.each([
|
||||
it.each([
|
||||
'svg_with_text_and_style.html',
|
||||
'many_svgs.html',
|
||||
])('should extract svgs', async (filename: string) => {
|
||||
@@ -179,7 +177,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
expectWithInstructions(importer.extractSvgs(content, titleGenerator())).toMatchSnapshot();
|
||||
});
|
||||
|
||||
skipIfNotCI('should ignore broken characters at the start of paragraph', async () => {
|
||||
it('should ignore broken characters at the start of paragraph', async () => {
|
||||
let idx = 0;
|
||||
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
|
||||
const notes = await importNote(`${supportDir}/onenote/bug_broken_character.zip`);
|
||||
@@ -189,7 +187,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
BaseModel.setIdGenerator(originalIdGenerator);
|
||||
});
|
||||
|
||||
skipIfNotCI('should remove hyperlink from title', async () => {
|
||||
it('should remove hyperlink from title', async () => {
|
||||
let idx = 0;
|
||||
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
|
||||
const notes = await importNote(`${supportDir}/onenote/remove_hyperlink_on_title.zip`);
|
||||
@@ -200,7 +198,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
BaseModel.setIdGenerator(originalIdGenerator);
|
||||
});
|
||||
|
||||
skipIfNotCI('should group link parts even if they have different css styles', async () => {
|
||||
it('should group link parts even if they have different css styles', async () => {
|
||||
const notes = await importNote(`${supportDir}/onenote/remove_hyperlink_on_title.zip`);
|
||||
|
||||
const noteToTest = notes.find(n => n.title === 'Tips from a Pro Using Trees for Dramatic Landscape Photography');
|
||||
@@ -209,7 +207,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
expectWithInstructions(noteToTest.body.includes('<a href="onenote:https://d.docs.live.net/c8d3bbab7f1acf3a/Documents/Photography/风景.one#Tips%20from%20a%20Pro%20Using%20Trees%20for%20Dramatic%20Landscape%20Photography§ion-id={262ADDFB-A4DC-4453-A239-0024D6769962}&page-id={88D803A5-4F43-48D4-9B16-4C024F5787DC}&end" style="">Tips from a Pro: Using Trees for Dramatic Landscape Photography</a>')).toBe(true);
|
||||
});
|
||||
|
||||
skipIfNotCI('should render links properly by ignoring wrongly set indices when the first character is a hyperlink marker', async () => {
|
||||
it('should render links properly by ignoring wrongly set indices when the first character is a hyperlink marker', async () => {
|
||||
let idx = 0;
|
||||
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
|
||||
const notes = await importNote(`${supportDir}/onenote/hyperlink_marker_as_first_character.zip`);
|
||||
@@ -220,7 +218,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
BaseModel.setIdGenerator(originalIdGenerator);
|
||||
});
|
||||
|
||||
skipIfNotCI('should be able to create notes from corrupted attachment', async () => {
|
||||
it('should be able to create notes from corrupted attachment', async () => {
|
||||
let idx = 0;
|
||||
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
|
||||
const notes = await withWarningSilenced(/OneNoteConverter:/, async () => importNote(`${supportDir}/onenote/corrupted_attachment.zip`));
|
||||
@@ -233,7 +231,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
BaseModel.setIdGenerator(originalIdGenerator);
|
||||
});
|
||||
|
||||
skipIfNotCI('should render audio as links to resource', async () => {
|
||||
it('should render audio as links to resource', async () => {
|
||||
let idx = 0;
|
||||
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
|
||||
const notes = await importNote(`${supportDir}/onenote/note_with_audio_embedded.zip`);
|
||||
@@ -246,7 +244,7 @@ describe('InteropService_Importer_OneNote', () => {
|
||||
BaseModel.setIdGenerator(originalIdGenerator);
|
||||
});
|
||||
|
||||
skipIfNotCI('should use default value for EntityGuid and InkBias if not found', async () => {
|
||||
it('should use default value for EntityGuid and InkBias if not found', async () => {
|
||||
let idx = 0;
|
||||
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
|
||||
const notes = await withWarningSilenced(/OneNoteConverter:/, async () => importNote(`${supportDir}/onenote/ink_bias_and_entity_guid.zip`));
|
||||
|
@@ -9,6 +9,7 @@ export enum PlanName {
|
||||
Basic = 'basic',
|
||||
Pro = 'pro',
|
||||
Teams = 'teams',
|
||||
JoplinServerBusiness = 'joplinServerBusiness',
|
||||
}
|
||||
|
||||
interface PlanFeature {
|
||||
@@ -17,6 +18,7 @@ interface PlanFeature {
|
||||
basic: boolean;
|
||||
pro: boolean;
|
||||
teams: boolean;
|
||||
joplinServerBusiness?: boolean;
|
||||
basicInfo?: string;
|
||||
proInfo?: string;
|
||||
teamsInfo?: string;
|
||||
@@ -25,11 +27,16 @@ interface PlanFeature {
|
||||
teamsInfoShort?: string;
|
||||
}
|
||||
|
||||
enum PlanHostingType {
|
||||
Managed = 'managed',
|
||||
Self = 'self',
|
||||
}
|
||||
|
||||
export interface Plan {
|
||||
name: string;
|
||||
title: string;
|
||||
priceMonthly: StripePublicConfigPrice;
|
||||
priceYearly: StripePublicConfigPrice;
|
||||
priceMonthly?: StripePublicConfigPrice;
|
||||
priceYearly?: StripePublicConfigPrice;
|
||||
featured: boolean;
|
||||
iconName: string;
|
||||
featuresOn: FeatureId[];
|
||||
@@ -39,6 +46,8 @@ export interface Plan {
|
||||
cfaLabel: string;
|
||||
cfaUrl: string;
|
||||
footnote: string;
|
||||
learnMoreUrl?: string;
|
||||
hostingType: PlanHostingType;
|
||||
}
|
||||
|
||||
export enum PricePeriod {
|
||||
@@ -155,26 +164,29 @@ const features = (): Record<FeatureId, PlanFeature> => {
|
||||
basic: true,
|
||||
pro: true,
|
||||
teams: true,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
sync: {
|
||||
title: _('Sync as many devices as you want'),
|
||||
basic: true,
|
||||
pro: true,
|
||||
teams: true,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
clipper: {
|
||||
title: _('Web Clipper'),
|
||||
description: _('The [Web Clipper](%s) is a browser extension that allows you to save web pages and screenshots from your browser.', 'https://joplinapp.org/help/apps/clipper'),
|
||||
basic: true,
|
||||
pro: true,
|
||||
teams: true,
|
||||
},
|
||||
// clipper: {
|
||||
// title: _('Web Clipper'),
|
||||
// description: _('The [Web Clipper](%s) is a browser extension that allows you to save web pages and screenshots from your browser.', 'https://joplinapp.org/help/apps/clipper'),
|
||||
// basic: false,
|
||||
// pro: false,
|
||||
// teams: false,
|
||||
// },
|
||||
collaborate: {
|
||||
title: _('Collaborate on a notebook with others'),
|
||||
description: _('This allows another user to share a notebook with you, and you can then both collaborate on it. It does not however allow you to share a notebook with someone else, unless you have the feature "%s".', shareNotebookTitle),
|
||||
basic: true,
|
||||
pro: true,
|
||||
teams: true,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
share: {
|
||||
title: shareNotebookTitle,
|
||||
@@ -182,6 +194,7 @@ const features = (): Record<FeatureId, PlanFeature> => {
|
||||
basic: false,
|
||||
pro: true,
|
||||
teams: true,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
emailToNote: {
|
||||
title: _('Email to Note'),
|
||||
@@ -189,6 +202,7 @@ const features = (): Record<FeatureId, PlanFeature> => {
|
||||
basic: false,
|
||||
pro: true,
|
||||
teams: true,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
customBanner: {
|
||||
title: _('Customise the note publishing banner'),
|
||||
@@ -196,6 +210,7 @@ const features = (): Record<FeatureId, PlanFeature> => {
|
||||
basic: false,
|
||||
pro: true,
|
||||
teams: true,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
multiUsers: {
|
||||
title: _('Manage multiple users'),
|
||||
@@ -203,6 +218,7 @@ const features = (): Record<FeatureId, PlanFeature> => {
|
||||
basic: false,
|
||||
pro: false,
|
||||
teams: true,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
consolidatedBilling: {
|
||||
title: _('Consolidated billing'),
|
||||
@@ -217,12 +233,28 @@ const features = (): Record<FeatureId, PlanFeature> => {
|
||||
basic: false,
|
||||
pro: false,
|
||||
teams: true,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
prioritySupport: {
|
||||
title: _('Priority support'),
|
||||
basic: false,
|
||||
pro: false,
|
||||
teams: true,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
selfHosted: {
|
||||
title: _('Self-hosted'),
|
||||
basic: false,
|
||||
pro: false,
|
||||
teams: false,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
sourceCodeAvailable: {
|
||||
title: _('Source code available'),
|
||||
basic: false,
|
||||
pro: false,
|
||||
teams: false,
|
||||
joplinServerBusiness: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -303,6 +335,11 @@ export const createFeatureTableMd = () => {
|
||||
name: 'teams',
|
||||
label: 'Teams',
|
||||
},
|
||||
{
|
||||
name: 'joplinServerBusiness',
|
||||
label: 'Joplin Server Business',
|
||||
labelUrl: 'https://joplinapp.org/help/apps/joplin_server_business',
|
||||
},
|
||||
];
|
||||
|
||||
const rows: MarkdownTableRow[] = [];
|
||||
@@ -332,6 +369,7 @@ export const createFeatureTableMd = () => {
|
||||
basic: getCellInfo(PlanName.Basic, feature),
|
||||
pro: getCellInfo(PlanName.Pro, feature),
|
||||
teams: getCellInfo(PlanName.Teams, feature),
|
||||
joplinServerBusiness: getCellInfo(PlanName.JoplinServerBusiness, feature),
|
||||
};
|
||||
|
||||
rows.push(row);
|
||||
@@ -362,6 +400,7 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<PlanName, Pla
|
||||
cfaLabel: _('Try it now'),
|
||||
cfaUrl: '',
|
||||
footnote: '',
|
||||
hostingType: PlanHostingType.Managed,
|
||||
},
|
||||
|
||||
pro: {
|
||||
@@ -384,6 +423,7 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<PlanName, Pla
|
||||
cfaLabel: _('Try it now'),
|
||||
cfaUrl: '',
|
||||
footnote: '',
|
||||
hostingType: PlanHostingType.Managed,
|
||||
},
|
||||
|
||||
teams: {
|
||||
@@ -406,6 +446,23 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<PlanName, Pla
|
||||
cfaLabel: _('Try it now'),
|
||||
cfaUrl: '',
|
||||
footnote: _('Per user. Minimum of 2 users.'),
|
||||
hostingType: PlanHostingType.Managed,
|
||||
},
|
||||
|
||||
joplinServerBusiness: {
|
||||
name: 'joplinServerBusiness',
|
||||
title: _('Joplin Server Business'),
|
||||
featured: false,
|
||||
iconName: 'business-icon',
|
||||
featuresOn: getFeatureIdsByPlan(PlanName.JoplinServerBusiness, true),
|
||||
featuresOff: getFeatureIdsByPlan(PlanName.JoplinServerBusiness, false),
|
||||
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',
|
||||
footnote: '',
|
||||
learnMoreUrl: 'https://joplinapp.org/help/apps/joplin_server_business',
|
||||
hostingType: PlanHostingType.Self,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@
|
||||
"highlight.js": "11.11.1",
|
||||
"html-entities": "1.4.0",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"katex": "0.16.21",
|
||||
"katex": "0.16.22",
|
||||
"markdown-it": "13.0.2",
|
||||
"markdown-it-abbr": "1.0.4",
|
||||
"markdown-it-anchor": "5.3.0",
|
||||
|
@@ -39,7 +39,7 @@
|
||||
"html-entities": "1.4.0",
|
||||
"jquery": "3.7.1",
|
||||
"knex": "3.1.0",
|
||||
"koa": "2.16.0",
|
||||
"koa": "2.16.1",
|
||||
"ldapts": "7.3.3",
|
||||
"markdown-it": "13.0.2",
|
||||
"mustache": "4.2.0",
|
||||
@@ -47,7 +47,7 @@
|
||||
"node-os-utils": "1.3.7",
|
||||
"nodemailer": "6.10.0",
|
||||
"nodemon": "3.1.9",
|
||||
"pg": "8.13.3",
|
||||
"pg": "8.14.1",
|
||||
"pm2": "5.4.3",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"prettycron": "0.10.0",
|
||||
|
@@ -914,8 +914,14 @@ export default class ItemModel extends BaseModel<Item> {
|
||||
const share = await this.models().share().byItemId(item.id);
|
||||
if (!share) throw new Error(`Cannot find share associated with item ${item.id}`);
|
||||
const userShare = await this.models().shareUser().byShareAndUserId(share.id, userId);
|
||||
if (!userShare) return;
|
||||
await this.models().shareUser().delete(userShare.id);
|
||||
|
||||
if (userShare) {
|
||||
// Leave the share
|
||||
await this.models().shareUser().delete(userShare.id);
|
||||
} else if (share.owner_id === userId) {
|
||||
// Delete the share
|
||||
await this.models().share().delete(share.id);
|
||||
}
|
||||
} else {
|
||||
await this.delete(item.id);
|
||||
}
|
||||
|
1
packages/tools/.gitignore
vendored
@@ -4,3 +4,4 @@ patreon_oauth_token.txt
|
||||
*.po~
|
||||
*.mo
|
||||
*.mo~
|
||||
fuzzer/profiles-tmp/
|
||||
|
@@ -183,4 +183,7 @@ topagency
|
||||
esbuild
|
||||
mapbox
|
||||
outfile
|
||||
|
||||
fuzzer
|
||||
Freespinny
|
||||
BestEtf
|
||||
Etf
|
||||
|
387
packages/tools/fuzzer/ActionTracker.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { ActionableClient, FolderData, FolderMetadata, FuzzContext, ItemId, NoteData, TreeItem, isFolder } from './types';
|
||||
import type Client from './Client';
|
||||
|
||||
interface ClientData {
|
||||
childIds: ItemId[];
|
||||
// Shared folders belonging to the client
|
||||
sharedFolderIds: ItemId[];
|
||||
}
|
||||
|
||||
class ActionTracker {
|
||||
private idToItem_: Map<ItemId, TreeItem> = new Map();
|
||||
private tree_: Map<string, ClientData> = new Map();
|
||||
public constructor(private readonly context_: FuzzContext) {}
|
||||
|
||||
private checkRep_() {
|
||||
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');
|
||||
}
|
||||
|
||||
if (isFolder(item)) {
|
||||
for (const childId of item.childIds) {
|
||||
checkItem(childId);
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
item.childIds.length,
|
||||
[...new Set(item.childIds)].length,
|
||||
'child IDs should be unique',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
for (const clientData of this.tree_.values()) {
|
||||
for (const childId of clientData.childIds) {
|
||||
assert.ok(this.idToItem_.has(childId), `root item ${childId} should exist`);
|
||||
|
||||
const item = this.idToItem_.get(childId);
|
||||
assert.ok(!!item);
|
||||
assert.equal(item.parentId, '', `${childId} should not have a parent`);
|
||||
|
||||
checkItem(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public track(client: { email: string }) {
|
||||
const clientId = client.email;
|
||||
this.tree_.set(clientId, {
|
||||
childIds: [],
|
||||
sharedFolderIds: [],
|
||||
});
|
||||
|
||||
const getChildIds = (itemId: ItemId) => {
|
||||
const item = this.idToItem_.get(itemId);
|
||||
if (!item || !isFolder(item)) return [];
|
||||
return item.childIds;
|
||||
};
|
||||
const updateChildren = (parentId: ItemId, updateFn: (oldChildren: ItemId[])=> ItemId[]) => {
|
||||
const parent = this.idToItem_.get(parentId);
|
||||
if (!parent) throw new Error(`Parent with ID ${parentId} not found.`);
|
||||
if (!isFolder(parent)) throw new Error(`Item ${parentId} is not a folder`);
|
||||
|
||||
this.idToItem_.set(parentId, {
|
||||
...parent,
|
||||
childIds: updateFn(parent.childIds),
|
||||
});
|
||||
};
|
||||
const addRootItem = (itemId: ItemId) => {
|
||||
const clientData = this.tree_.get(clientId);
|
||||
if (!clientData.childIds.includes(itemId)) {
|
||||
this.tree_.set(clientId, {
|
||||
...clientData,
|
||||
childIds: [...clientData.childIds, itemId],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Returns true iff the given item ID is now unused.
|
||||
const removeRootItem = (itemId: ItemId) => {
|
||||
const removeForClient = (clientId: string) => {
|
||||
const clientData = this.tree_.get(clientId);
|
||||
const childIds = clientData.childIds;
|
||||
|
||||
if (childIds.includes(itemId)) {
|
||||
const newChildIds = childIds.filter(otherId => otherId !== itemId);
|
||||
this.tree_.set(clientId, {
|
||||
...clientData,
|
||||
childIds: newChildIds,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasBeenCompletelyRemoved = () => {
|
||||
for (const clientData of this.tree_.values()) {
|
||||
if (clientData.childIds.includes(itemId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isOwnedByThis = this.tree_.get(clientId).sharedFolderIds.includes(itemId);
|
||||
|
||||
if (isOwnedByThis) { // Unshare
|
||||
let removed = false;
|
||||
for (const id of this.tree_.keys()) {
|
||||
const result = removeForClient(id);
|
||||
removed ||= result;
|
||||
}
|
||||
|
||||
const clientData = this.tree_.get(clientId);
|
||||
this.tree_.set(clientId, {
|
||||
...clientData,
|
||||
sharedFolderIds: clientData.sharedFolderIds.filter(id => id !== itemId),
|
||||
});
|
||||
|
||||
// At this point, the item shouldn't be a child of any clients:
|
||||
assert.ok(hasBeenCompletelyRemoved(), 'item should be removed from all clients');
|
||||
assert.ok(removed, 'should be a toplevel item');
|
||||
|
||||
// The item is unshared and can be removed entirely
|
||||
return true;
|
||||
} else {
|
||||
// Otherwise, even if part of a share, removing the
|
||||
// notebook just leaves the share.
|
||||
const removed = removeForClient(clientId);
|
||||
assert.ok(removed, 'should be a toplevel item');
|
||||
|
||||
if (hasBeenCompletelyRemoved()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
const addChild = (parentId: ItemId, childId: ItemId) => {
|
||||
if (parentId) {
|
||||
updateChildren(parentId, (oldChildren) => {
|
||||
if (oldChildren.includes(childId)) return oldChildren;
|
||||
return [...oldChildren, childId];
|
||||
});
|
||||
} else {
|
||||
addRootItem(childId);
|
||||
}
|
||||
};
|
||||
const removeChild = (parentId: ItemId, childId: ItemId) => {
|
||||
if (!parentId) {
|
||||
removeRootItem(childId);
|
||||
} else {
|
||||
updateChildren(parentId, (oldChildren) => {
|
||||
return oldChildren.filter(otherId => otherId !== childId);
|
||||
});
|
||||
}
|
||||
};
|
||||
const removeItemRecursive = (id: ItemId) => {
|
||||
const item = this.idToItem_.get(id);
|
||||
if (!item) throw new Error(`Item with ID ${id} not found.`);
|
||||
|
||||
if (item.parentId) {
|
||||
// The parent may already be removed
|
||||
if (this.idToItem_.has(item.parentId)) {
|
||||
removeChild(item.parentId, item.id);
|
||||
}
|
||||
|
||||
this.idToItem_.delete(id);
|
||||
} else {
|
||||
const idIsUnused = removeRootItem(item.id);
|
||||
if (idIsUnused) {
|
||||
this.idToItem_.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (isFolder(item)) {
|
||||
for (const childId of item.childIds) {
|
||||
const child = this.idToItem_.get(childId);
|
||||
assert.equal(child?.parentId, id, `child ${childId} should have accurate parent ID`);
|
||||
|
||||
removeItemRecursive(childId);
|
||||
}
|
||||
}
|
||||
};
|
||||
const mapItems = <T> (map: (item: TreeItem)=> T) => {
|
||||
const workList: ItemId[] = [...this.tree_.get(clientId).childIds];
|
||||
const result: T[] = [];
|
||||
|
||||
while (workList.length > 0) {
|
||||
const id = workList.pop();
|
||||
const item = this.idToItem_.get(id);
|
||||
if (!item) throw new Error(`Not found: ${id}`);
|
||||
|
||||
result.push(map(item));
|
||||
|
||||
if (isFolder(item)) {
|
||||
for (const childId of item.childIds) {
|
||||
workList.push(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const listFoldersDetailed = () => {
|
||||
return mapItems((item): FolderData => {
|
||||
return isFolder(item) ? item : null;
|
||||
}).filter(item => !!item);
|
||||
};
|
||||
|
||||
const tracker: ActionableClient = {
|
||||
createNote: (data: NoteData) => {
|
||||
assert.ok(!!data.parentId, `note ${data.id} should have a parentId`);
|
||||
assert.ok(!this.idToItem_.has(data.id), `note ${data.id} should not yet exist`);
|
||||
this.idToItem_.set(data.id, {
|
||||
...data,
|
||||
});
|
||||
addChild(data.parentId, data.id);
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
},
|
||||
updateNote: (data: NoteData) => {
|
||||
const oldItem = this.idToItem_.get(data.id);
|
||||
assert.ok(oldItem, `note ${data.id} should exist`);
|
||||
assert.ok(!!data.parentId, `note ${data.id} should have a parentId`);
|
||||
|
||||
removeChild(oldItem.parentId, data.id);
|
||||
this.idToItem_.set(data.id, {
|
||||
...data,
|
||||
});
|
||||
addChild(data.parentId, data.id);
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
},
|
||||
createFolder: (data: FolderMetadata) => {
|
||||
this.idToItem_.set(data.id, {
|
||||
...data,
|
||||
parentId: data.parentId ?? '',
|
||||
childIds: getChildIds(data.id),
|
||||
isShareRoot: false,
|
||||
});
|
||||
addChild(data.parentId, data.id);
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
},
|
||||
deleteFolder: (id: ItemId) => {
|
||||
this.checkRep_();
|
||||
|
||||
const item = this.idToItem_.get(id);
|
||||
if (!item) throw new Error(`Not found ${id}`);
|
||||
if (!isFolder(item)) throw new Error(`Not a folder ${id}`);
|
||||
|
||||
removeItemRecursive(id);
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
},
|
||||
shareFolder: (id: ItemId, shareWith: Client) => {
|
||||
const shareWithChildIds = this.tree_.get(shareWith.email).childIds;
|
||||
if (shareWithChildIds.includes(id)) {
|
||||
throw new Error(`Folder ${id} already shared with ${shareWith.email}`);
|
||||
}
|
||||
assert.ok(this.idToItem_.has(id), 'should exist');
|
||||
|
||||
const sharerClient = this.tree_.get(clientId);
|
||||
if (!sharerClient.sharedFolderIds.includes(id)) {
|
||||
this.tree_.set(clientId, {
|
||||
...sharerClient,
|
||||
sharedFolderIds: [...sharerClient.sharedFolderIds, id],
|
||||
});
|
||||
}
|
||||
|
||||
this.tree_.set(shareWith.email, {
|
||||
...this.tree_.get(shareWith.email),
|
||||
childIds: [...shareWithChildIds, id],
|
||||
});
|
||||
|
||||
this.idToItem_.set(id, {
|
||||
...this.idToItem_.get(id),
|
||||
isShareRoot: true,
|
||||
});
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
},
|
||||
moveItem: (itemId, newParentId) => {
|
||||
const item = this.idToItem_.get(itemId);
|
||||
assert.ok(item, `item with ${itemId} should exist`);
|
||||
|
||||
if (newParentId) {
|
||||
const parent = this.idToItem_.get(newParentId);
|
||||
assert.ok(parent, `parent with ID ${newParentId} should exist`);
|
||||
} else {
|
||||
assert.equal(newParentId, '', 'parentId should be empty if a toplevel folder');
|
||||
}
|
||||
|
||||
if (isFolder(item)) {
|
||||
assert.equal(item.isShareRoot, false, 'cannot move toplevel shared folders without first unsharing');
|
||||
}
|
||||
|
||||
removeChild(item.parentId, itemId);
|
||||
addChild(newParentId, itemId);
|
||||
this.idToItem_.set(itemId, {
|
||||
...item,
|
||||
parentId: newParentId,
|
||||
});
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve();
|
||||
},
|
||||
sync: () => Promise.resolve(),
|
||||
listNotes: () => {
|
||||
const notes = mapItems(item => {
|
||||
return isFolder(item) ? null : item;
|
||||
}).filter(item => !!item);
|
||||
|
||||
this.checkRep_();
|
||||
return Promise.resolve(notes);
|
||||
},
|
||||
listFolders: () => {
|
||||
this.checkRep_();
|
||||
const folderData = listFoldersDetailed().map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
parentId: item.parentId,
|
||||
}));
|
||||
|
||||
return Promise.resolve(folderData);
|
||||
},
|
||||
allFolderDescendants: (parentId) => {
|
||||
this.checkRep_();
|
||||
|
||||
const descendants: ItemId[] = [];
|
||||
const addDescendants = (id: ItemId) => {
|
||||
const item = this.idToItem_.get(id);
|
||||
assert.ok(isFolder(item), 'should be a folder');
|
||||
|
||||
for (const id of item.childIds) {
|
||||
descendants.push(id);
|
||||
|
||||
const item = this.idToItem_.get(id);
|
||||
if (isFolder(item)) {
|
||||
addDescendants(item.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
descendants.push(parentId);
|
||||
addDescendants(parentId);
|
||||
|
||||
return Promise.resolve(descendants);
|
||||
},
|
||||
randomFolder: async (options) => {
|
||||
let folders = listFoldersDetailed();
|
||||
if (options.filter) {
|
||||
folders = folders.filter(options.filter);
|
||||
}
|
||||
|
||||
const folderIndex = this.context_.randInt(0, folders.length);
|
||||
return folders.length ? folders[folderIndex] : null;
|
||||
},
|
||||
randomNote: async () => {
|
||||
const notes = await tracker.listNotes();
|
||||
const noteIndex = this.context_.randInt(0, notes.length);
|
||||
return notes.length ? notes[noteIndex] : null;
|
||||
},
|
||||
};
|
||||
return tracker;
|
||||
}
|
||||
}
|
||||
|
||||
export default ActionTracker;
|
420
packages/tools/fuzzer/Client.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
import uuid, { createSecureRandom } from '@joplin/lib/uuid';
|
||||
import { ActionableClient, FolderMetadata, FuzzContext, HttpMethod, ItemId, Json, NoteData, RandomFolderOptions, UserData } from './types';
|
||||
import { join } from 'path';
|
||||
import { mkdir } from 'fs-extra';
|
||||
import getStringProperty from './utils/getStringProperty';
|
||||
import { strict as assert } from 'assert';
|
||||
import ClipperServer from '@joplin/lib/ClipperServer';
|
||||
import ActionTracker from './ActionTracker';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import execa = require('execa');
|
||||
import { cliDirectory } from './constants';
|
||||
import { commandToString } from '@joplin/utils';
|
||||
import { quotePath } from '@joplin/utils/path';
|
||||
import getNumberProperty from './utils/getNumberProperty';
|
||||
import retryWithCount from './utils/retryWithCount';
|
||||
|
||||
const logger = Logger.create('Client');
|
||||
|
||||
|
||||
class Client implements ActionableClient {
|
||||
public readonly email: string;
|
||||
|
||||
public static async create(actionTracker: ActionTracker, context: FuzzContext) {
|
||||
const id = uuid.create();
|
||||
const profileDirectory = join(context.baseDir, id);
|
||||
await mkdir(profileDirectory);
|
||||
|
||||
const email = `${id}@localhost`;
|
||||
const password = createSecureRandom();
|
||||
const apiOutput = await context.execApi('POST', 'api/users', {
|
||||
email,
|
||||
});
|
||||
const serverId = getStringProperty(apiOutput, 'id');
|
||||
|
||||
// The password needs to be set *after* creating the user.
|
||||
const userRoute = `api/users/${encodeURIComponent(serverId)}`;
|
||||
await context.execApi('PATCH', userRoute, {
|
||||
email,
|
||||
password,
|
||||
email_confirmed: 1,
|
||||
});
|
||||
|
||||
const closeAccount = async () => {
|
||||
await context.execApi('DELETE', userRoute, {});
|
||||
};
|
||||
|
||||
try {
|
||||
const userData = {
|
||||
email: getStringProperty(apiOutput, 'email'),
|
||||
password,
|
||||
};
|
||||
|
||||
assert.equal(email, userData.email);
|
||||
|
||||
const apiToken = createSecureRandom().replace(/[-]/g, '_');
|
||||
const apiPort = await ClipperServer.instance().findAvailablePort();
|
||||
|
||||
const client = new Client(
|
||||
actionTracker.track({ email }),
|
||||
userData,
|
||||
profileDirectory,
|
||||
apiPort,
|
||||
apiToken,
|
||||
closeAccount,
|
||||
);
|
||||
|
||||
// Joplin Server sync
|
||||
await client.execCliCommand_('config', 'sync.target', '9');
|
||||
await client.execCliCommand_('config', 'sync.9.path', context.serverUrl);
|
||||
await client.execCliCommand_('config', 'sync.9.username', userData.email);
|
||||
await client.execCliCommand_('config', 'sync.9.password', userData.password);
|
||||
await client.execCliCommand_('config', 'api.token', apiToken);
|
||||
await client.execCliCommand_('config', 'api.port', String(apiPort));
|
||||
|
||||
const e2eePassword = createSecureRandom().replace(/^-/, '_');
|
||||
await client.execCliCommand_('e2ee', 'enable', '--password', e2eePassword);
|
||||
logger.info('Created and configured client');
|
||||
|
||||
// Run asynchronously -- the API server command doesn't exit until the server
|
||||
// is closed.
|
||||
void (async () => {
|
||||
try {
|
||||
await client.execCliCommand_('server', 'start');
|
||||
} catch (error) {
|
||||
logger.info('API server exited');
|
||||
logger.debug('API server exit status', error);
|
||||
}
|
||||
})();
|
||||
|
||||
await client.sync();
|
||||
return client;
|
||||
} catch (error) {
|
||||
await closeAccount();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly tracker_: ActionableClient,
|
||||
userData: UserData,
|
||||
private readonly profileDirectory: string,
|
||||
private readonly apiPort_: number,
|
||||
private readonly apiToken_: string,
|
||||
private readonly cleanUp_: ()=> Promise<void>,
|
||||
) {
|
||||
this.email = userData.email;
|
||||
}
|
||||
|
||||
public async close() {
|
||||
await this.execCliCommand_('server', 'stop');
|
||||
await this.cleanUp_();
|
||||
}
|
||||
|
||||
private get cliCommandArguments() {
|
||||
return [
|
||||
'start-no-build',
|
||||
'--profile', this.profileDirectory,
|
||||
'--env', 'dev',
|
||||
];
|
||||
}
|
||||
|
||||
public getHelpText() {
|
||||
return [
|
||||
`Client ${this.email}:`,
|
||||
`\tCommand: cd ${quotePath(cliDirectory)} && ${commandToString('yarn', this.cliCommandArguments)}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
private async execCliCommand_(commandName: string, ...args: string[]) {
|
||||
assert.match(commandName, /^[a-z]/, 'Command name must start with a lowercase letter.');
|
||||
const commandResult = await execa('yarn', [
|
||||
...this.cliCommandArguments,
|
||||
commandName,
|
||||
...args,
|
||||
], {
|
||||
cwd: cliDirectory,
|
||||
// Connects /dev/null to stdin
|
||||
stdin: 'ignore',
|
||||
});
|
||||
logger.debug('Ran command: ', commandResult.command, commandResult.exitCode);
|
||||
logger.debug(' Output: ', commandResult.stdout);
|
||||
return commandResult;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-dupe-class-members -- This is not a duplicate class member
|
||||
private async execApiCommand_(method: 'GET', route: string): Promise<Json>;
|
||||
// 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<Json>;
|
||||
// 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<Json> {
|
||||
route = route.replace(/^[/]/, '');
|
||||
const url = new URL(`http://localhost:${this.apiPort_}/${route}`);
|
||||
url.searchParams.append('token', this.apiToken_);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request to ${route} failed with error: ${await response.text()}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async execPagedApiCommand_<Result>(
|
||||
method: 'GET',
|
||||
route: string,
|
||||
params: Record<string, string>,
|
||||
deserializeItem: (data: Json)=> Result,
|
||||
): Promise<Result[]> {
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
const results: Result[] = [];
|
||||
let hasMore = true;
|
||||
for (let page = 1; hasMore; page++) {
|
||||
searchParams.set('page', String(page));
|
||||
searchParams.set('limit', '10');
|
||||
const response = await this.execApiCommand_(
|
||||
method, `${route}?${searchParams}`,
|
||||
);
|
||||
if (
|
||||
typeof response !== 'object'
|
||||
|| !('has_more' in response)
|
||||
|| !('items' in response)
|
||||
|| !Array.isArray(response.items)
|
||||
) {
|
||||
throw new Error(`Invalid response: ${JSON.stringify(response)}`);
|
||||
}
|
||||
hasMore = !!response.has_more;
|
||||
|
||||
for (const item of response.items) {
|
||||
results.push(deserializeItem(item));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async decrypt_() {
|
||||
// E2EE decryption can occasionally fail with "Master key is not loaded:".
|
||||
// Allow e2ee decryption to be retried:
|
||||
await retryWithCount(async () => {
|
||||
const result = await this.execCliCommand_('e2ee', 'decrypt', '--force');
|
||||
if (!result.stdout.includes('Completed decryption.')) {
|
||||
throw new Error(`Decryption did not complete: ${result.stdout}`);
|
||||
}
|
||||
}, {
|
||||
count: 3,
|
||||
onFail: async (error)=>{
|
||||
logger.warn('E2EE decryption failed:', error);
|
||||
logger.info('Syncing before retry...');
|
||||
await this.execCliCommand_('sync');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async sync() {
|
||||
logger.info('Sync', this.email);
|
||||
|
||||
await this.tracker_.sync();
|
||||
|
||||
const result = await this.execCliCommand_('sync');
|
||||
if (result.stdout.match(/Last error:/i)) {
|
||||
throw new Error(`Sync failed: ${result.stdout}`);
|
||||
}
|
||||
|
||||
await this.decrypt_();
|
||||
}
|
||||
|
||||
public async createFolder(folder: FolderMetadata) {
|
||||
logger.info('Create folder', folder.id, 'in', `${folder.parentId ?? 'root'}/${this.email}`);
|
||||
await this.tracker_.createFolder(folder);
|
||||
|
||||
await this.execApiCommand_('POST', '/folders', {
|
||||
id: folder.id,
|
||||
title: folder.title,
|
||||
parent_id: folder.parentId ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
private async assertNoteMatchesState_(expected: NoteData) {
|
||||
assert.equal(
|
||||
(await this.execCliCommand_('cat', expected.id)).stdout,
|
||||
`${expected.title}\n\n${expected.body}`,
|
||||
'note should exist',
|
||||
);
|
||||
}
|
||||
|
||||
public async createNote(note: NoteData) {
|
||||
logger.info('Create note', note.id, 'in', `${note.parentId}/${this.email}`);
|
||||
await this.tracker_.createNote(note);
|
||||
|
||||
await this.execApiCommand_('POST', '/notes', {
|
||||
id: note.id,
|
||||
title: note.title,
|
||||
body: note.body,
|
||||
parent_id: note.parentId ?? '',
|
||||
});
|
||||
await this.assertNoteMatchesState_(note);
|
||||
}
|
||||
|
||||
public async updateNote(note: NoteData) {
|
||||
logger.info('Update note', note.id, 'in', `${note.parentId}/${this.email}`);
|
||||
await this.tracker_.updateNote(note);
|
||||
await this.execApiCommand_('PUT', `/notes/${encodeURIComponent(note.id)}`, {
|
||||
title: note.title,
|
||||
body: note.body,
|
||||
parent_id: note.parentId ?? '',
|
||||
});
|
||||
await this.assertNoteMatchesState_(note);
|
||||
}
|
||||
|
||||
public async deleteFolder(id: string) {
|
||||
logger.info('Delete folder', id, 'in', this.email);
|
||||
await this.tracker_.deleteFolder(id);
|
||||
|
||||
await this.execCliCommand_('rmbook', '--permanent', '--force', id);
|
||||
}
|
||||
|
||||
public async shareFolder(id: string, shareWith: Client) {
|
||||
await this.tracker_.shareFolder(id, shareWith);
|
||||
|
||||
logger.info('Share', id, 'with', shareWith.email);
|
||||
await this.execCliCommand_('share', 'add', id, shareWith.email);
|
||||
await this.sync();
|
||||
await shareWith.sync();
|
||||
|
||||
const shareWithIncoming = JSON.parse((await shareWith.execCliCommand_('share', 'list', '--json')).stdout);
|
||||
const pendingInvitations = shareWithIncoming.invitations.filter((invitation: unknown) => {
|
||||
if (typeof invitation !== 'object' || !('accepted' in invitation)) {
|
||||
throw new Error('Invalid invitation format');
|
||||
}
|
||||
return !invitation.accepted;
|
||||
});
|
||||
assert.deepEqual(pendingInvitations, [
|
||||
{
|
||||
accepted: false,
|
||||
waiting: true,
|
||||
rejected: false,
|
||||
folderId: id,
|
||||
fromUser: {
|
||||
email: this.email,
|
||||
},
|
||||
},
|
||||
], 'there should be a single incoming share from the expected user');
|
||||
|
||||
await shareWith.execCliCommand_('share', 'accept', id);
|
||||
}
|
||||
|
||||
public async moveItem(itemId: ItemId, newParentId: ItemId) {
|
||||
logger.info('Move', itemId, 'to', newParentId);
|
||||
await this.tracker_.moveItem(itemId, newParentId);
|
||||
const movingToRoot = !newParentId;
|
||||
await this.execCliCommand_('mv', itemId, movingToRoot ? 'root' : newParentId);
|
||||
}
|
||||
|
||||
public async listNotes() {
|
||||
const params = {
|
||||
fields: 'id,parent_id,body,title,is_conflict,conflict_original_id',
|
||||
include_deleted: '1',
|
||||
include_conflicts: '1',
|
||||
};
|
||||
return await this.execPagedApiCommand_(
|
||||
'GET',
|
||||
'/notes',
|
||||
params,
|
||||
item => ({
|
||||
id: getStringProperty(item, 'id'),
|
||||
parentId: getNumberProperty(item, 'is_conflict') === 1 ? (
|
||||
`[conflicts for ${getStringProperty(item, 'conflict_original_id')} in ${this.email}]`
|
||||
) : getStringProperty(item, 'parent_id'),
|
||||
title: getStringProperty(item, 'title'),
|
||||
body: getStringProperty(item, 'body'),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async listFolders() {
|
||||
const params = {
|
||||
fields: 'id,parent_id,title',
|
||||
include_deleted: '1',
|
||||
};
|
||||
return await this.execPagedApiCommand_(
|
||||
'GET',
|
||||
'/folders',
|
||||
params,
|
||||
item => ({
|
||||
id: getStringProperty(item, 'id'),
|
||||
parentId: getStringProperty(item, 'parent_id'),
|
||||
title: getStringProperty(item, 'title'),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async randomFolder(options: RandomFolderOptions) {
|
||||
return this.tracker_.randomFolder(options);
|
||||
}
|
||||
|
||||
public async allFolderDescendants(parentId: ItemId) {
|
||||
return this.tracker_.allFolderDescendants(parentId);
|
||||
}
|
||||
|
||||
public async randomNote() {
|
||||
return this.tracker_.randomNote();
|
||||
}
|
||||
|
||||
public async checkState(_allClients: Client[]) {
|
||||
logger.info('Check state', this.email);
|
||||
|
||||
type ItemSlice = { id: string };
|
||||
const compare = (a: ItemSlice, b: ItemSlice) => {
|
||||
if (a.id === b.id) return 0;
|
||||
return a.id < b.id ? -1 : 1;
|
||||
};
|
||||
|
||||
const assertNoAdjacentEqualIds = (sortedById: ItemSlice[], assertionLabel: string) => {
|
||||
for (let i = 1; i < sortedById.length; i++) {
|
||||
const current = sortedById[i];
|
||||
const previous = sortedById[i - 1];
|
||||
assert.notEqual(
|
||||
current.id,
|
||||
previous.id,
|
||||
`[${assertionLabel}] item ${i} should have a different ID from item ${i - 1}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const checkNoteState = async () => {
|
||||
const notes = [...await this.listNotes()];
|
||||
const expectedNotes = [...await this.tracker_.listNotes()];
|
||||
|
||||
notes.sort(compare);
|
||||
expectedNotes.sort(compare);
|
||||
|
||||
assertNoAdjacentEqualIds(notes, 'notes');
|
||||
assertNoAdjacentEqualIds(expectedNotes, 'expectedNotes');
|
||||
assert.deepEqual(notes, expectedNotes, 'should have the same notes as the expected state');
|
||||
};
|
||||
|
||||
const checkFolderState = async () => {
|
||||
const folders = [...await this.listFolders()];
|
||||
const expectedFolders = [...await this.tracker_.listFolders()];
|
||||
|
||||
folders.sort(compare);
|
||||
expectedFolders.sort(compare);
|
||||
|
||||
assertNoAdjacentEqualIds(folders, 'folders');
|
||||
assertNoAdjacentEqualIds(expectedFolders, 'expectedFolders');
|
||||
assert.deepEqual(folders, expectedFolders, 'should have the same folders as the expected state');
|
||||
};
|
||||
|
||||
await checkNoteState();
|
||||
await checkFolderState();
|
||||
}
|
||||
}
|
||||
|
||||
export default Client;
|
||||
|
54
packages/tools/fuzzer/ClientPool.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import ActionTracker from './ActionTracker';
|
||||
import Client from './Client';
|
||||
import { CleanupTask, FuzzContext } from './types';
|
||||
|
||||
type AddCleanupTask = (task: CleanupTask)=> void;
|
||||
type ClientFilter = (client: Client)=> boolean;
|
||||
|
||||
export default class ClientPool {
|
||||
public static async create(
|
||||
context: FuzzContext,
|
||||
clientCount: number,
|
||||
addCleanupTask: AddCleanupTask,
|
||||
) {
|
||||
if (clientCount <= 0) throw new Error('There must be at least 1 client');
|
||||
|
||||
const actionTracker = new ActionTracker(context);
|
||||
const clientPool: Client[] = [];
|
||||
for (let i = 0; i < clientCount; i++) {
|
||||
const client = await Client.create(actionTracker, context);
|
||||
addCleanupTask(() => client.close());
|
||||
clientPool.push(client);
|
||||
}
|
||||
|
||||
return new ClientPool(context, clientPool);
|
||||
}
|
||||
public constructor(
|
||||
private readonly context_: FuzzContext,
|
||||
public readonly clients: Client[],
|
||||
) { }
|
||||
|
||||
public randomClient(filter: ClientFilter = ()=>true) {
|
||||
const clients = this.clients.filter(filter);
|
||||
return clients[
|
||||
this.context_.randInt(0, clients.length)
|
||||
];
|
||||
}
|
||||
|
||||
public async checkState() {
|
||||
for (const client of this.clients) {
|
||||
await client.checkState(this.clients);
|
||||
}
|
||||
}
|
||||
|
||||
public async syncAll() {
|
||||
for (const client of this.clients) {
|
||||
await client.sync();
|
||||
}
|
||||
}
|
||||
|
||||
public helpText() {
|
||||
return this.clients.map(client => client.getHelpText()).join('\n\n');
|
||||
}
|
||||
}
|
||||
|
79
packages/tools/fuzzer/Server.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { join } from 'path';
|
||||
import { HttpMethod, Json, UserData } from './types';
|
||||
import { packagesDir } from './constants';
|
||||
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
|
||||
import { Env } from '@joplin/lib/models/Setting';
|
||||
import execa = require('execa');
|
||||
import { msleep } from '@joplin/utils/time';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('Server');
|
||||
|
||||
const createApi = async (serverUrl: string, adminAuth: UserData) => {
|
||||
const api = new JoplinServerApi({
|
||||
baseUrl: () => serverUrl,
|
||||
userContentBaseUrl: () => serverUrl,
|
||||
password: () => adminAuth.password,
|
||||
username: () => adminAuth.email,
|
||||
session: ()=>null,
|
||||
env: Env.Dev,
|
||||
});
|
||||
await api.loadSession();
|
||||
return api;
|
||||
};
|
||||
|
||||
export default class Server {
|
||||
private api_: JoplinServerApi|null = null;
|
||||
private server_: execa.ExecaChildProcess<string>;
|
||||
|
||||
public constructor(
|
||||
private readonly serverUrl_: string,
|
||||
private readonly adminAuth_: UserData,
|
||||
) {
|
||||
const serverDir = join(packagesDir, 'server');
|
||||
const mainEntrypoint = join(serverDir, 'dist', 'app.js');
|
||||
this.server_ = execa.node(mainEntrypoint, [
|
||||
'--env', 'dev',
|
||||
], {
|
||||
env: { JOPLIN_IS_TESTING: '1' },
|
||||
cwd: join(packagesDir, 'server'),
|
||||
stdin: 'ignore', // No stdin
|
||||
// For debugging:
|
||||
// stderr: process.stderr,
|
||||
// stdout: process.stdout,
|
||||
});
|
||||
}
|
||||
|
||||
public async checkConnection() {
|
||||
let lastError;
|
||||
for (let retry = 0; retry < 30; retry++) {
|
||||
try {
|
||||
const response = await fetch(`${this.serverUrl_}api/ping`);
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
await msleep(500);
|
||||
}
|
||||
if (lastError) {
|
||||
throw lastError;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async execApi(method: HttpMethod, route: string, action: Json) {
|
||||
this.api_ ??= await createApi(this.serverUrl_, this.adminAuth_);
|
||||
logger.debug('API EXEC', method, route, action);
|
||||
const result = await this.api_.exec(method, route, {}, action);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async close() {
|
||||
this.server_.cancel();
|
||||
logger.info('Closed the server.');
|
||||
}
|
||||
}
|
||||
|
||||
|
5
packages/tools/fuzzer/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
export const packagesDir = dirname(dirname(__dirname));
|
||||
export const cliDirectory = join(packagesDir, 'app-cli');
|
||||
|
362
packages/tools/fuzzer/sync-fuzzer.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import { join } from 'path';
|
||||
import { exists, mkdir, remove } from 'fs-extra';
|
||||
import Setting, { Env } from '@joplin/lib/models/Setting';
|
||||
import Logger, { TargetType } from '@joplin/utils/Logger';
|
||||
import { waitForCliInput } from '@joplin/utils/cli';
|
||||
import Server from './Server';
|
||||
import { CleanupTask, FuzzContext } from './types';
|
||||
import ClientPool from './ClientPool';
|
||||
import retryWithCount from './utils/retryWithCount';
|
||||
import Client from './Client';
|
||||
import SeededRandom from './utils/SeededRandom';
|
||||
import { env } from 'process';
|
||||
import yargs = require('yargs');
|
||||
import { strict as assert } from 'assert';
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node');
|
||||
|
||||
const globalLogger = new Logger();
|
||||
globalLogger.addTarget(TargetType.Console);
|
||||
Logger.initializeGlobalLogger(globalLogger);
|
||||
const logger = Logger.create('fuzzer');
|
||||
|
||||
const createProfilesDirectory = async () => {
|
||||
const path = join(__dirname, 'profiles-tmp');
|
||||
if (await exists(path)) {
|
||||
throw new Error([
|
||||
'Another instance of the sync fuzzer may be running!',
|
||||
'The parent directory for test profiles already exists. An instance of the fuzzer is either already running or was closed before it could clean up.',
|
||||
`To ignore this issue, delete ${JSON.stringify(path)} and re-run the fuzzer.`,
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
await mkdir(path);
|
||||
return {
|
||||
path,
|
||||
remove: async () => {
|
||||
await remove(path);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const doRandomAction = async (context: FuzzContext, client: Client, clientPool: ClientPool) => {
|
||||
const selectOrCreateParentFolder = async () => {
|
||||
let parentId = (await client.randomFolder({}))?.id;
|
||||
|
||||
// Create a toplevel folder to serve as this
|
||||
// folder's parent if none exist yet
|
||||
if (!parentId) {
|
||||
parentId = uuid.create();
|
||||
await client.createFolder({
|
||||
parentId: '',
|
||||
id: parentId,
|
||||
title: 'Parent folder',
|
||||
});
|
||||
}
|
||||
|
||||
return parentId;
|
||||
};
|
||||
|
||||
const selectOrCreateNote = async () => {
|
||||
let note = await client.randomNote();
|
||||
|
||||
if (!note) {
|
||||
await client.createNote({
|
||||
parentId: await selectOrCreateParentFolder(),
|
||||
id: uuid.create(),
|
||||
title: 'Test note',
|
||||
body: 'Body',
|
||||
});
|
||||
|
||||
note = await client.randomNote();
|
||||
assert.ok(note, 'should have selected a random note');
|
||||
}
|
||||
|
||||
return note;
|
||||
};
|
||||
|
||||
const actions = {
|
||||
newSubfolder: async () => {
|
||||
const folderId = uuid.create();
|
||||
const parentId = await selectOrCreateParentFolder();
|
||||
|
||||
await client.createFolder({
|
||||
parentId: parentId,
|
||||
id: folderId,
|
||||
title: 'Subfolder',
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
newToplevelFolder: async () => {
|
||||
const folderId = uuid.create();
|
||||
await client.createFolder({
|
||||
parentId: null,
|
||||
id: folderId,
|
||||
title: `Folder ${context.randInt(0, 1000)}`,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
newNote: async () => {
|
||||
const parentId = await selectOrCreateParentFolder();
|
||||
await client.createNote({
|
||||
parentId: parentId,
|
||||
title: `Test (x${context.randInt(0, 1000)})`,
|
||||
body: 'Testing...',
|
||||
id: uuid.create(),
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
renameNote: async () => {
|
||||
const note = await selectOrCreateNote();
|
||||
|
||||
await client.updateNote({
|
||||
...note,
|
||||
title: `Renamed (${context.randInt(0, 1000)})`,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
updateNoteBody: async () => {
|
||||
const note = await selectOrCreateNote();
|
||||
|
||||
await client.updateNote({
|
||||
...note,
|
||||
body: `${note.body}\n\nUpdated.\n`,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
moveNote: async () => {
|
||||
const note = await client.randomNote();
|
||||
if (!note) return false;
|
||||
const targetParent = await client.randomFolder({
|
||||
filter: folder => folder.id !== note.parentId,
|
||||
});
|
||||
if (!targetParent) return false;
|
||||
|
||||
await client.moveItem(note.id, targetParent.id);
|
||||
|
||||
return true;
|
||||
},
|
||||
shareFolder: async () => {
|
||||
const target = await client.randomFolder({
|
||||
filter: candidate => (
|
||||
!candidate.parentId && !candidate.isShareRoot
|
||||
),
|
||||
});
|
||||
if (!target) return false;
|
||||
|
||||
const other = clientPool.randomClient(c => c !== client);
|
||||
await client.shareFolder(target.id, other);
|
||||
return true;
|
||||
},
|
||||
deleteFolder: async () => {
|
||||
const target = await client.randomFolder({});
|
||||
if (!target) return false;
|
||||
|
||||
await client.deleteFolder(target.id);
|
||||
return true;
|
||||
},
|
||||
moveFolderToToplevel: async () => {
|
||||
const target = await client.randomFolder({
|
||||
// Don't choose items that are already toplevel
|
||||
filter: item => !!item.parentId,
|
||||
});
|
||||
if (!target) return false;
|
||||
|
||||
await client.moveItem(target.id, '');
|
||||
return true;
|
||||
},
|
||||
moveFolderTo: async () => {
|
||||
const target = await client.randomFolder({
|
||||
// Don't move shared folders (should not be allowed by the GUI in the main apps).
|
||||
filter: item => !item.isShareRoot,
|
||||
});
|
||||
if (!target) return false;
|
||||
|
||||
const targetDescendants = new Set(await client.allFolderDescendants(target.id));
|
||||
|
||||
const newParent = await client.randomFolder({
|
||||
filter: (item) => {
|
||||
// Avoid making the folder a child of itself
|
||||
return !targetDescendants.has(item.id);
|
||||
},
|
||||
});
|
||||
if (!newParent) return false;
|
||||
|
||||
await client.moveItem(target.id, newParent.id);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const actionKeys = [...Object.keys(actions)] as (keyof typeof actions)[];
|
||||
|
||||
let result = false;
|
||||
while (!result) { // Loop until an action was done
|
||||
const randomAction = actionKeys[context.randInt(0, actionKeys.length)];
|
||||
logger.info(`Action: ${randomAction} in ${client.email}`);
|
||||
result = await actions[randomAction]();
|
||||
if (!result) {
|
||||
logger.info(` ${randomAction} was skipped (preconditions not met).`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface Options {
|
||||
seed: number;
|
||||
maximumSteps: number;
|
||||
maximumStepsBetweenSyncs: number;
|
||||
clientCount: number;
|
||||
}
|
||||
|
||||
const main = async (options: Options) => {
|
||||
shimInit();
|
||||
Setting.setConstant('env', Env.Dev);
|
||||
|
||||
const cleanupTasks: CleanupTask[] = [];
|
||||
|
||||
const cleanUp = async () => {
|
||||
logger.info('Cleaning up....');
|
||||
while (cleanupTasks.length) {
|
||||
const task = cleanupTasks.pop();
|
||||
try {
|
||||
await task();
|
||||
} catch (error) {
|
||||
logger.warn('Clean up task failed:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Run cleanup on Ctrl-C
|
||||
process.on('SIGINT', async () => {
|
||||
logger.info('Intercepted ctrl-c. Cleaning up...');
|
||||
await cleanUp();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
let clientHelpText;
|
||||
|
||||
try {
|
||||
const joplinServerUrl = 'http://localhost:22300/';
|
||||
const server = new Server(joplinServerUrl, {
|
||||
email: 'admin@localhost',
|
||||
password: env['FUZZER_SERVER_ADMIN_PASSWORD'] ?? 'admin',
|
||||
});
|
||||
cleanupTasks.push(() => server.close());
|
||||
|
||||
if (!await server.checkConnection()) {
|
||||
throw new Error('Could not connect to the server.');
|
||||
}
|
||||
|
||||
const profilesDirectory = await createProfilesDirectory();
|
||||
cleanupTasks.push(profilesDirectory.remove);
|
||||
|
||||
logger.info('Starting with seed', options.seed);
|
||||
const random = new SeededRandom(options.seed);
|
||||
|
||||
const fuzzContext: FuzzContext = {
|
||||
serverUrl: joplinServerUrl,
|
||||
baseDir: profilesDirectory.path,
|
||||
execApi: server.execApi.bind(server),
|
||||
randInt: (a, b) => random.nextInRange(a, b),
|
||||
};
|
||||
const clientPool = await ClientPool.create(
|
||||
fuzzContext,
|
||||
options.clientCount,
|
||||
task => { cleanupTasks.push(task); },
|
||||
);
|
||||
clientHelpText = clientPool.helpText();
|
||||
|
||||
const maxSteps = options.maximumSteps;
|
||||
for (let stepIndex = 1; maxSteps <= 0 || stepIndex <= maxSteps; stepIndex++) {
|
||||
const client = clientPool.randomClient();
|
||||
|
||||
// Ensure that the client starts up-to-date with the other synced clients.
|
||||
await client.sync();
|
||||
|
||||
logger.info('Step', stepIndex, '/', maxSteps > 0 ? maxSteps : 'Infinity');
|
||||
const actionsBeforeFullSync = fuzzContext.randInt(1, options.maximumStepsBetweenSyncs + 1);
|
||||
for (let subStepIndex = 1; subStepIndex <= actionsBeforeFullSync; subStepIndex++) {
|
||||
if (actionsBeforeFullSync > 1) {
|
||||
logger.info('Sub-step', subStepIndex, '/', actionsBeforeFullSync, '(in step', stepIndex, ')');
|
||||
}
|
||||
await doRandomAction(fuzzContext, client, clientPool);
|
||||
}
|
||||
await client.sync();
|
||||
|
||||
// .checkState can fail occasionally due to incomplete
|
||||
// syncs (perhaps because the server is still processing
|
||||
// share-related changes?). Allow this to be retried:
|
||||
await retryWithCount(async () => {
|
||||
await clientPool.checkState();
|
||||
}, {
|
||||
count: 3,
|
||||
onFail: async () => {
|
||||
logger.info('.checkState failed. Syncing all clients...');
|
||||
await clientPool.syncAll();
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('ERROR', error);
|
||||
if (clientHelpText) {
|
||||
logger.info('Client information:\n', clientHelpText);
|
||||
}
|
||||
logger.info('An error occurred. Pausing before continuing cleanup.');
|
||||
await waitForCliInput();
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await cleanUp();
|
||||
|
||||
logger.info('Cleanup complete');
|
||||
process.exit();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
void yargs
|
||||
.usage('$0 <cmd>')
|
||||
.command(
|
||||
'start',
|
||||
[
|
||||
'Starts the synchronization fuzzer. The fuzzer starts Joplin Server, creates multiple CLI clients, and attempts to find sync bugs.\n\n',
|
||||
'The fuzzer starts Joplin Server in development mode, using the existing development mode database and uses the admin@localhost user to',
|
||||
'create and set up user accounts.\n',
|
||||
'Use the FUZZER_SERVER_ADMIN_PASSWORD environment variable to specify the admin@localhost password for this dev version of Joplin Server.\n\n',
|
||||
'If the fuzzer detects incorrect/unexpected client state, it pauses, allowing the profile directories and databases',
|
||||
'of the clients to be inspected.',
|
||||
].join(' '),
|
||||
(yargs) => {
|
||||
return yargs.options({
|
||||
'seed': { type: 'number', default: 12345 },
|
||||
'steps': {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
defaultDescription: 'The maximum number of steps to take before stopping the fuzzer. Set to zero for an unlimited number of steps.',
|
||||
},
|
||||
'steps-between-syncs': {
|
||||
type: 'number',
|
||||
default: 3,
|
||||
defaultDescription: 'The maximum number of sub-steps taken before all clients are synchronised.',
|
||||
},
|
||||
'clients': {
|
||||
type: 'number',
|
||||
default: 3,
|
||||
defaultDescription: 'Number of client apps to create.',
|
||||
},
|
||||
});
|
||||
},
|
||||
async (argv) => {
|
||||
await main({
|
||||
seed: argv.seed,
|
||||
maximumSteps: argv.steps,
|
||||
clientCount: argv.clients,
|
||||
maximumStepsBetweenSyncs: argv['steps-between-syncs'],
|
||||
});
|
||||
},
|
||||
)
|
||||
.help()
|
||||
.argv;
|
62
packages/tools/fuzzer/types.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type Client from './Client';
|
||||
|
||||
export type Json = string|number|Json[]|{ [key: string]: Json };
|
||||
|
||||
export type HttpMethod = 'GET'|'POST'|'DELETE'|'PUT'|'PATCH';
|
||||
|
||||
export type ItemId = string;
|
||||
export type NoteData = {
|
||||
parentId: ItemId;
|
||||
id: ItemId;
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
export type FolderMetadata = {
|
||||
parentId: ItemId;
|
||||
id: ItemId;
|
||||
title: string;
|
||||
};
|
||||
export type FolderData = FolderMetadata & {
|
||||
childIds: ItemId[];
|
||||
isShareRoot: boolean;
|
||||
};
|
||||
export type TreeItem = NoteData|FolderData;
|
||||
|
||||
export const isFolder = (item: TreeItem): item is FolderData => {
|
||||
return 'childIds' in item;
|
||||
};
|
||||
|
||||
export interface FuzzContext {
|
||||
serverUrl: string;
|
||||
baseDir: string;
|
||||
execApi: (method: HttpMethod, route: string, debugAction: Json)=> Promise<Json>;
|
||||
randInt: (low: number, high: number)=> number;
|
||||
}
|
||||
|
||||
export interface RandomFolderOptions {
|
||||
filter?: (folder: FolderData)=> boolean;
|
||||
}
|
||||
|
||||
export interface ActionableClient {
|
||||
createFolder(data: FolderMetadata): Promise<void>;
|
||||
shareFolder(id: ItemId, shareWith: Client): Promise<void>;
|
||||
deleteFolder(id: ItemId): Promise<void>;
|
||||
createNote(data: NoteData): Promise<void>;
|
||||
updateNote(data: NoteData): Promise<void>;
|
||||
moveItem(itemId: ItemId, newParentId: ItemId): Promise<void>;
|
||||
sync(): Promise<void>;
|
||||
|
||||
listNotes(): Promise<NoteData[]>;
|
||||
listFolders(): Promise<FolderMetadata[]>;
|
||||
allFolderDescendants(parentId: ItemId): Promise<ItemId[]>;
|
||||
randomFolder(options: RandomFolderOptions): Promise<FolderMetadata>;
|
||||
randomNote(): Promise<NoteData>;
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export type CleanupTask = ()=> Promise<void>;
|
||||
|
52
packages/tools/fuzzer/utils/SeededRandom.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
|
||||
// SeededRandom provides a very simple random number generator
|
||||
// that can be seeded (since NodeJS built-ins can't).
|
||||
//
|
||||
// See also:
|
||||
// - https://arxiv.org/pdf/1704.00358
|
||||
// - https://en.wikipedia.org/wiki/Middle-square_method
|
||||
|
||||
// Some large odd number, see https://en.wikipedia.org/wiki/Weyl_sequence
|
||||
const step = BigInt('0x12345678ABCDE123'); // uint64
|
||||
const maxSize = BigInt(1) << BigInt(64);
|
||||
|
||||
const extractMiddle = (value: bigint, halfSize: bigint) => {
|
||||
// Remove the lower quarter
|
||||
const quarterSize = halfSize / BigInt(2);
|
||||
value >>= quarterSize;
|
||||
|
||||
// Remove the upper quarter
|
||||
const halfMaximumValue = BigInt(1) << halfSize;
|
||||
value %= halfMaximumValue;
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export default class SeededRandom {
|
||||
private value_: bigint;
|
||||
private nextStep_: bigint = step;
|
||||
private halfSize_ = BigInt(32);
|
||||
|
||||
public constructor(seed: number) {
|
||||
this.value_ = BigInt(seed);
|
||||
}
|
||||
|
||||
public next() {
|
||||
this.value_ = this.value_ * this.value_ + this.nextStep_;
|
||||
|
||||
// Move to the next item in the sequence. Mod to prevent from getting
|
||||
// too large. See https://en.wikipedia.org/wiki/Weyl_sequence.
|
||||
this.nextStep_ = (step + this.nextStep_) % maxSize;
|
||||
|
||||
this.value_ = extractMiddle(this.value_, this.halfSize_);
|
||||
return this.value_;
|
||||
}
|
||||
|
||||
// The resultant range includes `a` but excludes `b`.
|
||||
public nextInRange(a: number, b: number) {
|
||||
if (b <= a + 1) return a;
|
||||
|
||||
const range = b - a;
|
||||
return Number(this.next() % BigInt(range)) + a;
|
||||
}
|
||||
}
|
11
packages/tools/fuzzer/utils/getNumberProperty.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import getProperty from './getProperty';
|
||||
|
||||
const getNumberProperty = (object: unknown, propertyName: string) => {
|
||||
const value = getProperty(object, propertyName);
|
||||
if (typeof value !== 'number') {
|
||||
throw new Error(`Property value is not a string (is ${typeof value})`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export default getNumberProperty;
|
15
packages/tools/fuzzer/utils/getProperty.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
const getProperty = (object: unknown, propertyName: string) => {
|
||||
if (typeof object !== 'object' || object === null) {
|
||||
throw new Error(`Cannot access property ${JSON.stringify(propertyName)} on non-object`);
|
||||
}
|
||||
|
||||
if (!(propertyName in object)) {
|
||||
throw new Error(`No such property ${JSON.stringify(propertyName)} in object`);
|
||||
}
|
||||
|
||||
return object[propertyName as keyof object];
|
||||
};
|
||||
|
||||
export default getProperty;
|
||||
|
11
packages/tools/fuzzer/utils/getStringProperty.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import getProperty from './getProperty';
|
||||
|
||||
const getStringProperty = (object: unknown, propertyName: string) => {
|
||||
const value = getProperty(object, propertyName);
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`Property value is not a string (is ${typeof value})`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export default getStringProperty;
|
21
packages/tools/fuzzer/utils/retryWithCount.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
interface Options {
|
||||
count: number;
|
||||
onFail: (error: Error)=> Promise<void>;
|
||||
}
|
||||
|
||||
const retryWithCount = async (task: ()=> Promise<void>, { count, onFail }: Options) => {
|
||||
let lastError: Error|null = null;
|
||||
for (let retry = 0; retry < count; retry ++) {
|
||||
try {
|
||||
return await task();
|
||||
} catch (error) {
|
||||
await onFail(error);
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastError) throw lastError;
|
||||
};
|
||||
|
||||
export default retryWithCount;
|
||||
|
@@ -159,6 +159,8 @@
|
||||
"v3.3.13": true,
|
||||
"android-v3.3.9": true,
|
||||
"android-v3.3.10": true,
|
||||
"ios-v13.3.8": true
|
||||
"ios-v13.3.8": true,
|
||||
"android-v3.3.11": true,
|
||||
"ios-v13.3.9": true
|
||||
}
|
||||
}
|
@@ -95,11 +95,6 @@
|
||||
"imageName": "Route4Me.png",
|
||||
"githubUser": "route4me"
|
||||
},
|
||||
{
|
||||
"url": "https://casinoreviews.net",
|
||||
"title": "Casino Reviews",
|
||||
"imageName": "CasinoReviews.png"
|
||||
},
|
||||
{
|
||||
"url": "https://topagency.webflow.io",
|
||||
"title": "WebDesignAgency",
|
||||
@@ -124,13 +119,6 @@
|
||||
"imageName": "Slotozilla.png",
|
||||
"alt": "casino without making any upfront cost"
|
||||
},
|
||||
{
|
||||
"url": "https://www.reddit.com/r/tiktokRise/",
|
||||
"title": "Tiktok Rise",
|
||||
"imageName": "TiktokRise.jpg",
|
||||
"alt": "Tiktok Rise",
|
||||
"githubUser": "knickman"
|
||||
},
|
||||
{
|
||||
"url": "https://essaywriter.pro",
|
||||
"title": "write my essay services by EssayWriter",
|
||||
@@ -154,6 +142,26 @@
|
||||
"title": "high-quality paper writing service PaperWriter",
|
||||
"imageName": "PaperWriter.png",
|
||||
"alt": "high-quality paper writing service PaperWriter"
|
||||
},
|
||||
{
|
||||
"url": "https://homeworkguy.org/someone-to-take-my-online-class",
|
||||
"title": "someone to take my online class",
|
||||
"imageName": "HomeworkGuy.png",
|
||||
"alt": "someone to take my online class",
|
||||
"githubUser": "Nftsworld007"
|
||||
},
|
||||
{
|
||||
"url": "https://www.bestetf.net/",
|
||||
"title": "BestETF",
|
||||
"imageName": "BestEtf.png",
|
||||
"alt": "BestETF",
|
||||
"githubUser": "traspire"
|
||||
},
|
||||
{
|
||||
"url": "https://freespinny.io/free-spins-no-deposit/",
|
||||
"title": "Freespinny.io Free Spins Bonus site",
|
||||
"imageName": "Freespinny.png",
|
||||
"alt": "Freespinny.io Free Spins Bonus site"
|
||||
}
|
||||
],
|
||||
"orgsOld": [
|
||||
@@ -195,6 +203,13 @@
|
||||
"title": "Achieve academic success with Edubirdie — your trusted partner for expert writing assistance and resources!",
|
||||
"imageName": "Edubirdie.png",
|
||||
"alt": "EduBirdie"
|
||||
},
|
||||
{
|
||||
"url": "https://www.reddit.com/r/tiktokRise/",
|
||||
"title": "Tiktok Rise",
|
||||
"imageName": "TiktokRise.jpg",
|
||||
"alt": "Tiktok Rise",
|
||||
"githubUser": "knickman"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "11.2.0",
|
||||
"knex": "3.1.0",
|
||||
"koa": "2.16.0",
|
||||
"koa": "2.16.1",
|
||||
"koa-body": "6.0.1",
|
||||
"pg-boss": "10.1.6",
|
||||
"sqlite3": "5.1.6"
|
||||
|
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "28.0.3",
|
||||
"@rollup/plugin-node-resolve": "15.3.1",
|
||||
"@rollup/plugin-node-resolve": "16.0.1",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"browserify": "14.5.0",
|
||||
"rollup": "4.2.0",
|
||||
|
@@ -124,6 +124,8 @@ export function getStyleProp(node, name) {
|
||||
|
||||
const o = css.parse('div {' + style + '}');
|
||||
if (!o.stylesheet.rules.length) return null;
|
||||
const prop = o.stylesheet.rules[0].declarations.find(d => d.property.toLowerCase() === name);
|
||||
const prop = o.stylesheet.rules[0].declarations.find(d => {
|
||||
return d.type === 'declaration' && d.property.toLowerCase() === name;
|
||||
});
|
||||
return prop ? prop.value : null;
|
||||
}
|
||||
|
@@ -1,16 +1,17 @@
|
||||
|
||||
const readline = require('readline/promises');
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
export const isTTY = () => process.stdin.isTTY;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
let readlineInterface: any = null;
|
||||
const waitForCliInput = async () => {
|
||||
export const waitForCliInput = async () => {
|
||||
readlineInterface ??= readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
if (process.stdin.isTTY) {
|
||||
if (isTTY()) {
|
||||
const green = '\x1b[92m';
|
||||
const reset = '\x1b[0m';
|
||||
await readlineInterface.question(`${green}[Press enter to continue]${reset}`);
|
||||
@@ -21,4 +22,3 @@ const waitForCliInput = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export default waitForCliInput;
|
@@ -18,7 +18,8 @@
|
||||
"./types": "./dist/types.js",
|
||||
"./url": "./dist/url.js",
|
||||
"./ipc": "./dist/ipc.js",
|
||||
"./path": "./dist/path.js"
|
||||
"./path": "./dist/path.js",
|
||||
"./cli": "./dist/cli.js"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@@ -1,5 +1,9 @@
|
||||
# Joplin Android Changelog
|
||||
|
||||
## [android-v3.3.11](https://github.com/laurent22/joplin/releases/tag/android-v3.3.11) (Pre-release) - 2025-07-09T22:51:55Z
|
||||
|
||||
- Fixed: Biometrics: Fix notebook list can still be accessed when the app is locked (#12691 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [android-v3.3.10](https://github.com/laurent22/joplin/releases/tag/android-v3.3.10) (Pre-release) - 2025-06-10T08:07:25Z
|
||||
|
||||
- New: Add additional checks when updating sidebar state (#12428 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
@@ -1,5 +1,10 @@
|
||||
# Joplin iOS Changelog
|
||||
|
||||
## [ios-v13.3.9](https://github.com/laurent22/joplin/releases/tag/ios-v13.3.9) - 2025-07-09T23:17:23Z
|
||||
|
||||
- New: Add additional checks when updating sidebar state (#12428 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
- Fixed: Biometrics: Fix notebook list can still be accessed when the app is locked (#12691 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
||||
## [ios-v13.3.8](https://github.com/laurent22/joplin/releases/tag/ios-v13.3.8) - 2025-06-09T17:15:06Z
|
||||
|
||||
- Fixed: Fix error shown the first time a user attempts to record (#12328) (#12314 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
|
||||
|
@@ -1,236 +1,237 @@
|
||||
---
|
||||
updated: 2025-06-01T02:15:29Z
|
||||
updated: 2025-07-06T16:57:49Z
|
||||
---
|
||||
|
||||
# Joplin statistics
|
||||
|
||||
| Name | Value |
|
||||
| ----- | ----- |
|
||||
| Total Windows downloads | 6,559,219 |
|
||||
| Total macOs downloads | 1,988,312 |
|
||||
| Total Linux downloads | 1,516,553 |
|
||||
| Total Windows downloads | 6,736,767 |
|
||||
| Total macOs downloads | 2,006,140 |
|
||||
| Total Linux downloads | 1,545,047 |
|
||||
| Windows % | 65% |
|
||||
| macOS % | 20% |
|
||||
| macOS % | 19% |
|
||||
| Linux % | 15% |
|
||||
|
||||
(p) Indicates pre-releases
|
||||
|
||||
| Version | Date | Windows | macOS | Linux | Total |
|
||||
| ----- | ----- | ----- | ----- | ----- | ----- |
|
||||
| [v3.4.1](https://github.com/laurent22/joplin/releases/tag/v3.4.1) (p) | 2025-05-20T09:59:39Z | 1,523 | 325 | 391 | 2,239 |
|
||||
| [v3.3.12](https://github.com/laurent22/joplin/releases/tag/v3.3.12) | 2025-05-04T18:12:23Z | 130,348 | 18,775 | 21,928 | 171,051 |
|
||||
| [v3.3.10](https://github.com/laurent22/joplin/releases/tag/v3.3.10) | 2025-05-02T19:46:15Z | 23,052 | 5,064 | 1,794 | 29,910 |
|
||||
| [v3.3.9](https://github.com/laurent22/joplin/releases/tag/v3.3.9) | 2025-05-01T21:02:12Z | 21,670 | 6,070 | 1,173 | 28,913 |
|
||||
| [v3.3.7](https://github.com/laurent22/joplin/releases/tag/v3.3.7) (p) | 2025-04-29T13:47:19Z | 716 | 0 | 160 | 876 |
|
||||
| [v3.3.6](https://github.com/laurent22/joplin/releases/tag/v3.3.6) (p) | 2025-04-24T12:27:20Z | 1,017 | 262 | 240 | 1,519 |
|
||||
| [v3.3.5](https://github.com/laurent22/joplin/releases/tag/v3.3.5) (p) | 2025-04-17T13:40:31Z | 1,349 | 281 | 297 | 1,927 |
|
||||
| [v3.3.4](https://github.com/laurent22/joplin/releases/tag/v3.3.4) (p) | 2025-04-07T20:23:35Z | 1,591 | 385 | 363 | 2,339 |
|
||||
| [v3.3.3](https://github.com/laurent22/joplin/releases/tag/v3.3.3) (p) | 2025-03-16T11:52:33Z | 2,665 | 797 | 930 | 4,392 |
|
||||
| [v3.2.13](https://github.com/laurent22/joplin/releases/tag/v3.2.13) | 2025-02-28T14:38:21Z | 220,987 | 32,528 | 44,285 | 297,800 |
|
||||
| [v3.3.2](https://github.com/laurent22/joplin/releases/tag/v3.3.2) (p) | 2025-02-19T17:34:26Z | 2,332 | 558 | 639 | 3,529 |
|
||||
| [v3.3.1](https://github.com/laurent22/joplin/releases/tag/v3.3.1) (p) | 2025-02-16T17:06:26Z | 828 | 166 | 162 | 1,156 |
|
||||
| [v3.2.12](https://github.com/laurent22/joplin/releases/tag/v3.2.12) | 2025-01-23T23:52:04Z | 151,832 | 25,189 | 27,900 | 204,921 |
|
||||
| [v3.2.11](https://github.com/laurent22/joplin/releases/tag/v3.2.11) | 2025-01-13T17:48:21Z | 65,814 | 14,811 | 6,855 | 87,480 |
|
||||
| [v3.2.10](https://github.com/laurent22/joplin/releases/tag/v3.2.10) (p) | 2025-01-10T10:17:28Z | 1,279 | 161 | 184 | 1,624 |
|
||||
| [v3.2.9](https://github.com/laurent22/joplin/releases/tag/v3.2.9) (p) | 2025-01-09T22:58:42Z | 354 | 84 | 50 | 488 |
|
||||
| [v3.2.7](https://github.com/laurent22/joplin/releases/tag/v3.2.7) (p) | 2025-01-06T16:35:41Z | 862 | 150 | 809 | 1,821 |
|
||||
| [v3.2.6](https://github.com/laurent22/joplin/releases/tag/v3.2.6) (p) | 2024-12-23T21:54:40Z | 1,723 | 329 | 474 | 2,526 |
|
||||
| [v3.2.5](https://github.com/laurent22/joplin/releases/tag/v3.2.5) (p) | 2024-12-18T10:41:13Z | 986 | 193 | 217 | 1,396 |
|
||||
| [v3.2.4](https://github.com/laurent22/joplin/releases/tag/v3.2.4) (p) | 2024-12-12T17:59:52Z | 1,002 | 146 | 232 | 1,380 |
|
||||
| [v3.2.3](https://github.com/laurent22/joplin/releases/tag/v3.2.3) (p) | 2024-11-18T00:09:05Z | 2,688 | 548 | 883 | 4,119 |
|
||||
| [v3.2.1](https://github.com/laurent22/joplin/releases/tag/v3.2.1) (p) | 2024-11-10T16:16:27Z | 1,232 | 227 | 345 | 1,804 |
|
||||
| [v3.1.24](https://github.com/laurent22/joplin/releases/tag/v3.1.24) | 2024-11-09T15:08:29Z | 208,026 | 33,468 | 43,792 | 285,286 |
|
||||
| [v3.1.23](https://github.com/laurent22/joplin/releases/tag/v3.1.23) | 2024-11-07T10:56:45Z | 27,269 | 6,728 | 1,520 | 35,517 |
|
||||
| [v3.1.22](https://github.com/laurent22/joplin/releases/tag/v3.1.22) | 2024-11-05T08:59:32Z | 29,049 | 8,695 | 1,223 | 38,967 |
|
||||
| [v3.1.20](https://github.com/laurent22/joplin/releases/tag/v3.1.20) | 2024-10-22T12:21:32Z | 94,807 | 19,206 | 13,917 | 127,930 |
|
||||
| [v3.1.18](https://github.com/laurent22/joplin/releases/tag/v3.1.18) (p) | 2024-10-11T23:27:10Z | 1,504 | 279 | 587 | 2,370 |
|
||||
| [v3.1.17](https://github.com/laurent22/joplin/releases/tag/v3.1.17) (p) | 2024-09-26T11:57:54Z | 1,698 | 348 | 531 | 2,577 |
|
||||
| [v3.1.15](https://github.com/laurent22/joplin/releases/tag/v3.1.15) (p) | 2024-09-17T09:15:10Z | 1,177 | 223 | 492 | 1,892 |
|
||||
| [v3.1.8](https://github.com/laurent22/joplin/releases/tag/v3.1.8) (p) | 2024-09-08T20:32:44Z | 1,217 | 252 | 332 | 1,801 |
|
||||
| [v3.1.6](https://github.com/laurent22/joplin/releases/tag/v3.1.6) (p) | 2024-09-02T13:19:40Z | 960 | 226 | 425 | 1,611 |
|
||||
| [v3.1.4](https://github.com/laurent22/joplin/releases/tag/v3.1.4) (p) | 2024-08-27T17:46:38Z | 941 | 174 | 247 | 1,362 |
|
||||
| [v3.0.15](https://github.com/laurent22/joplin/releases/tag/v3.0.15) | 2024-08-21T09:19:58Z | 203,212 | 37,802 | 45,129 | 286,143 |
|
||||
| [v3.1.3](https://github.com/laurent22/joplin/releases/tag/v3.1.3) (p) | 2024-08-17T13:08:21Z | 1,226 | 274 | 483 | 1,983 |
|
||||
| [v3.1.2](https://github.com/laurent22/joplin/releases/tag/v3.1.2) (p) | 2024-08-16T09:00:59Z | 436 | 101 | 82 | 619 |
|
||||
| [v3.1.1](https://github.com/laurent22/joplin/releases/tag/v3.1.1) (p) | 2024-08-10T11:36:02Z | 1,078 | 201 | 260 | 1,539 |
|
||||
| [v2.14.23](https://github.com/laurent22/joplin/releases/tag/v2.14.23) | 2024-08-07T11:15:25Z | 10,759 | 2,636 | 619 | 14,014 |
|
||||
| [v3.0.14](https://github.com/laurent22/joplin/releases/tag/v3.0.14) | 2024-07-28T13:55:50Z | 89,307 | 18,570 | 18,816 | 126,693 |
|
||||
| [v3.0.12](https://github.com/laurent22/joplin/releases/tag/v3.0.12) | 2024-07-02T17:11:14Z | 44,191 | 12,830 | 7,334 | 64,355 |
|
||||
| [v3.0.11](https://github.com/laurent22/joplin/releases/tag/v3.0.11) (p) | 2024-06-29T10:20:02Z | 839 | 162 | 264 | 1,265 |
|
||||
| [v3.0.10](https://github.com/laurent22/joplin/releases/tag/v3.0.10) (p) | 2024-06-19T15:24:07Z | 1,616 | 289 | 559 | 2,464 |
|
||||
| [v3.0.9](https://github.com/laurent22/joplin/releases/tag/v3.0.9) (p) | 2024-06-12T19:07:50Z | 1,264 | 258 | 377 | 1,899 |
|
||||
| [v3.0.8](https://github.com/laurent22/joplin/releases/tag/v3.0.8) (p) | 2024-05-22T14:20:45Z | 2,658 | 0 | 911 | 3,569 |
|
||||
| [v2.14.22](https://github.com/laurent22/joplin/releases/tag/v2.14.22) | 2024-05-22T19:19:02Z | 142,918 | 30,755 | 25,463 | 199,136 |
|
||||
| [v3.0.6](https://github.com/laurent22/joplin/releases/tag/v3.0.6) (p) | 2024-04-27T13:16:04Z | 3,003 | 687 | 860 | 4,550 |
|
||||
| [v3.0.3](https://github.com/laurent22/joplin/releases/tag/v3.0.3) (p) | 2024-04-18T15:41:38Z | 1,480 | 314 | 331 | 2,125 |
|
||||
| [v3.0.2](https://github.com/laurent22/joplin/releases/tag/v3.0.2) (p) | 2024-03-21T18:18:49Z | 2,864 | 718 | 1,098 | 4,680 |
|
||||
| [v2.14.20](https://github.com/laurent22/joplin/releases/tag/v2.14.20) | 2024-03-18T17:05:17Z | 192,850 | 39,529 | 38,229 | 270,608 |
|
||||
| [v2.14.19](https://github.com/laurent22/joplin/releases/tag/v2.14.19) | 2024-03-08T10:45:16Z | 64,882 | 18,469 | 8,550 | 91,901 |
|
||||
| [v2.14.17](https://github.com/laurent22/joplin/releases/tag/v2.14.17) | 2024-03-01T18:10:26Z | 63,543 | 18,636 | 7,617 | 89,796 |
|
||||
| [v2.14.16](https://github.com/laurent22/joplin/releases/tag/v2.14.16) (p) | 2024-02-22T22:49:10Z | 1,347 | 283 | 377 | 2,007 |
|
||||
| [v2.14.15](https://github.com/laurent22/joplin/releases/tag/v2.14.15) (p) | 2024-02-19T11:24:57Z | 851 | 179 | 189 | 1,219 |
|
||||
| [v2.14.14](https://github.com/laurent22/joplin/releases/tag/v2.14.14) (p) | 2024-02-10T16:03:08Z | 1,270 | 242 | 378 | 1,890 |
|
||||
| [v2.14.13](https://github.com/laurent22/joplin/releases/tag/v2.14.13) (p) | 2024-02-09T16:31:54Z | 409 | 80 | 79 | 568 |
|
||||
| [v2.14.12](https://github.com/laurent22/joplin/releases/tag/v2.14.12) (p) | 2024-02-03T12:11:47Z | 978 | 217 | 244 | 1,439 |
|
||||
| [v2.14.11](https://github.com/laurent22/joplin/releases/tag/v2.14.11) (p) | 2024-01-26T11:53:05Z | 1,264 | 258 | 459 | 1,981 |
|
||||
| [v2.14.10](https://github.com/laurent22/joplin/releases/tag/v2.14.10) (p) | 2024-01-18T22:45:04Z | 2,085 | 267 | 370 | 2,722 |
|
||||
| [v2.13.15](https://github.com/laurent22/joplin/releases/tag/v2.13.15) | 2024-01-15T13:01:19Z | 149,838 | 34,150 | 29,260 | 213,248 |
|
||||
| [v2.13.14](https://github.com/laurent22/joplin/releases/tag/v2.13.14) | 2024-01-13T19:11:04Z | 19,929 | 7,826 | 2,361 | 30,116 |
|
||||
| [v2.14.9](https://github.com/laurent22/joplin/releases/tag/v2.14.9) (p) | 2024-01-11T22:17:59Z | 1,055 | 0 | 241 | 1,296 |
|
||||
| [v2.14.8](https://github.com/laurent22/joplin/releases/tag/v2.14.8) (p) | 2024-01-09T22:57:07Z | 753 | 235 | 173 | 1,161 |
|
||||
| [v2.14.7](https://github.com/laurent22/joplin/releases/tag/v2.14.7) (p) | 2024-01-08T11:51:49Z | 621 | 127 | 173 | 921 |
|
||||
| [v2.14.6](https://github.com/laurent22/joplin/releases/tag/v2.14.6) (p) | 2024-01-06T16:38:32Z | 727 | 149 | 149 | 1,025 |
|
||||
| [v2.13.13](https://github.com/laurent22/joplin/releases/tag/v2.13.13) | 2024-01-06T13:33:11Z | 53,990 | 15,933 | 6,345 | 76,268 |
|
||||
| [v2.13.12](https://github.com/laurent22/joplin/releases/tag/v2.13.12) | 2023-12-31T16:08:02Z | 45,268 | 14,297 | 5,083 | 64,648 |
|
||||
| [v2.13.11](https://github.com/laurent22/joplin/releases/tag/v2.13.11) | 2023-12-24T12:58:53Z | 46,130 | 13,262 | 5,973 | 65,365 |
|
||||
| [v2.13.10](https://github.com/laurent22/joplin/releases/tag/v2.13.10) | 2023-12-22T10:11:08Z | 19,865 | 8,696 | 1,470 | 30,031 |
|
||||
| [v2.13.9](https://github.com/laurent22/joplin/releases/tag/v2.13.9) | 2023-12-09T17:18:58Z | 67,999 | 21,923 | 8,610 | 98,532 |
|
||||
| [v2.13.8](https://github.com/laurent22/joplin/releases/tag/v2.13.8) | 2023-12-03T12:07:08Z | 50,709 | 17,902 | 5,214 | 73,825 |
|
||||
| [v2.13.6](https://github.com/laurent22/joplin/releases/tag/v2.13.6) (p) | 2023-11-17T19:24:03Z | 2,157 | 446 | 573 | 3,176 |
|
||||
| [v2.13.5](https://github.com/laurent22/joplin/releases/tag/v2.13.5) (p) | 2023-11-09T20:24:09Z | 1,467 | 339 | 440 | 2,246 |
|
||||
| [v3.3.13](https://github.com/laurent22/joplin/releases/tag/v3.3.13) | 2025-06-09T20:13:30Z | 125,532 | 13,790 | 20,996 | 160,318 |
|
||||
| [v3.4.1](https://github.com/laurent22/joplin/releases/tag/v3.4.1) (p) | 2025-05-20T09:59:39Z | 3,318 | 888 | 1,043 | 5,249 |
|
||||
| [v3.3.12](https://github.com/laurent22/joplin/releases/tag/v3.3.12) | 2025-05-04T18:12:23Z | 156,635 | 21,490 | 28,133 | 206,258 |
|
||||
| [v3.3.10](https://github.com/laurent22/joplin/releases/tag/v3.3.10) | 2025-05-02T19:46:15Z | 24,855 | 5,075 | 1,808 | 31,738 |
|
||||
| [v3.3.9](https://github.com/laurent22/joplin/releases/tag/v3.3.9) | 2025-05-01T21:02:12Z | 23,518 | 6,090 | 1,174 | 30,782 |
|
||||
| [v3.3.7](https://github.com/laurent22/joplin/releases/tag/v3.3.7) (p) | 2025-04-29T13:47:19Z | 726 | 0 | 162 | 888 |
|
||||
| [v3.3.6](https://github.com/laurent22/joplin/releases/tag/v3.3.6) (p) | 2025-04-24T12:27:20Z | 1,030 | 264 | 242 | 1,536 |
|
||||
| [v3.3.5](https://github.com/laurent22/joplin/releases/tag/v3.3.5) (p) | 2025-04-17T13:40:31Z | 1,360 | 283 | 298 | 1,941 |
|
||||
| [v3.3.4](https://github.com/laurent22/joplin/releases/tag/v3.3.4) (p) | 2025-04-07T20:23:35Z | 1,598 | 390 | 364 | 2,352 |
|
||||
| [v3.3.3](https://github.com/laurent22/joplin/releases/tag/v3.3.3) (p) | 2025-03-16T11:52:33Z | 2,671 | 801 | 963 | 4,435 |
|
||||
| [v3.2.13](https://github.com/laurent22/joplin/releases/tag/v3.2.13) | 2025-02-28T14:38:21Z | 221,882 | 32,849 | 44,434 | 299,165 |
|
||||
| [v3.3.2](https://github.com/laurent22/joplin/releases/tag/v3.3.2) (p) | 2025-02-19T17:34:26Z | 2,334 | 560 | 646 | 3,540 |
|
||||
| [v3.3.1](https://github.com/laurent22/joplin/releases/tag/v3.3.1) (p) | 2025-02-16T17:06:26Z | 831 | 172 | 167 | 1,170 |
|
||||
| [v3.2.12](https://github.com/laurent22/joplin/releases/tag/v3.2.12) | 2025-01-23T23:52:04Z | 152,204 | 25,222 | 27,919 | 205,345 |
|
||||
| [v3.2.11](https://github.com/laurent22/joplin/releases/tag/v3.2.11) | 2025-01-13T17:48:21Z | 66,131 | 14,815 | 6,859 | 87,805 |
|
||||
| [v3.2.10](https://github.com/laurent22/joplin/releases/tag/v3.2.10) (p) | 2025-01-10T10:17:28Z | 1,428 | 162 | 185 | 1,775 |
|
||||
| [v3.2.9](https://github.com/laurent22/joplin/releases/tag/v3.2.9) (p) | 2025-01-09T22:58:42Z | 356 | 87 | 51 | 494 |
|
||||
| [v3.2.7](https://github.com/laurent22/joplin/releases/tag/v3.2.7) (p) | 2025-01-06T16:35:41Z | 863 | 152 | 841 | 1,856 |
|
||||
| [v3.2.6](https://github.com/laurent22/joplin/releases/tag/v3.2.6) (p) | 2024-12-23T21:54:40Z | 1,736 | 331 | 475 | 2,542 |
|
||||
| [v3.2.5](https://github.com/laurent22/joplin/releases/tag/v3.2.5) (p) | 2024-12-18T10:41:13Z | 992 | 195 | 219 | 1,406 |
|
||||
| [v3.2.4](https://github.com/laurent22/joplin/releases/tag/v3.2.4) (p) | 2024-12-12T17:59:52Z | 1,006 | 148 | 233 | 1,387 |
|
||||
| [v3.2.3](https://github.com/laurent22/joplin/releases/tag/v3.2.3) (p) | 2024-11-18T00:09:05Z | 2,690 | 551 | 889 | 4,130 |
|
||||
| [v3.2.1](https://github.com/laurent22/joplin/releases/tag/v3.2.1) (p) | 2024-11-10T16:16:27Z | 1,237 | 230 | 346 | 1,813 |
|
||||
| [v3.1.24](https://github.com/laurent22/joplin/releases/tag/v3.1.24) | 2024-11-09T15:08:29Z | 208,423 | 33,473 | 43,800 | 285,696 |
|
||||
| [v3.1.23](https://github.com/laurent22/joplin/releases/tag/v3.1.23) | 2024-11-07T10:56:45Z | 27,494 | 6,731 | 1,521 | 35,746 |
|
||||
| [v3.1.22](https://github.com/laurent22/joplin/releases/tag/v3.1.22) | 2024-11-05T08:59:32Z | 29,300 | 8,697 | 1,223 | 39,220 |
|
||||
| [v3.1.20](https://github.com/laurent22/joplin/releases/tag/v3.1.20) | 2024-10-22T12:21:32Z | 95,254 | 19,245 | 13,920 | 128,419 |
|
||||
| [v3.1.18](https://github.com/laurent22/joplin/releases/tag/v3.1.18) (p) | 2024-10-11T23:27:10Z | 1,507 | 280 | 588 | 2,375 |
|
||||
| [v3.1.17](https://github.com/laurent22/joplin/releases/tag/v3.1.17) (p) | 2024-09-26T11:57:54Z | 1,699 | 350 | 532 | 2,581 |
|
||||
| [v3.1.15](https://github.com/laurent22/joplin/releases/tag/v3.1.15) (p) | 2024-09-17T09:15:10Z | 1,180 | 224 | 494 | 1,898 |
|
||||
| [v3.1.8](https://github.com/laurent22/joplin/releases/tag/v3.1.8) (p) | 2024-09-08T20:32:44Z | 1,222 | 256 | 334 | 1,812 |
|
||||
| [v3.1.6](https://github.com/laurent22/joplin/releases/tag/v3.1.6) (p) | 2024-09-02T13:19:40Z | 963 | 229 | 426 | 1,618 |
|
||||
| [v3.1.4](https://github.com/laurent22/joplin/releases/tag/v3.1.4) (p) | 2024-08-27T17:46:38Z | 944 | 176 | 248 | 1,368 |
|
||||
| [v3.0.15](https://github.com/laurent22/joplin/releases/tag/v3.0.15) | 2024-08-21T09:19:58Z | 203,585 | 37,807 | 45,139 | 286,531 |
|
||||
| [v3.1.3](https://github.com/laurent22/joplin/releases/tag/v3.1.3) (p) | 2024-08-17T13:08:21Z | 1,235 | 283 | 486 | 2,004 |
|
||||
| [v3.1.2](https://github.com/laurent22/joplin/releases/tag/v3.1.2) (p) | 2024-08-16T09:00:59Z | 446 | 110 | 87 | 643 |
|
||||
| [v3.1.1](https://github.com/laurent22/joplin/releases/tag/v3.1.1) (p) | 2024-08-10T11:36:02Z | 1,082 | 203 | 264 | 1,549 |
|
||||
| [v2.14.23](https://github.com/laurent22/joplin/releases/tag/v2.14.23) | 2024-08-07T11:15:25Z | 10,792 | 2,647 | 624 | 14,063 |
|
||||
| [v3.0.14](https://github.com/laurent22/joplin/releases/tag/v3.0.14) | 2024-07-28T13:55:50Z | 89,640 | 18,571 | 18,826 | 127,037 |
|
||||
| [v3.0.12](https://github.com/laurent22/joplin/releases/tag/v3.0.12) | 2024-07-02T17:11:14Z | 44,593 | 12,832 | 7,336 | 64,761 |
|
||||
| [v3.0.11](https://github.com/laurent22/joplin/releases/tag/v3.0.11) (p) | 2024-06-29T10:20:02Z | 841 | 164 | 265 | 1,270 |
|
||||
| [v3.0.10](https://github.com/laurent22/joplin/releases/tag/v3.0.10) (p) | 2024-06-19T15:24:07Z | 1,618 | 291 | 559 | 2,468 |
|
||||
| [v3.0.9](https://github.com/laurent22/joplin/releases/tag/v3.0.9) (p) | 2024-06-12T19:07:50Z | 1,267 | 259 | 378 | 1,904 |
|
||||
| [v3.0.8](https://github.com/laurent22/joplin/releases/tag/v3.0.8) (p) | 2024-05-22T14:20:45Z | 2,660 | 0 | 912 | 3,572 |
|
||||
| [v2.14.22](https://github.com/laurent22/joplin/releases/tag/v2.14.22) | 2024-05-22T19:19:02Z | 143,224 | 30,785 | 25,477 | 199,486 |
|
||||
| [v3.0.6](https://github.com/laurent22/joplin/releases/tag/v3.0.6) (p) | 2024-04-27T13:16:04Z | 3,004 | 687 | 861 | 4,552 |
|
||||
| [v3.0.3](https://github.com/laurent22/joplin/releases/tag/v3.0.3) (p) | 2024-04-18T15:41:38Z | 1,483 | 320 | 331 | 2,134 |
|
||||
| [v3.0.2](https://github.com/laurent22/joplin/releases/tag/v3.0.2) (p) | 2024-03-21T18:18:49Z | 2,870 | 722 | 1,101 | 4,693 |
|
||||
| [v2.14.20](https://github.com/laurent22/joplin/releases/tag/v2.14.20) | 2024-03-18T17:05:17Z | 193,231 | 39,539 | 38,243 | 271,013 |
|
||||
| [v2.14.19](https://github.com/laurent22/joplin/releases/tag/v2.14.19) | 2024-03-08T10:45:16Z | 65,150 | 18,470 | 8,552 | 92,172 |
|
||||
| [v2.14.17](https://github.com/laurent22/joplin/releases/tag/v2.14.17) | 2024-03-01T18:10:26Z | 63,726 | 18,640 | 7,618 | 89,984 |
|
||||
| [v2.14.16](https://github.com/laurent22/joplin/releases/tag/v2.14.16) (p) | 2024-02-22T22:49:10Z | 1,349 | 284 | 378 | 2,011 |
|
||||
| [v2.14.15](https://github.com/laurent22/joplin/releases/tag/v2.14.15) (p) | 2024-02-19T11:24:57Z | 853 | 180 | 191 | 1,224 |
|
||||
| [v2.14.14](https://github.com/laurent22/joplin/releases/tag/v2.14.14) (p) | 2024-02-10T16:03:08Z | 1,272 | 243 | 379 | 1,894 |
|
||||
| [v2.14.13](https://github.com/laurent22/joplin/releases/tag/v2.14.13) (p) | 2024-02-09T16:31:54Z | 412 | 82 | 80 | 574 |
|
||||
| [v2.14.12](https://github.com/laurent22/joplin/releases/tag/v2.14.12) (p) | 2024-02-03T12:11:47Z | 980 | 218 | 247 | 1,445 |
|
||||
| [v2.14.11](https://github.com/laurent22/joplin/releases/tag/v2.14.11) (p) | 2024-01-26T11:53:05Z | 1,266 | 259 | 464 | 1,989 |
|
||||
| [v2.14.10](https://github.com/laurent22/joplin/releases/tag/v2.14.10) (p) | 2024-01-18T22:45:04Z | 2,087 | 269 | 371 | 2,727 |
|
||||
| [v2.13.15](https://github.com/laurent22/joplin/releases/tag/v2.13.15) | 2024-01-15T13:01:19Z | 150,122 | 34,151 | 29,262 | 213,535 |
|
||||
| [v2.13.14](https://github.com/laurent22/joplin/releases/tag/v2.13.14) | 2024-01-13T19:11:04Z | 20,115 | 7,827 | 2,363 | 30,305 |
|
||||
| [v2.14.9](https://github.com/laurent22/joplin/releases/tag/v2.14.9) (p) | 2024-01-11T22:17:59Z | 1,057 | 0 | 241 | 1,298 |
|
||||
| [v2.14.8](https://github.com/laurent22/joplin/releases/tag/v2.14.8) (p) | 2024-01-09T22:57:07Z | 757 | 237 | 174 | 1,168 |
|
||||
| [v2.14.7](https://github.com/laurent22/joplin/releases/tag/v2.14.7) (p) | 2024-01-08T11:51:49Z | 622 | 128 | 174 | 924 |
|
||||
| [v2.14.6](https://github.com/laurent22/joplin/releases/tag/v2.14.6) (p) | 2024-01-06T16:38:32Z | 728 | 150 | 151 | 1,029 |
|
||||
| [v2.13.13](https://github.com/laurent22/joplin/releases/tag/v2.13.13) | 2024-01-06T13:33:11Z | 54,202 | 15,934 | 6,346 | 76,482 |
|
||||
| [v2.13.12](https://github.com/laurent22/joplin/releases/tag/v2.13.12) | 2023-12-31T16:08:02Z | 45,490 | 14,298 | 5,084 | 64,872 |
|
||||
| [v2.13.11](https://github.com/laurent22/joplin/releases/tag/v2.13.11) | 2023-12-24T12:58:53Z | 46,271 | 13,263 | 5,974 | 65,508 |
|
||||
| [v2.13.10](https://github.com/laurent22/joplin/releases/tag/v2.13.10) | 2023-12-22T10:11:08Z | 19,973 | 8,697 | 1,477 | 30,147 |
|
||||
| [v2.13.9](https://github.com/laurent22/joplin/releases/tag/v2.13.9) | 2023-12-09T17:18:58Z | 68,224 | 21,924 | 8,611 | 98,759 |
|
||||
| [v2.13.8](https://github.com/laurent22/joplin/releases/tag/v2.13.8) | 2023-12-03T12:07:08Z | 50,889 | 17,904 | 5,216 | 74,009 |
|
||||
| [v2.13.6](https://github.com/laurent22/joplin/releases/tag/v2.13.6) (p) | 2023-11-17T19:24:03Z | 2,159 | 446 | 576 | 3,181 |
|
||||
| [v2.13.5](https://github.com/laurent22/joplin/releases/tag/v2.13.5) (p) | 2023-11-09T20:24:09Z | 1,469 | 340 | 440 | 2,249 |
|
||||
| [v2.13.4](https://github.com/laurent22/joplin/releases/tag/v2.13.4) (p) | 2023-10-31T00:01:00Z | 1,540 | 372 | 485 | 2,397 |
|
||||
| [v2.13.3](https://github.com/laurent22/joplin/releases/tag/v2.13.3) (p) | 2023-10-24T09:25:33Z | 1,303 | 281 | 301 | 1,885 |
|
||||
| [v2.12.19](https://github.com/laurent22/joplin/releases/tag/v2.12.19) | 2023-10-21T09:39:18Z | 166,838 | 43,656 | 27,867 | 238,361 |
|
||||
| [v2.13.3](https://github.com/laurent22/joplin/releases/tag/v2.13.3) (p) | 2023-10-24T09:25:33Z | 1,303 | 281 | 302 | 1,886 |
|
||||
| [v2.12.19](https://github.com/laurent22/joplin/releases/tag/v2.12.19) | 2023-10-21T09:39:18Z | 167,229 | 43,659 | 27,880 | 238,768 |
|
||||
| [v2.13.2](https://github.com/laurent22/joplin/releases/tag/v2.13.2) (p) | 2023-10-06T17:00:07Z | 2,034 | 503 | 704 | 3,241 |
|
||||
| [v2.12.18](https://github.com/laurent22/joplin/releases/tag/v2.12.18) | 2023-09-22T14:37:24Z | 109,047 | 36,516 | 18,706 | 164,269 |
|
||||
| [v2.12.17](https://github.com/laurent22/joplin/releases/tag/v2.12.17) | 2023-09-14T21:54:52Z | 47,458 | 21,032 | 6,632 | 75,122 |
|
||||
| [v2.12.18](https://github.com/laurent22/joplin/releases/tag/v2.12.18) | 2023-09-22T14:37:24Z | 109,356 | 36,520 | 18,709 | 164,585 |
|
||||
| [v2.12.17](https://github.com/laurent22/joplin/releases/tag/v2.12.17) | 2023-09-14T21:54:52Z | 47,687 | 21,032 | 6,633 | 75,352 |
|
||||
| [v2.13.1](https://github.com/laurent22/joplin/releases/tag/v2.13.1) (p) | 2023-09-13T09:31:50Z | 1,390 | 427 | 667 | 2,484 |
|
||||
| [v2.12.16](https://github.com/laurent22/joplin/releases/tag/v2.12.16) | 2023-09-11T22:33:37Z | 28,383 | 14,668 | 2,452 | 45,503 |
|
||||
| [v2.12.15](https://github.com/laurent22/joplin/releases/tag/v2.12.15) | 2023-08-27T11:35:39Z | 64,640 | 28,088 | 8,402 | 101,130 |
|
||||
| [v2.12.12](https://github.com/laurent22/joplin/releases/tag/v2.12.12) (p) | 2023-08-19T22:44:56Z | 2,888 | 387 | 428 | 3,703 |
|
||||
| [v2.12.10](https://github.com/laurent22/joplin/releases/tag/v2.12.10) (p) | 2023-07-30T18:25:58Z | 7,520 | 3,817 | 914 | 12,251 |
|
||||
| [v2.12.9](https://github.com/laurent22/joplin/releases/tag/v2.12.9) (p) | 2023-07-25T16:06:08Z | 2,352 | 369 | 321 | 3,042 |
|
||||
| [v2.12.7](https://github.com/laurent22/joplin/releases/tag/v2.12.7) (p) | 2023-07-13T12:55:31Z | 2,165 | 664 | 591 | 3,420 |
|
||||
| [v2.12.5](https://github.com/laurent22/joplin/releases/tag/v2.12.5) (p) | 2023-07-12T15:03:46Z | 1,905 | 164 | 151 | 2,220 |
|
||||
| [v2.12.4](https://github.com/laurent22/joplin/releases/tag/v2.12.4) (p) | 2023-07-07T22:36:53Z | 1,064 | 437 | 217 | 1,718 |
|
||||
| [v2.12.3](https://github.com/laurent22/joplin/releases/tag/v2.12.3) (p) | 2023-07-07T10:16:55Z | 394 | 192 | 95 | 681 |
|
||||
| [v2.11.11](https://github.com/laurent22/joplin/releases/tag/v2.11.11) | 2023-06-23T15:16:37Z | 189,480 | 67,230 | 38,865 | 295,575 |
|
||||
| [v2.11.9](https://github.com/laurent22/joplin/releases/tag/v2.11.9) (p) | 2023-06-06T16:23:27Z | 2,303 | 574 | 746 | 3,623 |
|
||||
| [v2.11.6](https://github.com/laurent22/joplin/releases/tag/v2.11.6) (p) | 2023-05-31T20:13:08Z | 1,162 | 435 | 345 | 1,942 |
|
||||
| [v2.11.5](https://github.com/laurent22/joplin/releases/tag/v2.11.5) (p) | 2023-05-28T00:41:40Z | 1,029 | 307 | 282 | 1,618 |
|
||||
| [v2.10.19](https://github.com/laurent22/joplin/releases/tag/v2.10.19) | 2023-05-17T12:25:41Z | 124,680 | 48,335 | 22,477 | 195,492 |
|
||||
| [v2.11.4](https://github.com/laurent22/joplin/releases/tag/v2.11.4) (p) | 2023-05-16T10:02:21Z | 1,083 | 468 | 415 | 1,966 |
|
||||
| [v2.11.3](https://github.com/laurent22/joplin/releases/tag/v2.11.3) (p) | 2023-05-16T09:09:57Z | 138 | 40 | 40 | 218 |
|
||||
| [v2.10.18](https://github.com/laurent22/joplin/releases/tag/v2.10.18) | 2023-05-09T13:27:43Z | 56,526 | 24,257 | 6,791 | 87,574 |
|
||||
| [v2.10.17](https://github.com/laurent22/joplin/releases/tag/v2.10.17) | 2023-05-08T17:27:28Z | 19,229 | 11,508 | 887 | 31,624 |
|
||||
| [v2.10.16](https://github.com/laurent22/joplin/releases/tag/v2.10.16) | 2023-04-27T09:27:45Z | 9,488 | 4,257 | 780 | 14,525 |
|
||||
| [v2.10.15](https://github.com/laurent22/joplin/releases/tag/v2.10.15) (p) | 2023-04-26T22:02:16Z | 374 | 141 | 59 | 574 |
|
||||
| [v2.10.13](https://github.com/laurent22/joplin/releases/tag/v2.10.13) (p) | 2023-04-03T16:53:46Z | 4,659 | 829 | 1,078 | 6,566 |
|
||||
| [v2.10.12](https://github.com/laurent22/joplin/releases/tag/v2.10.12) (p) | 2023-03-23T12:17:13Z | 3,625 | 519 | 604 | 4,748 |
|
||||
| [v2.10.11](https://github.com/laurent22/joplin/releases/tag/v2.10.11) (p) | 2023-03-17T10:54:02Z | 2,995 | 383 | 393 | 3,771 |
|
||||
| [v2.10.10](https://github.com/laurent22/joplin/releases/tag/v2.10.10) (p) | 2023-03-13T23:16:37Z | 2,501 | 284 | 255 | 3,040 |
|
||||
| [v2.10.9](https://github.com/laurent22/joplin/releases/tag/v2.10.9) (p) | 2023-03-12T16:16:45Z | 2,035 | 214 | 297 | 2,546 |
|
||||
| [v2.10.8](https://github.com/laurent22/joplin/releases/tag/v2.10.8) (p) | 2023-02-26T12:53:55Z | 4,517 | 573 | 872 | 5,962 |
|
||||
| [v2.10.7](https://github.com/laurent22/joplin/releases/tag/v2.10.7) (p) | 2023-02-24T10:56:20Z | 2,187 | 191 | 280 | 2,658 |
|
||||
| [v2.10.6](https://github.com/laurent22/joplin/releases/tag/v2.10.6) (p) | 2023-02-20T14:00:05Z | 2,978 | 342 | 290 | 3,610 |
|
||||
| [v2.12.16](https://github.com/laurent22/joplin/releases/tag/v2.12.16) | 2023-09-11T22:33:37Z | 28,478 | 14,668 | 2,452 | 45,598 |
|
||||
| [v2.12.15](https://github.com/laurent22/joplin/releases/tag/v2.12.15) | 2023-08-27T11:35:39Z | 64,835 | 28,100 | 8,417 | 101,352 |
|
||||
| [v2.12.12](https://github.com/laurent22/joplin/releases/tag/v2.12.12) (p) | 2023-08-19T22:44:56Z | 3,005 | 387 | 429 | 3,821 |
|
||||
| [v2.12.10](https://github.com/laurent22/joplin/releases/tag/v2.12.10) (p) | 2023-07-30T18:25:58Z | 7,631 | 3,817 | 914 | 12,362 |
|
||||
| [v2.12.9](https://github.com/laurent22/joplin/releases/tag/v2.12.9) (p) | 2023-07-25T16:06:08Z | 2,457 | 369 | 322 | 3,148 |
|
||||
| [v2.12.7](https://github.com/laurent22/joplin/releases/tag/v2.12.7) (p) | 2023-07-13T12:55:31Z | 2,172 | 664 | 593 | 3,429 |
|
||||
| [v2.12.5](https://github.com/laurent22/joplin/releases/tag/v2.12.5) (p) | 2023-07-12T15:03:46Z | 1,931 | 165 | 156 | 2,252 |
|
||||
| [v2.12.4](https://github.com/laurent22/joplin/releases/tag/v2.12.4) (p) | 2023-07-07T22:36:53Z | 1,071 | 448 | 220 | 1,739 |
|
||||
| [v2.12.3](https://github.com/laurent22/joplin/releases/tag/v2.12.3) (p) | 2023-07-07T10:16:55Z | 401 | 202 | 99 | 702 |
|
||||
| [v2.11.11](https://github.com/laurent22/joplin/releases/tag/v2.11.11) | 2023-06-23T15:16:37Z | 189,759 | 67,237 | 38,874 | 295,870 |
|
||||
| [v2.11.9](https://github.com/laurent22/joplin/releases/tag/v2.11.9) (p) | 2023-06-06T16:23:27Z | 2,310 | 578 | 749 | 3,637 |
|
||||
| [v2.11.6](https://github.com/laurent22/joplin/releases/tag/v2.11.6) (p) | 2023-05-31T20:13:08Z | 1,173 | 439 | 348 | 1,960 |
|
||||
| [v2.11.5](https://github.com/laurent22/joplin/releases/tag/v2.11.5) (p) | 2023-05-28T00:41:40Z | 1,035 | 311 | 286 | 1,632 |
|
||||
| [v2.10.19](https://github.com/laurent22/joplin/releases/tag/v2.10.19) | 2023-05-17T12:25:41Z | 124,926 | 48,338 | 22,482 | 195,746 |
|
||||
| [v2.11.4](https://github.com/laurent22/joplin/releases/tag/v2.11.4) (p) | 2023-05-16T10:02:21Z | 1,089 | 472 | 419 | 1,980 |
|
||||
| [v2.11.3](https://github.com/laurent22/joplin/releases/tag/v2.11.3) (p) | 2023-05-16T09:09:57Z | 146 | 44 | 43 | 233 |
|
||||
| [v2.10.18](https://github.com/laurent22/joplin/releases/tag/v2.10.18) | 2023-05-09T13:27:43Z | 56,717 | 24,261 | 6,801 | 87,779 |
|
||||
| [v2.10.17](https://github.com/laurent22/joplin/releases/tag/v2.10.17) | 2023-05-08T17:27:28Z | 19,372 | 11,512 | 892 | 31,776 |
|
||||
| [v2.10.16](https://github.com/laurent22/joplin/releases/tag/v2.10.16) | 2023-04-27T09:27:45Z | 9,680 | 4,260 | 784 | 14,724 |
|
||||
| [v2.10.15](https://github.com/laurent22/joplin/releases/tag/v2.10.15) (p) | 2023-04-26T22:02:16Z | 381 | 145 | 62 | 588 |
|
||||
| [v2.10.13](https://github.com/laurent22/joplin/releases/tag/v2.10.13) (p) | 2023-04-03T16:53:46Z | 4,764 | 829 | 1,078 | 6,671 |
|
||||
| [v2.10.12](https://github.com/laurent22/joplin/releases/tag/v2.10.12) (p) | 2023-03-23T12:17:13Z | 3,734 | 519 | 604 | 4,857 |
|
||||
| [v2.10.11](https://github.com/laurent22/joplin/releases/tag/v2.10.11) (p) | 2023-03-17T10:54:02Z | 3,103 | 383 | 393 | 3,879 |
|
||||
| [v2.10.10](https://github.com/laurent22/joplin/releases/tag/v2.10.10) (p) | 2023-03-13T23:16:37Z | 2,608 | 284 | 256 | 3,148 |
|
||||
| [v2.10.9](https://github.com/laurent22/joplin/releases/tag/v2.10.9) (p) | 2023-03-12T16:16:45Z | 2,146 | 214 | 297 | 2,657 |
|
||||
| [v2.10.8](https://github.com/laurent22/joplin/releases/tag/v2.10.8) (p) | 2023-02-26T12:53:55Z | 4,628 | 573 | 872 | 6,073 |
|
||||
| [v2.10.7](https://github.com/laurent22/joplin/releases/tag/v2.10.7) (p) | 2023-02-24T10:56:20Z | 2,295 | 191 | 280 | 2,766 |
|
||||
| [v2.10.6](https://github.com/laurent22/joplin/releases/tag/v2.10.6) (p) | 2023-02-20T14:00:05Z | 3,084 | 342 | 290 | 3,716 |
|
||||
| [v2.10.5](https://github.com/laurent22/joplin/releases/tag/v2.10.5) | 2023-01-16T15:00:53Z | 367 | 103 | 309 | 779 |
|
||||
| [v2.10.4](https://github.com/laurent22/joplin/releases/tag/v2.10.4) (p) | 2023-01-05T13:09:20Z | 8,201 | 1,303 | 1,812 | 11,316 |
|
||||
| [v2.10.3](https://github.com/laurent22/joplin/releases/tag/v2.10.3) (p) | 2022-12-31T15:53:23Z | 2,763 | 314 | 417 | 3,494 |
|
||||
| [v2.10.2](https://github.com/laurent22/joplin/releases/tag/v2.10.2) (p) | 2022-12-18T18:05:08Z | 4,191 | 591 | 639 | 5,421 |
|
||||
| [v2.9.17](https://github.com/laurent22/joplin/releases/tag/v2.9.17) | 2022-11-15T10:28:37Z | 335,646 | 108,788 | 83,358 | 527,792 |
|
||||
| [v2.9.12](https://github.com/laurent22/joplin/releases/tag/v2.9.12) (p) | 2022-11-01T17:06:05Z | 11,379 | 612 | 545 | 12,536 |
|
||||
| [v2.9.11](https://github.com/laurent22/joplin/releases/tag/v2.9.11) (p) | 2022-10-23T16:09:58Z | 3,528 | 531 | 762 | 4,821 |
|
||||
| [v2.9.4](https://github.com/laurent22/joplin/releases/tag/v2.9.4) (p) | 2022-08-18T16:52:26Z | 8,578 | 1,868 | 2,200 | 12,646 |
|
||||
| [v2.10.4](https://github.com/laurent22/joplin/releases/tag/v2.10.4) (p) | 2023-01-05T13:09:20Z | 8,305 | 1,303 | 1,812 | 11,420 |
|
||||
| [v2.10.3](https://github.com/laurent22/joplin/releases/tag/v2.10.3) (p) | 2022-12-31T15:53:23Z | 2,866 | 314 | 417 | 3,597 |
|
||||
| [v2.10.2](https://github.com/laurent22/joplin/releases/tag/v2.10.2) (p) | 2022-12-18T18:05:08Z | 4,296 | 591 | 639 | 5,526 |
|
||||
| [v2.9.17](https://github.com/laurent22/joplin/releases/tag/v2.9.17) | 2022-11-15T10:28:37Z | 335,842 | 108,794 | 83,364 | 528,000 |
|
||||
| [v2.9.12](https://github.com/laurent22/joplin/releases/tag/v2.9.12) (p) | 2022-11-01T17:06:05Z | 11,482 | 612 | 546 | 12,640 |
|
||||
| [v2.9.11](https://github.com/laurent22/joplin/releases/tag/v2.9.11) (p) | 2022-10-23T16:09:58Z | 3,634 | 531 | 762 | 4,927 |
|
||||
| [v2.9.4](https://github.com/laurent22/joplin/releases/tag/v2.9.4) (p) | 2022-08-18T16:52:26Z | 8,687 | 1,868 | 2,201 | 12,756 |
|
||||
| [v2.9.3](https://github.com/laurent22/joplin/releases/tag/v2.9.3) (p) | 2022-08-18T13:11:09Z | 363 | 92 | 275 | 730 |
|
||||
| [v2.9.2](https://github.com/laurent22/joplin/releases/tag/v2.9.2) (p) | 2022-08-12T18:12:12Z | 1,533 | 447 | 0 | 1,980 |
|
||||
| [v2.9.1](https://github.com/laurent22/joplin/releases/tag/v2.9.1) (p) | 2022-07-11T09:59:32Z | 8,011 | 1,343 | 1,412 | 10,766 |
|
||||
| [v2.8.8](https://github.com/laurent22/joplin/releases/tag/v2.8.8) | 2022-05-17T14:48:06Z | 351,763 | 114,379 | 113,587 | 579,729 |
|
||||
| [v2.8.7](https://github.com/laurent22/joplin/releases/tag/v2.8.7) (p) | 2022-05-06T11:34:27Z | 4,513 | 366 | 429 | 5,308 |
|
||||
| [v2.8.6](https://github.com/laurent22/joplin/releases/tag/v2.8.6) (p) | 2022-05-03T10:08:25Z | 4,143 | 403 | 332 | 4,878 |
|
||||
| [v2.8.5](https://github.com/laurent22/joplin/releases/tag/v2.8.5) (p) | 2022-04-27T13:51:50Z | 4,236 | 370 | 357 | 4,963 |
|
||||
| [v2.8.4](https://github.com/laurent22/joplin/releases/tag/v2.8.4) (p) | 2022-04-19T18:00:09Z | 4,718 | 590 | 334 | 5,642 |
|
||||
| [v2.8.2](https://github.com/laurent22/joplin/releases/tag/v2.8.2) (p) | 2022-04-14T11:35:45Z | 4,136 | 280 | 282 | 4,698 |
|
||||
| [v2.7.15](https://github.com/laurent22/joplin/releases/tag/v2.7.15) | 2022-03-17T13:03:23Z | 156,446 | 56,783 | 51,285 | 264,514 |
|
||||
| [v2.7.14](https://github.com/laurent22/joplin/releases/tag/v2.7.14) | 2022-02-27T11:30:53Z | 34,701 | 16,783 | 4,811 | 56,295 |
|
||||
| [v2.7.13](https://github.com/laurent22/joplin/releases/tag/v2.7.13) | 2022-02-24T17:42:12Z | 55,125 | 25,726 | 11,724 | 92,575 |
|
||||
| [v2.7.12](https://github.com/laurent22/joplin/releases/tag/v2.7.12) (p) | 2022-02-14T15:06:14Z | 4,907 | 465 | 504 | 5,876 |
|
||||
| [v2.7.11](https://github.com/laurent22/joplin/releases/tag/v2.7.11) (p) | 2022-02-12T13:00:02Z | 4,052 | 195 | 174 | 4,421 |
|
||||
| [v2.7.10](https://github.com/laurent22/joplin/releases/tag/v2.7.10) (p) | 2022-02-11T18:19:09Z | 3,563 | 126 | 95 | 3,784 |
|
||||
| [v2.7.8](https://github.com/laurent22/joplin/releases/tag/v2.7.8) (p) | 2022-01-19T09:35:27Z | 5,786 | 771 | 830 | 7,387 |
|
||||
| [v2.7.7](https://github.com/laurent22/joplin/releases/tag/v2.7.7) (p) | 2022-01-18T14:05:07Z | 3,881 | 157 | 144 | 4,182 |
|
||||
| [v2.7.6](https://github.com/laurent22/joplin/releases/tag/v2.7.6) (p) | 2022-01-17T17:08:28Z | 3,809 | 184 | 121 | 4,114 |
|
||||
| [v2.6.10](https://github.com/laurent22/joplin/releases/tag/v2.6.10) | 2021-12-19T11:31:16Z | 136,462 | 51,206 | 49,319 | 236,987 |
|
||||
| [v2.6.9](https://github.com/laurent22/joplin/releases/tag/v2.6.9) | 2021-12-17T11:57:32Z | 19,441 | 9,500 | 3,194 | 32,135 |
|
||||
| [v2.6.7](https://github.com/laurent22/joplin/releases/tag/v2.6.7) (p) | 2021-12-16T10:47:23Z | 4,025 | 180 | 112 | 4,317 |
|
||||
| [v2.6.6](https://github.com/laurent22/joplin/releases/tag/v2.6.6) (p) | 2021-12-13T12:31:43Z | 4,020 | 257 | 174 | 4,451 |
|
||||
| [v2.6.5](https://github.com/laurent22/joplin/releases/tag/v2.6.5) (p) | 2021-12-13T10:07:04Z | 3,355 | 51 | 36 | 3,442 |
|
||||
| [v2.6.4](https://github.com/laurent22/joplin/releases/tag/v2.6.4) (p) | 2021-12-09T19:53:43Z | 4,091 | 291 | 207 | 4,589 |
|
||||
| [v2.6.2](https://github.com/laurent22/joplin/releases/tag/v2.6.2) (p) | 2021-11-18T12:19:12Z | 5,892 | 793 | 702 | 7,387 |
|
||||
| [v2.5.12](https://github.com/laurent22/joplin/releases/tag/v2.5.12) | 2021-11-08T11:07:11Z | 83,180 | 32,507 | 25,236 | 140,923 |
|
||||
| [v2.5.10](https://github.com/laurent22/joplin/releases/tag/v2.5.10) | 2021-11-01T08:22:42Z | 47,339 | 19,050 | 10,098 | 76,487 |
|
||||
| [v2.5.8](https://github.com/laurent22/joplin/releases/tag/v2.5.8) | 2021-10-31T11:38:03Z | 16,061 | 6,581 | 2,329 | 24,971 |
|
||||
| [v2.5.7](https://github.com/laurent22/joplin/releases/tag/v2.5.7) (p) | 2021-10-29T14:47:33Z | 3,624 | 203 | 164 | 3,991 |
|
||||
| [v2.5.6](https://github.com/laurent22/joplin/releases/tag/v2.5.6) (p) | 2021-10-28T22:03:09Z | 3,676 | 178 | 108 | 3,962 |
|
||||
| [v2.5.4](https://github.com/laurent22/joplin/releases/tag/v2.5.4) (p) | 2021-10-19T10:10:54Z | 5,168 | 568 | 580 | 6,316 |
|
||||
| [v2.4.12](https://github.com/laurent22/joplin/releases/tag/v2.4.12) | 2021-10-13T17:24:34Z | 47,788 | 19,984 | 9,789 | 77,561 |
|
||||
| [v2.5.1](https://github.com/laurent22/joplin/releases/tag/v2.5.1) (p) | 2021-10-02T09:51:58Z | 6,229 | 907 | 947 | 8,083 |
|
||||
| [v2.4.9](https://github.com/laurent22/joplin/releases/tag/v2.4.9) | 2021-09-29T19:08:58Z | 59,488 | 23,262 | 15,914 | 98,664 |
|
||||
| [v2.4.8](https://github.com/laurent22/joplin/releases/tag/v2.4.8) (p) | 2021-09-22T19:01:46Z | 10,020 | 1,774 | 535 | 12,329 |
|
||||
| [v2.4.7](https://github.com/laurent22/joplin/releases/tag/v2.4.7) (p) | 2021-09-19T12:53:22Z | 4,149 | 259 | 210 | 4,618 |
|
||||
| [v2.4.6](https://github.com/laurent22/joplin/releases/tag/v2.4.6) (p) | 2021-09-09T18:57:17Z | 4,913 | 460 | 520 | 5,893 |
|
||||
| [v2.4.5](https://github.com/laurent22/joplin/releases/tag/v2.4.5) (p) | 2021-09-06T18:03:28Z | 4,157 | 275 | 225 | 4,657 |
|
||||
| [v2.4.4](https://github.com/laurent22/joplin/releases/tag/v2.4.4) (p) | 2021-08-30T16:02:51Z | 4,371 | 380 | 373 | 5,124 |
|
||||
| [v2.4.3](https://github.com/laurent22/joplin/releases/tag/v2.4.3) (p) | 2021-08-28T15:27:32Z | 3,858 | 207 | 180 | 4,245 |
|
||||
| [v2.4.2](https://github.com/laurent22/joplin/releases/tag/v2.4.2) (p) | 2021-08-27T17:13:21Z | 3,634 | 151 | 91 | 3,876 |
|
||||
| [v2.4.1](https://github.com/laurent22/joplin/releases/tag/v2.4.1) (p) | 2021-08-21T11:52:30Z | 4,472 | 372 | 337 | 5,181 |
|
||||
| [v2.3.5](https://github.com/laurent22/joplin/releases/tag/v2.3.5) | 2021-08-17T06:43:30Z | 85,517 | 31,439 | 33,144 | 150,100 |
|
||||
| [v2.3.3](https://github.com/laurent22/joplin/releases/tag/v2.3.3) | 2021-08-14T09:19:40Z | 18,702 | 6,886 | 4,062 | 29,650 |
|
||||
| [v2.2.7](https://github.com/laurent22/joplin/releases/tag/v2.2.7) | 2021-08-11T11:03:26Z | 18,568 | 7,524 | 2,607 | 28,699 |
|
||||
| [v2.2.6](https://github.com/laurent22/joplin/releases/tag/v2.2.6) (p) | 2021-08-09T19:29:20Z | 10,896 | 4,619 | 958 | 16,473 |
|
||||
| [v2.2.5](https://github.com/laurent22/joplin/releases/tag/v2.2.5) (p) | 2021-08-07T10:35:24Z | 4,206 | 278 | 208 | 4,692 |
|
||||
| [v2.2.4](https://github.com/laurent22/joplin/releases/tag/v2.2.4) (p) | 2021-08-05T16:42:48Z | 3,862 | 208 | 133 | 4,203 |
|
||||
| [v2.2.2](https://github.com/laurent22/joplin/releases/tag/v2.2.2) (p) | 2021-07-19T10:28:35Z | 5,785 | 737 | 648 | 7,170 |
|
||||
| [v2.1.9](https://github.com/laurent22/joplin/releases/tag/v2.1.9) | 2021-07-19T10:28:43Z | 50,195 | 18,962 | 16,821 | 85,978 |
|
||||
| [v2.2.1](https://github.com/laurent22/joplin/releases/tag/v2.2.1) (p) | 2021-07-09T17:38:25Z | 5,409 | 417 | 394 | 6,220 |
|
||||
| [v2.1.8](https://github.com/laurent22/joplin/releases/tag/v2.1.8) | 2021-07-03T08:25:16Z | 33,677 | 12,206 | 12,736 | 58,619 |
|
||||
| [v2.1.7](https://github.com/laurent22/joplin/releases/tag/v2.1.7) | 2021-06-26T19:48:55Z | 16,985 | 6,410 | 3,638 | 27,033 |
|
||||
| [v2.1.5](https://github.com/laurent22/joplin/releases/tag/v2.1.5) (p) | 2021-06-23T15:08:52Z | 4,306 | 254 | 202 | 4,762 |
|
||||
| [v2.1.3](https://github.com/laurent22/joplin/releases/tag/v2.1.3) (p) | 2021-06-19T16:32:51Z | 4,373 | 312 | 217 | 4,902 |
|
||||
| [v2.0.11](https://github.com/laurent22/joplin/releases/tag/v2.0.11) | 2021-06-16T17:55:49Z | 26,740 | 9,281 | 9,905 | 45,926 |
|
||||
| [v2.0.10](https://github.com/laurent22/joplin/releases/tag/v2.0.10) | 2021-06-16T07:58:29Z | 5,918 | 946 | 399 | 7,263 |
|
||||
| [v2.0.9](https://github.com/laurent22/joplin/releases/tag/v2.0.9) (p) | 2021-06-12T09:30:30Z | 4,360 | 307 | 899 | 5,566 |
|
||||
| [v2.0.8](https://github.com/laurent22/joplin/releases/tag/v2.0.8) (p) | 2021-06-10T16:15:08Z | 3,797 | 246 | 599 | 4,642 |
|
||||
| [v2.9.1](https://github.com/laurent22/joplin/releases/tag/v2.9.1) (p) | 2022-07-11T09:59:32Z | 8,123 | 1,343 | 1,413 | 10,879 |
|
||||
| [v2.8.8](https://github.com/laurent22/joplin/releases/tag/v2.8.8) | 2022-05-17T14:48:06Z | 351,940 | 114,392 | 113,590 | 579,922 |
|
||||
| [v2.8.7](https://github.com/laurent22/joplin/releases/tag/v2.8.7) (p) | 2022-05-06T11:34:27Z | 4,623 | 366 | 430 | 5,419 |
|
||||
| [v2.8.6](https://github.com/laurent22/joplin/releases/tag/v2.8.6) (p) | 2022-05-03T10:08:25Z | 4,254 | 403 | 333 | 4,990 |
|
||||
| [v2.8.5](https://github.com/laurent22/joplin/releases/tag/v2.8.5) (p) | 2022-04-27T13:51:50Z | 4,345 | 370 | 357 | 5,072 |
|
||||
| [v2.8.4](https://github.com/laurent22/joplin/releases/tag/v2.8.4) (p) | 2022-04-19T18:00:09Z | 4,823 | 590 | 334 | 5,747 |
|
||||
| [v2.8.2](https://github.com/laurent22/joplin/releases/tag/v2.8.2) (p) | 2022-04-14T11:35:45Z | 4,247 | 280 | 282 | 4,809 |
|
||||
| [v2.7.15](https://github.com/laurent22/joplin/releases/tag/v2.7.15) | 2022-03-17T13:03:23Z | 156,564 | 56,784 | 51,287 | 264,635 |
|
||||
| [v2.7.14](https://github.com/laurent22/joplin/releases/tag/v2.7.14) | 2022-02-27T11:30:53Z | 34,807 | 16,783 | 4,813 | 56,403 |
|
||||
| [v2.7.13](https://github.com/laurent22/joplin/releases/tag/v2.7.13) | 2022-02-24T17:42:12Z | 55,244 | 25,726 | 11,724 | 92,694 |
|
||||
| [v2.7.12](https://github.com/laurent22/joplin/releases/tag/v2.7.12) (p) | 2022-02-14T15:06:14Z | 5,011 | 465 | 505 | 5,981 |
|
||||
| [v2.7.11](https://github.com/laurent22/joplin/releases/tag/v2.7.11) (p) | 2022-02-12T13:00:02Z | 4,167 | 195 | 174 | 4,536 |
|
||||
| [v2.7.10](https://github.com/laurent22/joplin/releases/tag/v2.7.10) (p) | 2022-02-11T18:19:09Z | 3,678 | 126 | 95 | 3,899 |
|
||||
| [v2.7.8](https://github.com/laurent22/joplin/releases/tag/v2.7.8) (p) | 2022-01-19T09:35:27Z | 5,899 | 771 | 830 | 7,500 |
|
||||
| [v2.7.7](https://github.com/laurent22/joplin/releases/tag/v2.7.7) (p) | 2022-01-18T14:05:07Z | 3,989 | 157 | 144 | 4,290 |
|
||||
| [v2.7.6](https://github.com/laurent22/joplin/releases/tag/v2.7.6) (p) | 2022-01-17T17:08:28Z | 3,917 | 184 | 121 | 4,222 |
|
||||
| [v2.6.10](https://github.com/laurent22/joplin/releases/tag/v2.6.10) | 2021-12-19T11:31:16Z | 136,611 | 51,208 | 49,320 | 237,139 |
|
||||
| [v2.6.9](https://github.com/laurent22/joplin/releases/tag/v2.6.9) | 2021-12-17T11:57:32Z | 19,558 | 9,502 | 3,194 | 32,254 |
|
||||
| [v2.6.7](https://github.com/laurent22/joplin/releases/tag/v2.6.7) (p) | 2021-12-16T10:47:23Z | 4,136 | 180 | 112 | 4,428 |
|
||||
| [v2.6.6](https://github.com/laurent22/joplin/releases/tag/v2.6.6) (p) | 2021-12-13T12:31:43Z | 4,135 | 257 | 174 | 4,566 |
|
||||
| [v2.6.5](https://github.com/laurent22/joplin/releases/tag/v2.6.5) (p) | 2021-12-13T10:07:04Z | 3,462 | 51 | 36 | 3,549 |
|
||||
| [v2.6.4](https://github.com/laurent22/joplin/releases/tag/v2.6.4) (p) | 2021-12-09T19:53:43Z | 4,200 | 291 | 207 | 4,698 |
|
||||
| [v2.6.2](https://github.com/laurent22/joplin/releases/tag/v2.6.2) (p) | 2021-11-18T12:19:12Z | 5,997 | 793 | 702 | 7,492 |
|
||||
| [v2.5.12](https://github.com/laurent22/joplin/releases/tag/v2.5.12) | 2021-11-08T11:07:11Z | 83,335 | 32,507 | 25,236 | 141,078 |
|
||||
| [v2.5.10](https://github.com/laurent22/joplin/releases/tag/v2.5.10) | 2021-11-01T08:22:42Z | 47,459 | 19,051 | 10,098 | 76,608 |
|
||||
| [v2.5.8](https://github.com/laurent22/joplin/releases/tag/v2.5.8) | 2021-10-31T11:38:03Z | 16,182 | 6,581 | 2,332 | 25,095 |
|
||||
| [v2.5.7](https://github.com/laurent22/joplin/releases/tag/v2.5.7) (p) | 2021-10-29T14:47:33Z | 3,731 | 203 | 164 | 4,098 |
|
||||
| [v2.5.6](https://github.com/laurent22/joplin/releases/tag/v2.5.6) (p) | 2021-10-28T22:03:09Z | 3,786 | 178 | 108 | 4,072 |
|
||||
| [v2.5.4](https://github.com/laurent22/joplin/releases/tag/v2.5.4) (p) | 2021-10-19T10:10:54Z | 5,278 | 568 | 580 | 6,426 |
|
||||
| [v2.4.12](https://github.com/laurent22/joplin/releases/tag/v2.4.12) | 2021-10-13T17:24:34Z | 47,915 | 19,984 | 9,789 | 77,688 |
|
||||
| [v2.5.1](https://github.com/laurent22/joplin/releases/tag/v2.5.1) (p) | 2021-10-02T09:51:58Z | 6,335 | 907 | 947 | 8,189 |
|
||||
| [v2.4.9](https://github.com/laurent22/joplin/releases/tag/v2.4.9) | 2021-09-29T19:08:58Z | 59,609 | 23,262 | 15,914 | 98,785 |
|
||||
| [v2.4.8](https://github.com/laurent22/joplin/releases/tag/v2.4.8) (p) | 2021-09-22T19:01:46Z | 10,121 | 1,774 | 535 | 12,430 |
|
||||
| [v2.4.7](https://github.com/laurent22/joplin/releases/tag/v2.4.7) (p) | 2021-09-19T12:53:22Z | 4,259 | 259 | 210 | 4,728 |
|
||||
| [v2.4.6](https://github.com/laurent22/joplin/releases/tag/v2.4.6) (p) | 2021-09-09T18:57:17Z | 5,023 | 460 | 520 | 6,003 |
|
||||
| [v2.4.5](https://github.com/laurent22/joplin/releases/tag/v2.4.5) (p) | 2021-09-06T18:03:28Z | 4,264 | 275 | 225 | 4,764 |
|
||||
| [v2.4.4](https://github.com/laurent22/joplin/releases/tag/v2.4.4) (p) | 2021-08-30T16:02:51Z | 4,473 | 380 | 373 | 5,226 |
|
||||
| [v2.4.3](https://github.com/laurent22/joplin/releases/tag/v2.4.3) (p) | 2021-08-28T15:27:32Z | 3,963 | 207 | 181 | 4,351 |
|
||||
| [v2.4.2](https://github.com/laurent22/joplin/releases/tag/v2.4.2) (p) | 2021-08-27T17:13:21Z | 3,742 | 151 | 91 | 3,984 |
|
||||
| [v2.4.1](https://github.com/laurent22/joplin/releases/tag/v2.4.1) (p) | 2021-08-21T11:52:30Z | 4,579 | 372 | 337 | 5,288 |
|
||||
| [v2.3.5](https://github.com/laurent22/joplin/releases/tag/v2.3.5) | 2021-08-17T06:43:30Z | 85,691 | 31,439 | 33,145 | 150,275 |
|
||||
| [v2.3.3](https://github.com/laurent22/joplin/releases/tag/v2.3.3) | 2021-08-14T09:19:40Z | 18,869 | 6,886 | 4,062 | 29,817 |
|
||||
| [v2.2.7](https://github.com/laurent22/joplin/releases/tag/v2.2.7) | 2021-08-11T11:03:26Z | 18,724 | 7,524 | 2,607 | 28,855 |
|
||||
| [v2.2.6](https://github.com/laurent22/joplin/releases/tag/v2.2.6) (p) | 2021-08-09T19:29:20Z | 10,994 | 4,619 | 958 | 16,571 |
|
||||
| [v2.2.5](https://github.com/laurent22/joplin/releases/tag/v2.2.5) (p) | 2021-08-07T10:35:24Z | 4,310 | 278 | 208 | 4,796 |
|
||||
| [v2.2.4](https://github.com/laurent22/joplin/releases/tag/v2.2.4) (p) | 2021-08-05T16:42:48Z | 3,972 | 208 | 133 | 4,313 |
|
||||
| [v2.2.2](https://github.com/laurent22/joplin/releases/tag/v2.2.2) (p) | 2021-07-19T10:28:35Z | 5,891 | 737 | 648 | 7,276 |
|
||||
| [v2.1.9](https://github.com/laurent22/joplin/releases/tag/v2.1.9) | 2021-07-19T10:28:43Z | 50,342 | 18,962 | 16,822 | 86,126 |
|
||||
| [v2.2.1](https://github.com/laurent22/joplin/releases/tag/v2.2.1) (p) | 2021-07-09T17:38:25Z | 5,511 | 417 | 394 | 6,322 |
|
||||
| [v2.1.8](https://github.com/laurent22/joplin/releases/tag/v2.1.8) | 2021-07-03T08:25:16Z | 33,780 | 12,206 | 12,736 | 58,722 |
|
||||
| [v2.1.7](https://github.com/laurent22/joplin/releases/tag/v2.1.7) | 2021-06-26T19:48:55Z | 17,095 | 6,410 | 3,638 | 27,143 |
|
||||
| [v2.1.5](https://github.com/laurent22/joplin/releases/tag/v2.1.5) (p) | 2021-06-23T15:08:52Z | 4,409 | 254 | 202 | 4,865 |
|
||||
| [v2.1.3](https://github.com/laurent22/joplin/releases/tag/v2.1.3) (p) | 2021-06-19T16:32:51Z | 4,475 | 312 | 217 | 5,004 |
|
||||
| [v2.0.11](https://github.com/laurent22/joplin/releases/tag/v2.0.11) | 2021-06-16T17:55:49Z | 26,862 | 9,282 | 9,908 | 46,052 |
|
||||
| [v2.0.10](https://github.com/laurent22/joplin/releases/tag/v2.0.10) | 2021-06-16T07:58:29Z | 6,025 | 946 | 399 | 7,370 |
|
||||
| [v2.0.9](https://github.com/laurent22/joplin/releases/tag/v2.0.9) (p) | 2021-06-12T09:30:30Z | 4,484 | 307 | 900 | 5,691 |
|
||||
| [v2.0.8](https://github.com/laurent22/joplin/releases/tag/v2.0.8) (p) | 2021-06-10T16:15:08Z | 3,904 | 246 | 599 | 4,749 |
|
||||
| [v2.0.4](https://github.com/laurent22/joplin/releases/tag/v2.0.4) (p) | 2021-06-02T12:54:17Z | 1,642 | 404 | 393 | 2,439 |
|
||||
| [v2.0.2](https://github.com/laurent22/joplin/releases/tag/v2.0.2) (p) | 2021-05-21T18:07:48Z | 5,892 | 503 | 1,681 | 8,076 |
|
||||
| [v2.0.1](https://github.com/laurent22/joplin/releases/tag/v2.0.1) (p) | 2021-05-15T13:22:58Z | 948 | 288 | 1,039 | 2,275 |
|
||||
| [v1.8.5](https://github.com/laurent22/joplin/releases/tag/v1.8.5) | 2021-05-10T11:58:14Z | 41,897 | 16,306 | 19,440 | 77,643 |
|
||||
| [v1.8.4](https://github.com/laurent22/joplin/releases/tag/v1.8.4) (p) | 2021-05-09T18:05:05Z | 3,740 | 152 | 473 | 4,365 |
|
||||
| [v1.8.3](https://github.com/laurent22/joplin/releases/tag/v1.8.3) (p) | 2021-05-04T10:38:16Z | 4,764 | 320 | 951 | 6,035 |
|
||||
| [v1.8.2](https://github.com/laurent22/joplin/releases/tag/v1.8.2) (p) | 2021-04-25T10:50:51Z | 5,043 | 451 | 1,300 | 6,794 |
|
||||
| [v1.8.1](https://github.com/laurent22/joplin/releases/tag/v1.8.1) (p) | 2021-03-29T10:46:41Z | 6,063 | 840 | 2,466 | 9,369 |
|
||||
| [v1.7.11](https://github.com/laurent22/joplin/releases/tag/v1.7.11) | 2021-02-03T12:50:01Z | 121,096 | 43,007 | 64,457 | 228,560 |
|
||||
| [v1.7.10](https://github.com/laurent22/joplin/releases/tag/v1.7.10) | 2021-01-30T13:25:29Z | 14,610 | 4,883 | 4,529 | 24,022 |
|
||||
| [v2.0.2](https://github.com/laurent22/joplin/releases/tag/v2.0.2) (p) | 2021-05-21T18:07:48Z | 5,997 | 503 | 1,681 | 8,181 |
|
||||
| [v2.0.1](https://github.com/laurent22/joplin/releases/tag/v2.0.1) (p) | 2021-05-15T13:22:58Z | 950 | 288 | 1,039 | 2,277 |
|
||||
| [v1.8.5](https://github.com/laurent22/joplin/releases/tag/v1.8.5) | 2021-05-10T11:58:14Z | 42,030 | 16,310 | 19,440 | 77,780 |
|
||||
| [v1.8.4](https://github.com/laurent22/joplin/releases/tag/v1.8.4) (p) | 2021-05-09T18:05:05Z | 3,846 | 152 | 473 | 4,471 |
|
||||
| [v1.8.3](https://github.com/laurent22/joplin/releases/tag/v1.8.3) (p) | 2021-05-04T10:38:16Z | 4,870 | 320 | 951 | 6,141 |
|
||||
| [v1.8.2](https://github.com/laurent22/joplin/releases/tag/v1.8.2) (p) | 2021-04-25T10:50:51Z | 5,152 | 451 | 1,300 | 6,903 |
|
||||
| [v1.8.1](https://github.com/laurent22/joplin/releases/tag/v1.8.1) (p) | 2021-03-29T10:46:41Z | 6,168 | 840 | 2,466 | 9,474 |
|
||||
| [v1.7.11](https://github.com/laurent22/joplin/releases/tag/v1.7.11) | 2021-02-03T12:50:01Z | 121,243 | 43,010 | 64,462 | 228,715 |
|
||||
| [v1.7.10](https://github.com/laurent22/joplin/releases/tag/v1.7.10) | 2021-01-30T13:25:29Z | 14,623 | 4,883 | 4,529 | 24,035 |
|
||||
| [v1.7.9](https://github.com/laurent22/joplin/releases/tag/v1.7.9) (p) | 2021-01-28T09:50:21Z | 531 | 148 | 514 | 1,193 |
|
||||
| [v1.7.6](https://github.com/laurent22/joplin/releases/tag/v1.7.6) (p) | 2021-01-27T10:36:05Z | 346 | 108 | 304 | 758 |
|
||||
| [v1.7.5](https://github.com/laurent22/joplin/releases/tag/v1.7.5) (p) | 2021-01-26T09:53:05Z | 439 | 219 | 468 | 1,126 |
|
||||
| [v1.7.4](https://github.com/laurent22/joplin/releases/tag/v1.7.4) (p) | 2021-01-22T17:58:38Z | 729 | 219 | 641 | 1,589 |
|
||||
| [v1.6.8](https://github.com/laurent22/joplin/releases/tag/v1.6.8) | 2021-01-20T18:11:34Z | 23,464 | 7,731 | 7,636 | 38,831 |
|
||||
| [v1.6.8](https://github.com/laurent22/joplin/releases/tag/v1.6.8) | 2021-01-20T18:11:34Z | 23,625 | 7,731 | 7,636 | 38,992 |
|
||||
| [v1.7.3](https://github.com/laurent22/joplin/releases/tag/v1.7.3) (p) | 2021-01-20T11:23:50Z | 383 | 95 | 459 | 937 |
|
||||
| [v1.6.7](https://github.com/laurent22/joplin/releases/tag/v1.6.7) | 2021-01-11T23:20:33Z | 14,999 | 4,669 | 4,572 | 24,240 |
|
||||
| [v1.6.6](https://github.com/laurent22/joplin/releases/tag/v1.6.6) | 2021-01-09T16:15:31Z | 12,865 | 3,441 | 4,819 | 21,125 |
|
||||
| [v1.6.5](https://github.com/laurent22/joplin/releases/tag/v1.6.5) (p) | 2021-01-09T01:24:32Z | 4,007 | 91 | 324 | 4,422 |
|
||||
| [v1.6.7](https://github.com/laurent22/joplin/releases/tag/v1.6.7) | 2021-01-11T23:20:33Z | 15,128 | 4,669 | 4,572 | 24,369 |
|
||||
| [v1.6.6](https://github.com/laurent22/joplin/releases/tag/v1.6.6) | 2021-01-09T16:15:31Z | 12,865 | 3,441 | 4,820 | 21,126 |
|
||||
| [v1.6.5](https://github.com/laurent22/joplin/releases/tag/v1.6.5) (p) | 2021-01-09T01:24:32Z | 4,111 | 91 | 324 | 4,526 |
|
||||
| [v1.6.4](https://github.com/laurent22/joplin/releases/tag/v1.6.4) (p) | 2021-01-07T19:11:32Z | 422 | 93 | 220 | 735 |
|
||||
| [v1.6.2](https://github.com/laurent22/joplin/releases/tag/v1.6.2) (p) | 2021-01-04T22:34:55Z | 704 | 242 | 606 | 1,552 |
|
||||
| [v1.5.14](https://github.com/laurent22/joplin/releases/tag/v1.5.14) | 2020-12-30T01:48:46Z | 15,502 | 5,233 | 5,551 | 26,286 |
|
||||
| [v1.6.2](https://github.com/laurent22/joplin/releases/tag/v1.6.2) (p) | 2021-01-04T22:34:55Z | 705 | 242 | 606 | 1,553 |
|
||||
| [v1.5.14](https://github.com/laurent22/joplin/releases/tag/v1.5.14) | 2020-12-30T01:48:46Z | 15,630 | 5,233 | 5,551 | 26,414 |
|
||||
| [v1.6.1](https://github.com/laurent22/joplin/releases/tag/v1.6.1) (p) | 2020-12-29T19:37:45Z | 202 | 53 | 184 | 439 |
|
||||
| [v1.5.13](https://github.com/laurent22/joplin/releases/tag/v1.5.13) | 2020-12-29T18:29:15Z | 786 | 242 | 228 | 1,256 |
|
||||
| [v1.5.12](https://github.com/laurent22/joplin/releases/tag/v1.5.12) | 2020-12-28T15:14:08Z | 2,567 | 1,796 | 943 | 5,306 |
|
||||
| [v1.5.11](https://github.com/laurent22/joplin/releases/tag/v1.5.11) | 2020-12-27T19:54:07Z | 14,417 | 4,662 | 4,309 | 23,388 |
|
||||
| [v1.5.11](https://github.com/laurent22/joplin/releases/tag/v1.5.11) | 2020-12-27T19:54:07Z | 14,417 | 4,662 | 4,311 | 23,390 |
|
||||
| [v1.5.10](https://github.com/laurent22/joplin/releases/tag/v1.5.10) (p) | 2020-12-26T12:35:36Z | 324 | 124 | 286 | 734 |
|
||||
| [v1.5.9](https://github.com/laurent22/joplin/releases/tag/v1.5.9) (p) | 2020-12-23T18:01:08Z | 358 | 386 | 427 | 1,171 |
|
||||
| [v1.5.8](https://github.com/laurent22/joplin/releases/tag/v1.5.8) (p) | 2020-12-20T09:45:19Z | 594 | 179 | 660 | 1,433 |
|
||||
| [v1.5.7](https://github.com/laurent22/joplin/releases/tag/v1.5.7) (p) | 2020-12-10T12:58:33Z | 920 | 269 | 1,010 | 2,199 |
|
||||
| [v1.5.4](https://github.com/laurent22/joplin/releases/tag/v1.5.4) (p) | 2020-12-05T12:07:49Z | 732 | 182 | 652 | 1,566 |
|
||||
| [v1.4.19](https://github.com/laurent22/joplin/releases/tag/v1.4.19) | 2020-12-01T11:11:16Z | 29,857 | 13,629 | 11,714 | 55,200 |
|
||||
| [v1.4.18](https://github.com/laurent22/joplin/releases/tag/v1.4.18) | 2020-11-28T12:21:41Z | 11,757 | 3,910 | 3,162 | 18,829 |
|
||||
| [v1.4.19](https://github.com/laurent22/joplin/releases/tag/v1.4.19) | 2020-12-01T11:11:16Z | 29,982 | 13,630 | 11,715 | 55,327 |
|
||||
| [v1.4.18](https://github.com/laurent22/joplin/releases/tag/v1.4.18) | 2020-11-28T12:21:41Z | 11,757 | 3,910 | 3,163 | 18,830 |
|
||||
| [v1.4.16](https://github.com/laurent22/joplin/releases/tag/v1.4.16) | 2020-11-27T19:40:16Z | 1,648 | 861 | 621 | 3,130 |
|
||||
| [v1.4.15](https://github.com/laurent22/joplin/releases/tag/v1.4.15) | 2020-11-27T13:25:43Z | 1,037 | 513 | 296 | 1,846 |
|
||||
| [v1.4.12](https://github.com/laurent22/joplin/releases/tag/v1.4.12) | 2020-11-23T18:58:07Z | 3,219 | 1,376 | 1,328 | 5,923 |
|
||||
| [v1.4.11](https://github.com/laurent22/joplin/releases/tag/v1.4.11) (p) | 2020-11-19T23:06:51Z | 4,800 | 191 | 611 | 5,602 |
|
||||
| [v1.4.10](https://github.com/laurent22/joplin/releases/tag/v1.4.10) (p) | 2020-11-14T09:53:15Z | 694 | 232 | 703 | 1,629 |
|
||||
| [v1.4.11](https://github.com/laurent22/joplin/releases/tag/v1.4.11) (p) | 2020-11-19T23:06:51Z | 4,906 | 192 | 611 | 5,709 |
|
||||
| [v1.4.10](https://github.com/laurent22/joplin/releases/tag/v1.4.10) (p) | 2020-11-14T09:53:15Z | 695 | 232 | 703 | 1,630 |
|
||||
| [v1.4.9](https://github.com/laurent22/joplin/releases/tag/v1.4.9) (p) | 2020-11-11T14:23:17Z | 873 | 177 | 420 | 1,470 |
|
||||
| [v1.4.7](https://github.com/laurent22/joplin/releases/tag/v1.4.7) (p) | 2020-11-07T18:23:29Z | 559 | 208 | 534 | 1,301 |
|
||||
| [v1.3.18](https://github.com/laurent22/joplin/releases/tag/v1.3.18) | 2020-11-06T12:07:02Z | 35,449 | 11,388 | 10,548 | 57,385 |
|
||||
| [v1.3.18](https://github.com/laurent22/joplin/releases/tag/v1.3.18) | 2020-11-06T12:07:02Z | 35,573 | 11,388 | 10,548 | 57,509 |
|
||||
| [v1.3.17](https://github.com/laurent22/joplin/releases/tag/v1.3.17) (p) | 2020-11-06T11:35:15Z | 86 | 64 | 43 | 193 |
|
||||
| [v1.4.6](https://github.com/laurent22/joplin/releases/tag/v1.4.6) (p) | 2020-11-05T22:44:12Z | 753 | 128 | 70 | 951 |
|
||||
| [v1.3.15](https://github.com/laurent22/joplin/releases/tag/v1.3.15) | 2020-11-04T12:22:50Z | 2,795 | 1,344 | 871 | 5,010 |
|
||||
@@ -243,137 +244,137 @@ updated: 2025-06-01T02:15:29Z
|
||||
| [v1.3.3](https://github.com/laurent22/joplin/releases/tag/v1.3.3) (p) | 2020-10-17T10:56:57Z | 156 | 81 | 52 | 289 |
|
||||
| [v1.3.2](https://github.com/laurent22/joplin/releases/tag/v1.3.2) (p) | 2020-10-11T20:39:49Z | 711 | 217 | 587 | 1,515 |
|
||||
| [v1.3.1](https://github.com/laurent22/joplin/releases/tag/v1.3.1) (p) | 2020-10-11T15:10:18Z | 121 | 87 | 63 | 271 |
|
||||
| [v1.2.6](https://github.com/laurent22/joplin/releases/tag/v1.2.6) | 2020-10-09T13:56:59Z | 49,405 | 17,792 | 14,091 | 81,288 |
|
||||
| [v1.2.6](https://github.com/laurent22/joplin/releases/tag/v1.2.6) | 2020-10-09T13:56:59Z | 49,533 | 17,792 | 14,093 | 81,418 |
|
||||
| [v1.2.4](https://github.com/laurent22/joplin/releases/tag/v1.2.4) (p) | 2020-09-30T07:34:29Z | 860 | 288 | 821 | 1,969 |
|
||||
| [v1.2.3](https://github.com/laurent22/joplin/releases/tag/v1.2.3) (p) | 2020-09-29T15:13:02Z | 258 | 100 | 102 | 460 |
|
||||
| [v1.2.2](https://github.com/laurent22/joplin/releases/tag/v1.2.2) (p) | 2020-09-22T20:31:55Z | 1,155 | 242 | 658 | 2,055 |
|
||||
| [v1.1.4](https://github.com/laurent22/joplin/releases/tag/v1.1.4) | 2020-09-21T11:20:09Z | 28,233 | 13,555 | 7,785 | 49,573 |
|
||||
| [v1.2.2](https://github.com/laurent22/joplin/releases/tag/v1.2.2) (p) | 2020-09-22T20:31:55Z | 1,155 | 243 | 658 | 2,056 |
|
||||
| [v1.1.4](https://github.com/laurent22/joplin/releases/tag/v1.1.4) | 2020-09-21T11:20:09Z | 28,233 | 13,555 | 7,786 | 49,574 |
|
||||
| [v1.1.3](https://github.com/laurent22/joplin/releases/tag/v1.1.3) (p) | 2020-09-17T10:30:37Z | 626 | 189 | 484 | 1,299 |
|
||||
| [v1.1.2](https://github.com/laurent22/joplin/releases/tag/v1.1.2) (p) | 2020-09-15T12:58:38Z | 420 | 152 | 271 | 843 |
|
||||
| [v1.1.1](https://github.com/laurent22/joplin/releases/tag/v1.1.1) (p) | 2020-09-11T23:32:47Z | 597 | 232 | 371 | 1,200 |
|
||||
| [v1.0.245](https://github.com/laurent22/joplin/releases/tag/v1.0.245) | 2020-09-09T12:56:10Z | 22,959 | 10,065 | 5,676 | 38,700 |
|
||||
| [v1.0.245](https://github.com/laurent22/joplin/releases/tag/v1.0.245) | 2020-09-09T12:56:10Z | 22,966 | 10,065 | 5,676 | 38,707 |
|
||||
| [v1.0.242](https://github.com/laurent22/joplin/releases/tag/v1.0.242) | 2020-09-04T22:00:34Z | 12,908 | 6,463 | 3,046 | 22,417 |
|
||||
| [v1.0.241](https://github.com/laurent22/joplin/releases/tag/v1.0.241) | 2020-09-04T18:06:00Z | 26,664 | 5,987 | 5,147 | 37,798 |
|
||||
| [v1.0.241](https://github.com/laurent22/joplin/releases/tag/v1.0.241) | 2020-09-04T18:06:00Z | 26,671 | 5,987 | 5,149 | 37,807 |
|
||||
| [v1.0.239](https://github.com/laurent22/joplin/releases/tag/v1.0.239) (p) | 2020-09-01T21:56:36Z | 992 | 268 | 425 | 1,685 |
|
||||
| [v1.0.237](https://github.com/laurent22/joplin/releases/tag/v1.0.237) (p) | 2020-08-29T15:38:04Z | 637 | 963 | 360 | 1,960 |
|
||||
| [v1.0.236](https://github.com/laurent22/joplin/releases/tag/v1.0.236) (p) | 2020-08-28T09:16:54Z | 360 | 150 | 126 | 636 |
|
||||
| [v1.0.235](https://github.com/laurent22/joplin/releases/tag/v1.0.235) (p) | 2020-08-18T22:08:01Z | 2,065 | 528 | 944 | 3,537 |
|
||||
| [v1.0.234](https://github.com/laurent22/joplin/releases/tag/v1.0.234) (p) | 2020-08-17T23:13:02Z | 698 | 164 | 123 | 985 |
|
||||
| [v1.0.233](https://github.com/laurent22/joplin/releases/tag/v1.0.233) | 2020-08-01T14:51:15Z | 48,026 | 18,246 | 12,387 | 78,659 |
|
||||
| [v1.0.233](https://github.com/laurent22/joplin/releases/tag/v1.0.233) | 2020-08-01T14:51:15Z | 48,154 | 18,246 | 12,388 | 78,788 |
|
||||
| [v1.0.232](https://github.com/laurent22/joplin/releases/tag/v1.0.232) (p) | 2020-07-28T22:34:40Z | 703 | 262 | 201 | 1,166 |
|
||||
| [v1.0.227](https://github.com/laurent22/joplin/releases/tag/v1.0.227) | 2020-07-07T20:44:54Z | 41,979 | 15,324 | 9,674 | 66,977 |
|
||||
| [v1.0.227](https://github.com/laurent22/joplin/releases/tag/v1.0.227) | 2020-07-07T20:44:54Z | 41,979 | 15,324 | 9,677 | 66,980 |
|
||||
| [v1.0.226](https://github.com/laurent22/joplin/releases/tag/v1.0.226) (p) | 2020-07-04T10:21:26Z | 4,978 | 2,293 | 710 | 7,981 |
|
||||
| [v1.0.224](https://github.com/laurent22/joplin/releases/tag/v1.0.224) | 2020-06-20T22:26:08Z | 25,297 | 11,049 | 6,031 | 42,377 |
|
||||
| [v1.0.223](https://github.com/laurent22/joplin/releases/tag/v1.0.223) (p) | 2020-06-20T11:51:27Z | 231 | 152 | 101 | 484 |
|
||||
| [v1.0.221](https://github.com/laurent22/joplin/releases/tag/v1.0.221) (p) | 2020-06-20T01:44:20Z | 901 | 247 | 235 | 1,383 |
|
||||
| [v1.0.220](https://github.com/laurent22/joplin/releases/tag/v1.0.220) | 2020-06-13T18:26:22Z | 32,825 | 9,970 | 6,446 | 49,241 |
|
||||
| [v1.0.220](https://github.com/laurent22/joplin/releases/tag/v1.0.220) | 2020-06-13T18:26:22Z | 32,826 | 9,970 | 6,447 | 49,243 |
|
||||
| [v1.0.218](https://github.com/laurent22/joplin/releases/tag/v1.0.218) | 2020-06-07T10:43:34Z | 14,642 | 7,018 | 3,154 | 24,814 |
|
||||
| [v1.0.217](https://github.com/laurent22/joplin/releases/tag/v1.0.217) (p) | 2020-06-06T15:17:27Z | 280 | 138 | 85 | 503 |
|
||||
| [v1.0.216](https://github.com/laurent22/joplin/releases/tag/v1.0.216) | 2020-05-24T14:21:01Z | 41,798 | 14,355 | 10,220 | 66,373 |
|
||||
| [v1.0.216](https://github.com/laurent22/joplin/releases/tag/v1.0.216) | 2020-05-24T14:21:01Z | 41,930 | 14,357 | 10,221 | 66,508 |
|
||||
| [v1.0.214](https://github.com/laurent22/joplin/releases/tag/v1.0.214) (p) | 2020-05-21T17:15:15Z | 7,107 | 3,513 | 789 | 11,409 |
|
||||
| [v1.0.212](https://github.com/laurent22/joplin/releases/tag/v1.0.212) (p) | 2020-05-21T07:48:39Z | 255 | 112 | 72 | 439 |
|
||||
| [v1.0.211](https://github.com/laurent22/joplin/releases/tag/v1.0.211) (p) | 2020-05-20T08:59:16Z | 354 | 174 | 113 | 641 |
|
||||
| [v1.0.209](https://github.com/laurent22/joplin/releases/tag/v1.0.209) (p) | 2020-05-17T18:32:51Z | 1,449 | 894 | 173 | 2,516 |
|
||||
| [v1.0.207](https://github.com/laurent22/joplin/releases/tag/v1.0.207) (p) | 2020-05-10T16:37:35Z | 1,252 | 305 | 1,044 | 2,601 |
|
||||
| [v1.0.201](https://github.com/laurent22/joplin/releases/tag/v1.0.201) | 2020-04-15T22:55:13Z | 54,411 | 20,102 | 18,218 | 92,731 |
|
||||
| [v1.0.201](https://github.com/laurent22/joplin/releases/tag/v1.0.201) | 2020-04-15T22:55:13Z | 54,414 | 20,102 | 18,220 | 92,736 |
|
||||
| [v1.0.200](https://github.com/laurent22/joplin/releases/tag/v1.0.200) | 2020-04-12T12:17:46Z | 9,637 | 4,935 | 1,931 | 16,503 |
|
||||
| [v1.0.199](https://github.com/laurent22/joplin/releases/tag/v1.0.199) | 2020-04-10T18:41:58Z | 19,883 | 5,932 | 3,827 | 29,642 |
|
||||
| [v1.0.197](https://github.com/laurent22/joplin/releases/tag/v1.0.197) | 2020-03-30T17:21:22Z | 23,514 | 9,870 | 6,629 | 40,013 |
|
||||
| [v1.0.195](https://github.com/laurent22/joplin/releases/tag/v1.0.195) | 2020-03-22T19:56:12Z | 19,178 | 7,997 | 4,535 | 31,710 |
|
||||
| [v1.0.199](https://github.com/laurent22/joplin/releases/tag/v1.0.199) | 2020-04-10T18:41:58Z | 19,899 | 5,932 | 3,827 | 29,658 |
|
||||
| [v1.0.197](https://github.com/laurent22/joplin/releases/tag/v1.0.197) | 2020-03-30T17:21:22Z | 23,517 | 9,870 | 6,641 | 40,028 |
|
||||
| [v1.0.195](https://github.com/laurent22/joplin/releases/tag/v1.0.195) | 2020-03-22T19:56:12Z | 19,178 | 7,998 | 4,535 | 31,711 |
|
||||
| [v1.0.194](https://github.com/laurent22/joplin/releases/tag/v1.0.194) (p) | 2020-03-14T00:00:32Z | 1,339 | 1,431 | 543 | 3,313 |
|
||||
| [v1.0.193](https://github.com/laurent22/joplin/releases/tag/v1.0.193) | 2020-03-08T08:58:53Z | 28,845 | 10,968 | 7,445 | 47,258 |
|
||||
| [v1.0.192](https://github.com/laurent22/joplin/releases/tag/v1.0.192) (p) | 2020-03-06T23:27:52Z | 563 | 164 | 113 | 840 |
|
||||
| [v1.0.190](https://github.com/laurent22/joplin/releases/tag/v1.0.190) (p) | 2020-03-06T01:22:22Z | 441 | 133 | 110 | 684 |
|
||||
| [v1.0.189](https://github.com/laurent22/joplin/releases/tag/v1.0.189) (p) | 2020-03-04T17:27:15Z | 432 | 136 | 122 | 690 |
|
||||
| [v1.0.187](https://github.com/laurent22/joplin/releases/tag/v1.0.187) (p) | 2020-03-01T12:31:06Z | 983 | 277 | 298 | 1,558 |
|
||||
| [v1.0.179](https://github.com/laurent22/joplin/releases/tag/v1.0.179) | 2020-01-24T22:42:41Z | 71,769 | 29,127 | 22,597 | 123,493 |
|
||||
| [v1.0.178](https://github.com/laurent22/joplin/releases/tag/v1.0.178) | 2020-01-20T19:06:45Z | 17,678 | 6,006 | 2,620 | 26,304 |
|
||||
| [v1.0.177](https://github.com/laurent22/joplin/releases/tag/v1.0.177) (p) | 2019-12-30T14:40:40Z | 2,013 | 478 | 750 | 3,241 |
|
||||
| [v1.0.179](https://github.com/laurent22/joplin/releases/tag/v1.0.179) | 2020-01-24T22:42:41Z | 71,780 | 29,128 | 22,597 | 123,505 |
|
||||
| [v1.0.178](https://github.com/laurent22/joplin/releases/tag/v1.0.178) | 2020-01-20T19:06:45Z | 17,678 | 6,006 | 2,621 | 26,305 |
|
||||
| [v1.0.177](https://github.com/laurent22/joplin/releases/tag/v1.0.177) (p) | 2019-12-30T14:40:40Z | 2,013 | 478 | 752 | 3,243 |
|
||||
| [v1.0.176](https://github.com/laurent22/joplin/releases/tag/v1.0.176) (p) | 2019-12-14T10:36:44Z | 3,176 | 2,579 | 496 | 6,251 |
|
||||
| [v1.0.175](https://github.com/laurent22/joplin/releases/tag/v1.0.175) | 2019-12-08T11:48:47Z | 73,798 | 17,022 | 16,625 | 107,445 |
|
||||
| [v1.0.174](https://github.com/laurent22/joplin/releases/tag/v1.0.174) | 2019-11-12T18:20:58Z | 30,755 | 11,804 | 8,254 | 50,813 |
|
||||
| [v1.0.173](https://github.com/laurent22/joplin/releases/tag/v1.0.173) | 2019-11-11T08:33:35Z | 5,154 | 2,123 | 773 | 8,050 |
|
||||
| [v1.0.170](https://github.com/laurent22/joplin/releases/tag/v1.0.170) | 2019-10-13T22:13:04Z | 27,879 | 8,825 | 7,716 | 44,420 |
|
||||
| [v1.0.169](https://github.com/laurent22/joplin/releases/tag/v1.0.169) | 2019-09-27T18:35:13Z | 17,291 | 5,973 | 3,785 | 27,049 |
|
||||
| [v1.0.168](https://github.com/laurent22/joplin/releases/tag/v1.0.168) | 2019-09-25T21:21:38Z | 5,394 | 2,316 | 745 | 8,455 |
|
||||
| [v1.0.167](https://github.com/laurent22/joplin/releases/tag/v1.0.167) | 2019-09-10T08:48:37Z | 17,013 | 5,747 | 3,741 | 26,501 |
|
||||
| [v1.0.166](https://github.com/laurent22/joplin/releases/tag/v1.0.166) | 2019-09-09T17:35:54Z | 2,006 | 601 | 261 | 2,868 |
|
||||
| [v1.0.165](https://github.com/laurent22/joplin/releases/tag/v1.0.165) | 2019-08-14T21:46:29Z | 19,121 | 7,022 | 5,491 | 31,634 |
|
||||
| [v1.0.161](https://github.com/laurent22/joplin/releases/tag/v1.0.161) | 2019-07-13T18:30:00Z | 19,452 | 6,391 | 4,161 | 30,004 |
|
||||
| [v1.0.160](https://github.com/laurent22/joplin/releases/tag/v1.0.160) | 2019-06-15T00:21:40Z | 30,870 | 7,796 | 8,134 | 46,800 |
|
||||
| [v1.0.159](https://github.com/laurent22/joplin/releases/tag/v1.0.159) | 2019-06-08T00:00:19Z | 5,272 | 2,220 | 1,309 | 8,801 |
|
||||
| [v1.0.175](https://github.com/laurent22/joplin/releases/tag/v1.0.175) | 2019-12-08T11:48:47Z | 73,802 | 17,023 | 16,625 | 107,450 |
|
||||
| [v1.0.174](https://github.com/laurent22/joplin/releases/tag/v1.0.174) | 2019-11-12T18:20:58Z | 30,765 | 11,806 | 8,255 | 50,826 |
|
||||
| [v1.0.173](https://github.com/laurent22/joplin/releases/tag/v1.0.173) | 2019-11-11T08:33:35Z | 5,154 | 2,123 | 774 | 8,051 |
|
||||
| [v1.0.170](https://github.com/laurent22/joplin/releases/tag/v1.0.170) | 2019-10-13T22:13:04Z | 27,888 | 8,827 | 7,716 | 44,431 |
|
||||
| [v1.0.169](https://github.com/laurent22/joplin/releases/tag/v1.0.169) | 2019-09-27T18:35:13Z | 17,294 | 5,973 | 3,785 | 27,052 |
|
||||
| [v1.0.168](https://github.com/laurent22/joplin/releases/tag/v1.0.168) | 2019-09-25T21:21:38Z | 5,394 | 2,316 | 747 | 8,457 |
|
||||
| [v1.0.167](https://github.com/laurent22/joplin/releases/tag/v1.0.167) | 2019-09-10T08:48:37Z | 17,015 | 5,747 | 3,742 | 26,504 |
|
||||
| [v1.0.166](https://github.com/laurent22/joplin/releases/tag/v1.0.166) | 2019-09-09T17:35:54Z | 2,006 | 603 | 261 | 2,870 |
|
||||
| [v1.0.165](https://github.com/laurent22/joplin/releases/tag/v1.0.165) | 2019-08-14T21:46:29Z | 19,123 | 7,022 | 5,491 | 31,636 |
|
||||
| [v1.0.161](https://github.com/laurent22/joplin/releases/tag/v1.0.161) | 2019-07-13T18:30:00Z | 19,453 | 6,391 | 4,161 | 30,005 |
|
||||
| [v1.0.160](https://github.com/laurent22/joplin/releases/tag/v1.0.160) | 2019-06-15T00:21:40Z | 30,871 | 7,796 | 8,134 | 46,801 |
|
||||
| [v1.0.159](https://github.com/laurent22/joplin/releases/tag/v1.0.159) | 2019-06-08T00:00:19Z | 5,273 | 2,220 | 1,315 | 8,808 |
|
||||
| [v1.0.158](https://github.com/laurent22/joplin/releases/tag/v1.0.158) | 2019-05-27T19:01:18Z | 9,891 | 3,590 | 1,962 | 15,443 |
|
||||
| [v1.0.157](https://github.com/laurent22/joplin/releases/tag/v1.0.157) | 2019-05-26T17:55:53Z | 2,248 | 881 | 314 | 3,443 |
|
||||
| [v1.0.153](https://github.com/laurent22/joplin/releases/tag/v1.0.153) (p) | 2019-05-15T06:27:29Z | 916 | 143 | 131 | 1,190 |
|
||||
| [v1.0.152](https://github.com/laurent22/joplin/releases/tag/v1.0.152) | 2019-05-13T09:08:07Z | 13,948 | 4,475 | 4,091 | 22,514 |
|
||||
| [v1.0.151](https://github.com/laurent22/joplin/releases/tag/v1.0.151) | 2019-05-12T15:14:32Z | 2,002 | 575 | 984 | 3,561 |
|
||||
| [v1.0.152](https://github.com/laurent22/joplin/releases/tag/v1.0.152) | 2019-05-13T09:08:07Z | 13,948 | 4,477 | 4,092 | 22,517 |
|
||||
| [v1.0.151](https://github.com/laurent22/joplin/releases/tag/v1.0.151) | 2019-05-12T15:14:32Z | 2,003 | 575 | 985 | 3,563 |
|
||||
| [v1.0.150](https://github.com/laurent22/joplin/releases/tag/v1.0.150) | 2019-05-12T11:27:48Z | 496 | 175 | 96 | 767 |
|
||||
| [v1.0.148](https://github.com/laurent22/joplin/releases/tag/v1.0.148) (p) | 2019-05-08T19:12:24Z | 186 | 93 | 120 | 399 |
|
||||
| [v1.0.145](https://github.com/laurent22/joplin/releases/tag/v1.0.145) | 2019-05-03T09:16:53Z | 7,083 | 2,901 | 1,465 | 11,449 |
|
||||
| [v1.0.143](https://github.com/laurent22/joplin/releases/tag/v1.0.143) | 2019-04-22T10:51:38Z | 11,994 | 3,593 | 2,809 | 18,396 |
|
||||
| [v1.0.142](https://github.com/laurent22/joplin/releases/tag/v1.0.142) | 2019-04-02T16:44:51Z | 14,863 | 4,607 | 4,757 | 24,227 |
|
||||
| [v1.0.143](https://github.com/laurent22/joplin/releases/tag/v1.0.143) | 2019-04-22T10:51:38Z | 11,994 | 3,593 | 2,811 | 18,398 |
|
||||
| [v1.0.142](https://github.com/laurent22/joplin/releases/tag/v1.0.142) | 2019-04-02T16:44:51Z | 14,863 | 4,607 | 4,758 | 24,228 |
|
||||
| [v1.0.140](https://github.com/laurent22/joplin/releases/tag/v1.0.140) | 2019-03-10T20:59:58Z | 13,729 | 4,218 | 3,407 | 21,354 |
|
||||
| [v1.0.139](https://github.com/laurent22/joplin/releases/tag/v1.0.139) (p) | 2019-03-09T10:06:48Z | 186 | 104 | 69 | 359 |
|
||||
| [v1.0.138](https://github.com/laurent22/joplin/releases/tag/v1.0.138) (p) | 2019-03-03T17:23:00Z | 230 | 128 | 107 | 465 |
|
||||
| [v1.0.137](https://github.com/laurent22/joplin/releases/tag/v1.0.137) (p) | 2019-03-03T01:12:51Z | 650 | 95 | 107 | 852 |
|
||||
| [v1.0.135](https://github.com/laurent22/joplin/releases/tag/v1.0.135) | 2019-02-27T23:36:57Z | 12,704 | 3,998 | 4,108 | 20,810 |
|
||||
| [v1.0.135](https://github.com/laurent22/joplin/releases/tag/v1.0.135) | 2019-02-27T23:36:57Z | 12,704 | 3,998 | 4,109 | 20,811 |
|
||||
| [v1.0.134](https://github.com/laurent22/joplin/releases/tag/v1.0.134) | 2019-02-27T10:21:44Z | 1,512 | 610 | 244 | 2,366 |
|
||||
| [v1.0.132](https://github.com/laurent22/joplin/releases/tag/v1.0.132) | 2019-02-26T23:02:05Z | 1,150 | 491 | 119 | 1,760 |
|
||||
| [v1.0.127](https://github.com/laurent22/joplin/releases/tag/v1.0.127) | 2019-02-14T23:12:48Z | 10,028 | 3,214 | 2,960 | 16,202 |
|
||||
| [v1.0.127](https://github.com/laurent22/joplin/releases/tag/v1.0.127) | 2019-02-14T23:12:48Z | 10,029 | 3,214 | 2,961 | 16,204 |
|
||||
| [v1.0.126](https://github.com/laurent22/joplin/releases/tag/v1.0.126) (p) | 2019-02-09T19:46:16Z | 1,001 | 115 | 138 | 1,254 |
|
||||
| [v1.0.125](https://github.com/laurent22/joplin/releases/tag/v1.0.125) | 2019-01-26T18:14:33Z | 10,416 | 3,597 | 1,726 | 15,739 |
|
||||
| [v1.0.120](https://github.com/laurent22/joplin/releases/tag/v1.0.120) | 2019-01-10T21:42:53Z | 15,706 | 5,253 | 6,548 | 27,507 |
|
||||
| [v1.0.125](https://github.com/laurent22/joplin/releases/tag/v1.0.125) | 2019-01-26T18:14:33Z | 10,421 | 3,597 | 1,726 | 15,744 |
|
||||
| [v1.0.120](https://github.com/laurent22/joplin/releases/tag/v1.0.120) | 2019-01-10T21:42:53Z | 15,706 | 5,253 | 6,549 | 27,508 |
|
||||
| [v1.0.119](https://github.com/laurent22/joplin/releases/tag/v1.0.119) | 2018-12-18T12:40:22Z | 9,011 | 3,302 | 2,041 | 14,354 |
|
||||
| [v1.0.118](https://github.com/laurent22/joplin/releases/tag/v1.0.118) | 2019-01-11T08:34:13Z | 763 | 288 | 115 | 1,166 |
|
||||
| [v1.0.117](https://github.com/laurent22/joplin/releases/tag/v1.0.117) | 2018-11-24T12:05:24Z | 16,348 | 4,940 | 6,409 | 27,697 |
|
||||
| [v1.0.116](https://github.com/laurent22/joplin/releases/tag/v1.0.116) | 2018-11-20T19:09:24Z | 4,069 | 1,167 | 740 | 5,976 |
|
||||
| [v1.0.117](https://github.com/laurent22/joplin/releases/tag/v1.0.117) | 2018-11-24T12:05:24Z | 16,348 | 4,940 | 6,412 | 27,700 |
|
||||
| [v1.0.116](https://github.com/laurent22/joplin/releases/tag/v1.0.116) | 2018-11-20T19:09:24Z | 4,069 | 1,169 | 742 | 5,980 |
|
||||
| [v1.0.115](https://github.com/laurent22/joplin/releases/tag/v1.0.115) | 2018-11-16T16:52:02Z | 3,723 | 1,341 | 827 | 5,891 |
|
||||
| [v1.0.114](https://github.com/laurent22/joplin/releases/tag/v1.0.114) | 2018-10-24T20:14:10Z | 11,471 | 3,540 | 3,855 | 18,866 |
|
||||
| [v1.0.111](https://github.com/laurent22/joplin/releases/tag/v1.0.111) | 2018-09-30T20:15:09Z | 12,544 | 3,555 | 3,707 | 19,806 |
|
||||
| [v1.0.114](https://github.com/laurent22/joplin/releases/tag/v1.0.114) | 2018-10-24T20:14:10Z | 11,471 | 3,540 | 3,856 | 18,867 |
|
||||
| [v1.0.111](https://github.com/laurent22/joplin/releases/tag/v1.0.111) | 2018-09-30T20:15:09Z | 12,558 | 3,563 | 3,708 | 19,829 |
|
||||
| [v1.0.110](https://github.com/laurent22/joplin/releases/tag/v1.0.110) | 2018-09-29T12:29:21Z | 1,010 | 449 | 142 | 1,601 |
|
||||
| [v1.0.109](https://github.com/laurent22/joplin/releases/tag/v1.0.109) | 2018-09-27T18:01:41Z | 2,146 | 743 | 356 | 3,245 |
|
||||
| [v1.0.109](https://github.com/laurent22/joplin/releases/tag/v1.0.109) | 2018-09-27T18:01:41Z | 2,146 | 743 | 357 | 3,246 |
|
||||
| [v1.0.108](https://github.com/laurent22/joplin/releases/tag/v1.0.108) (p) | 2018-09-29T18:49:29Z | 66 | 60 | 38 | 164 |
|
||||
| [v1.0.107](https://github.com/laurent22/joplin/releases/tag/v1.0.107) | 2018-09-16T19:51:07Z | 7,209 | 2,175 | 1,737 | 11,121 |
|
||||
| [v1.0.107](https://github.com/laurent22/joplin/releases/tag/v1.0.107) | 2018-09-16T19:51:07Z | 7,209 | 2,175 | 1,738 | 11,122 |
|
||||
| [v1.0.106](https://github.com/laurent22/joplin/releases/tag/v1.0.106) | 2018-09-08T15:23:40Z | 4,601 | 1,495 | 341 | 6,437 |
|
||||
| [v1.0.105](https://github.com/laurent22/joplin/releases/tag/v1.0.105) | 2018-09-05T11:29:36Z | 4,702 | 1,634 | 1,485 | 7,821 |
|
||||
| [v1.0.104](https://github.com/laurent22/joplin/releases/tag/v1.0.104) | 2018-06-28T20:25:36Z | 15,208 | 4,743 | 7,411 | 27,362 |
|
||||
| [v1.0.103](https://github.com/laurent22/joplin/releases/tag/v1.0.103) | 2018-06-21T19:38:13Z | 2,128 | 926 | 703 | 3,757 |
|
||||
| [v1.0.101](https://github.com/laurent22/joplin/releases/tag/v1.0.101) | 2018-06-17T18:35:11Z | 1,375 | 648 | 434 | 2,457 |
|
||||
| [v1.0.100](https://github.com/laurent22/joplin/releases/tag/v1.0.100) | 2018-06-14T17:41:43Z | 960 | 477 | 272 | 1,709 |
|
||||
| [v1.0.104](https://github.com/laurent22/joplin/releases/tag/v1.0.104) | 2018-06-28T20:25:36Z | 15,208 | 4,743 | 7,414 | 27,365 |
|
||||
| [v1.0.103](https://github.com/laurent22/joplin/releases/tag/v1.0.103) | 2018-06-21T19:38:13Z | 2,130 | 926 | 704 | 3,760 |
|
||||
| [v1.0.101](https://github.com/laurent22/joplin/releases/tag/v1.0.101) | 2018-06-17T18:35:11Z | 1,375 | 648 | 436 | 2,459 |
|
||||
| [v1.0.100](https://github.com/laurent22/joplin/releases/tag/v1.0.100) | 2018-06-14T17:41:43Z | 961 | 477 | 274 | 1,712 |
|
||||
| [v1.0.99](https://github.com/laurent22/joplin/releases/tag/v1.0.99) | 2018-06-10T13:18:23Z | 1,329 | 642 | 409 | 2,380 |
|
||||
| [v1.0.97](https://github.com/laurent22/joplin/releases/tag/v1.0.97) | 2018-06-09T19:23:34Z | 357 | 193 | 87 | 637 |
|
||||
| [v1.0.96](https://github.com/laurent22/joplin/releases/tag/v1.0.96) | 2018-05-26T16:36:39Z | 2,800 | 1,267 | 1,749 | 5,816 |
|
||||
| [v1.0.95](https://github.com/laurent22/joplin/releases/tag/v1.0.95) | 2018-05-25T13:04:30Z | 466 | 258 | 163 | 887 |
|
||||
| [v1.0.94](https://github.com/laurent22/joplin/releases/tag/v1.0.94) | 2018-05-21T20:52:59Z | 1,214 | 629 | 444 | 2,287 |
|
||||
| [v1.0.93](https://github.com/laurent22/joplin/releases/tag/v1.0.93) | 2018-05-14T11:36:01Z | 1,879 | 1,358 | 804 | 4,041 |
|
||||
| [v1.0.95](https://github.com/laurent22/joplin/releases/tag/v1.0.95) | 2018-05-25T13:04:30Z | 468 | 258 | 163 | 889 |
|
||||
| [v1.0.94](https://github.com/laurent22/joplin/releases/tag/v1.0.94) | 2018-05-21T20:52:59Z | 1,216 | 629 | 446 | 2,291 |
|
||||
| [v1.0.93](https://github.com/laurent22/joplin/releases/tag/v1.0.93) | 2018-05-14T11:36:01Z | 1,880 | 1,359 | 804 | 4,043 |
|
||||
| [v1.0.91](https://github.com/laurent22/joplin/releases/tag/v1.0.91) | 2018-05-10T14:48:04Z | 872 | 594 | 364 | 1,830 |
|
||||
| [v1.0.89](https://github.com/laurent22/joplin/releases/tag/v1.0.89) | 2018-05-09T13:05:05Z | 546 | 286 | 157 | 989 |
|
||||
| [v1.0.85](https://github.com/laurent22/joplin/releases/tag/v1.0.85) | 2018-05-01T21:08:24Z | 1,696 | 992 | 677 | 3,365 |
|
||||
| [v1.0.83](https://github.com/laurent22/joplin/releases/tag/v1.0.83) | 2018-04-04T19:43:58Z | 5,630 | 2,577 | 2,703 | 10,910 |
|
||||
| [v1.0.85](https://github.com/laurent22/joplin/releases/tag/v1.0.85) | 2018-05-01T21:08:24Z | 1,698 | 992 | 677 | 3,367 |
|
||||
| [v1.0.83](https://github.com/laurent22/joplin/releases/tag/v1.0.83) | 2018-04-04T19:43:58Z | 5,632 | 2,577 | 2,703 | 10,912 |
|
||||
| [v1.0.82](https://github.com/laurent22/joplin/releases/tag/v1.0.82) | 2018-03-31T19:16:31Z | 751 | 450 | 167 | 1,368 |
|
||||
| [v1.0.81](https://github.com/laurent22/joplin/releases/tag/v1.0.81) | 2018-03-28T08:13:58Z | 1,039 | 637 | 826 | 2,502 |
|
||||
| [v1.0.81](https://github.com/laurent22/joplin/releases/tag/v1.0.81) | 2018-03-28T08:13:58Z | 1,039 | 637 | 827 | 2,503 |
|
||||
| [v1.0.79](https://github.com/laurent22/joplin/releases/tag/v1.0.79) | 2018-03-23T18:00:11Z | 971 | 580 | 428 | 1,979 |
|
||||
| [v1.0.78](https://github.com/laurent22/joplin/releases/tag/v1.0.78) | 2018-03-17T15:27:18Z | 1,354 | 943 | 915 | 3,212 |
|
||||
| [v1.0.77](https://github.com/laurent22/joplin/releases/tag/v1.0.77) | 2018-03-16T15:12:35Z | 202 | 144 | 88 | 434 |
|
||||
| [v1.0.72](https://github.com/laurent22/joplin/releases/tag/v1.0.72) | 2018-03-14T09:44:35Z | 451 | 296 | 98 | 845 |
|
||||
| [v1.0.70](https://github.com/laurent22/joplin/releases/tag/v1.0.70) | 2018-02-28T20:04:30Z | 2,004 | 1,093 | 1,296 | 4,393 |
|
||||
| [v1.0.70](https://github.com/laurent22/joplin/releases/tag/v1.0.70) | 2018-02-28T20:04:30Z | 2,004 | 1,093 | 1,297 | 4,394 |
|
||||
| [v1.0.67](https://github.com/laurent22/joplin/releases/tag/v1.0.67) | 2018-02-19T22:51:08Z | 1,947 | 645 | 0 | 2,592 |
|
||||
| [v1.0.66](https://github.com/laurent22/joplin/releases/tag/v1.0.66) | 2018-02-18T23:09:09Z | 450 | 174 | 109 | 733 |
|
||||
| [v1.0.65](https://github.com/laurent22/joplin/releases/tag/v1.0.65) | 2018-02-17T20:02:25Z | 349 | 171 | 156 | 676 |
|
||||
| [v1.0.64](https://github.com/laurent22/joplin/releases/tag/v1.0.64) | 2018-02-16T00:58:20Z | 1,196 | 583 | 1,148 | 2,927 |
|
||||
| [v1.0.63](https://github.com/laurent22/joplin/releases/tag/v1.0.63) | 2018-02-14T19:40:36Z | 413 | 200 | 115 | 728 |
|
||||
| [v1.0.63](https://github.com/laurent22/joplin/releases/tag/v1.0.63) | 2018-02-14T19:40:36Z | 413 | 200 | 117 | 730 |
|
||||
| [v1.0.62](https://github.com/laurent22/joplin/releases/tag/v1.0.62) | 2018-02-12T20:19:58Z | 714 | 345 | 400 | 1,459 |
|
||||
| [v0.10.61](https://github.com/laurent22/joplin/releases/tag/v0.10.61) | 2018-02-08T18:27:39Z | 1,124 | 678 | 987 | 2,789 |
|
||||
| [v0.10.60](https://github.com/laurent22/joplin/releases/tag/v0.10.60) | 2018-02-06T13:09:56Z | 772 | 565 | 577 | 1,914 |
|
||||
| [v0.10.54](https://github.com/laurent22/joplin/releases/tag/v0.10.54) | 2018-01-31T20:21:30Z | 1,959 | 1,504 | 348 | 3,811 |
|
||||
| [v0.10.52](https://github.com/laurent22/joplin/releases/tag/v0.10.52) | 2018-01-31T19:25:18Z | 164 | 677 | 41 | 882 |
|
||||
| [v0.10.51](https://github.com/laurent22/joplin/releases/tag/v0.10.51) | 2018-01-28T18:47:02Z | 1,430 | 1,643 | 352 | 3,425 |
|
||||
| [v0.10.54](https://github.com/laurent22/joplin/releases/tag/v0.10.54) | 2018-01-31T20:21:30Z | 1,961 | 1,504 | 348 | 3,813 |
|
||||
| [v0.10.52](https://github.com/laurent22/joplin/releases/tag/v0.10.52) | 2018-01-31T19:25:18Z | 164 | 677 | 42 | 883 |
|
||||
| [v0.10.51](https://github.com/laurent22/joplin/releases/tag/v0.10.51) | 2018-01-28T18:47:02Z | 1,431 | 1,643 | 352 | 3,426 |
|
||||
| [v0.10.48](https://github.com/laurent22/joplin/releases/tag/v0.10.48) | 2018-01-23T11:19:51Z | 2,112 | 1,795 | 56 | 3,963 |
|
||||
| [v0.10.47](https://github.com/laurent22/joplin/releases/tag/v0.10.47) | 2018-01-16T17:27:17Z | 1,334 | 1,317 | 91 | 2,742 |
|
||||
| [v0.10.43](https://github.com/laurent22/joplin/releases/tag/v0.10.43) | 2018-01-08T10:12:10Z | 3,486 | 2,401 | 1,240 | 7,127 |
|
||||
| [v0.10.43](https://github.com/laurent22/joplin/releases/tag/v0.10.43) | 2018-01-08T10:12:10Z | 3,486 | 2,401 | 1,241 | 7,128 |
|
||||
| [v0.10.41](https://github.com/laurent22/joplin/releases/tag/v0.10.41) | 2018-01-05T20:38:12Z | 1,245 | 1,598 | 266 | 3,109 |
|
||||
| [v0.10.40](https://github.com/laurent22/joplin/releases/tag/v0.10.40) | 2018-01-02T23:16:57Z | 1,638 | 1,832 | 362 | 3,832 |
|
||||
| [v0.10.39](https://github.com/laurent22/joplin/releases/tag/v0.10.39) | 2017-12-11T21:19:44Z | 5,995 | 4,445 | 3,328 | 13,768 |
|
||||
| [v0.10.39](https://github.com/laurent22/joplin/releases/tag/v0.10.39) | 2017-12-11T21:19:44Z | 5,995 | 4,445 | 3,329 | 13,769 |
|
||||
| [v0.10.38](https://github.com/laurent22/joplin/releases/tag/v0.10.38) | 2017-12-08T10:12:06Z | 1,089 | 1,274 | 330 | 2,693 |
|
||||
| [v0.10.37](https://github.com/laurent22/joplin/releases/tag/v0.10.37) | 2017-12-07T19:38:05Z | 291 | 892 | 109 | 1,292 |
|
||||
| [v0.10.36](https://github.com/laurent22/joplin/releases/tag/v0.10.36) | 2017-12-05T09:34:40Z | 1,052 | 1,405 | 465 | 2,922 |
|
||||
@@ -382,11 +383,11 @@ updated: 2025-06-01T02:15:29Z
|
||||
| [v0.10.33](https://github.com/laurent22/joplin/releases/tag/v0.10.33) | 2017-12-02T13:20:39Z | 95 | 711 | 51 | 857 |
|
||||
| [v0.10.31](https://github.com/laurent22/joplin/releases/tag/v0.10.31) | 2017-12-01T09:56:44Z | 918 | 1,499 | 437 | 2,854 |
|
||||
| [v0.10.30](https://github.com/laurent22/joplin/releases/tag/v0.10.30) | 2017-11-30T20:28:16Z | 842 | 1,428 | 452 | 2,722 |
|
||||
| [v0.10.28](https://github.com/laurent22/joplin/releases/tag/v0.10.28) | 2017-11-30T01:07:46Z | 1,492 | 1,761 | 906 | 4,159 |
|
||||
| [v0.10.26](https://github.com/laurent22/joplin/releases/tag/v0.10.26) | 2017-11-29T16:02:17Z | 307 | 756 | 291 | 1,354 |
|
||||
| [v0.10.25](https://github.com/laurent22/joplin/releases/tag/v0.10.25) | 2017-11-24T14:27:49Z | 259 | 752 | 6,780 | 7,791 |
|
||||
| [v0.10.28](https://github.com/laurent22/joplin/releases/tag/v0.10.28) | 2017-11-30T01:07:46Z | 1,492 | 1,761 | 908 | 4,161 |
|
||||
| [v0.10.26](https://github.com/laurent22/joplin/releases/tag/v0.10.26) | 2017-11-29T16:02:17Z | 307 | 757 | 291 | 1,355 |
|
||||
| [v0.10.25](https://github.com/laurent22/joplin/releases/tag/v0.10.25) | 2017-11-24T14:27:49Z | 259 | 752 | 6,784 | 7,795 |
|
||||
| [v0.10.23](https://github.com/laurent22/joplin/releases/tag/v0.10.23) | 2017-11-21T19:38:41Z | 247 | 714 | 67 | 1,028 |
|
||||
| [v0.10.22](https://github.com/laurent22/joplin/releases/tag/v0.10.22) | 2017-11-20T21:45:57Z | 192 | 700 | 50 | 942 |
|
||||
| [v0.10.22](https://github.com/laurent22/joplin/releases/tag/v0.10.22) | 2017-11-20T21:45:57Z | 192 | 701 | 50 | 943 |
|
||||
| [v0.10.21](https://github.com/laurent22/joplin/releases/tag/v0.10.21) | 2017-11-18T00:53:15Z | 170 | 693 | 44 | 907 |
|
||||
| [v0.10.20](https://github.com/laurent22/joplin/releases/tag/v0.10.20) | 2017-11-17T17:18:25Z | 161 | 706 | 56 | 923 |
|
||||
| [v0.10.20](https://github.com/laurent22/joplin/releases/tag/v0.10.20) | 2017-11-17T17:18:25Z | 163 | 706 | 57 | 926 |
|
||||
| [v0.10.19](https://github.com/laurent22/joplin/releases/tag/v0.10.19) | 2017-11-20T18:59:48Z | 183 | 707 | 55 | 945 |
|
95
readme/apps/joplin_server_business.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Joplin Server Business
|
||||
|
||||
<div style="overflow: auto;">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/joplin_server_business/main.png" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
|
||||
|
||||
Joplin Server Business is a synchronisation server that you can install on your own infrastructure, so that your data remains private and secure within your business.
|
||||
|
||||
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)
|
||||
|
||||
</div>
|
||||
|
||||
## Smart teamwork with Joplin Server
|
||||
|
||||
### Self-host to keep your data within your organisation
|
||||
|
||||
<div style="overflow: auto;">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/joplin_server_business/self_host.jpg" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
|
||||
|
||||
The data is hosted on your own server, giving you full control over it and ensuring it stays within your organisation.
|
||||
|
||||
</div>
|
||||
|
||||
### Share and collaborate on a notebook
|
||||
|
||||
<div style="overflow: auto;">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/joplin_server_business/share.jpg" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
|
||||
|
||||
Our service allows you to share notes and documents across unlimited devices. Create and modify teams to manage projects and planning.
|
||||
|
||||
</div>
|
||||
|
||||
### Publish notes to the internet
|
||||
|
||||
<div style="overflow: auto;">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/joplin_server_business/publish.jpg" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
|
||||
|
||||
You can publish a note so that it can be viewed in a browser by your colleagues and customers. The note can be available publicly on the internet or remain within your intranet.
|
||||
|
||||
</div>
|
||||
|
||||
### Manage multiples users and teams
|
||||
|
||||
<div style="overflow: auto;">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/joplin_server_business/teams.jpg" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
|
||||
|
||||
Using Joplin Server Business you can create and manage teams of users. Each team can collaborate on notebooks and notes and share information.
|
||||
|
||||
</div>
|
||||
|
||||
## By choosing Joplin Server Business your organisation benefits also from other features including:
|
||||
|
||||
### End-to-end encryption
|
||||
|
||||
Activate encryption to protect your data and secure communications across teams.
|
||||
|
||||
### Web clipper
|
||||
|
||||
Capture web pages and screenshots and save them as notes in Joplin.
|
||||
|
||||
### Open source code
|
||||
|
||||
Our desktop and mobile applications, as well as the end-to-end technology, are fully open source, ensuring transparency and increased security.
|
||||
|
||||
### Synchronization across devices
|
||||
|
||||
Securely synchronise your data across multiple devices - including iOS, Android, Windows, macOS and Linux.
|
||||
|
||||
### Customise it
|
||||
|
||||
Customise the app with plugins, custom themes and multiple text editors (Rich Text or Markdown). Or create your own company-specific workflow by developing scripts and plugins using the Extension API.
|
||||
|
||||
### Open source code
|
||||
|
||||
Our desktop and mobile applications, as well as the end-to-end technology, are fully open source, ensuring transparency and increased security.
|
||||
|
||||
### Multimedia notes (PDF, images, etc.)
|
||||
|
||||
Keep all your resources in one place. Save and share images, PDFs, videos, audio files and math expressions.
|
||||
|
||||
## Did you know that there are over 150 plugins available for Joplin products ?
|
||||
|
||||
[Go to the plugin website](https://joplinapp.org/plugins/)
|
||||
|
||||
## Ready to give it a try ?
|
||||
|
||||
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)
|
@@ -27,7 +27,7 @@ The install and update script supports the [following flags](https://github.com/
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
---|---|---
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' style="max-height: 40px;" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the [APK file](https://objects.joplinusercontent.com/v3.3.9/joplin-v3.3.9.apk?source=JoplinWebsite&type=New)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' style="max-height: 40px;" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the [APK file](https://objects.joplinusercontent.com/v3.3.11/joplin-v3.3.11.apk?source=JoplinWebsite&type=New)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' style="max-height: 40px;" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
|
76
yarn.lock
@@ -9468,7 +9468,7 @@ __metadata:
|
||||
jest: "npm:29.7.0"
|
||||
jest-environment-jsdom: "npm:29.7.0"
|
||||
json-stringify-safe: "npm:5.0.1"
|
||||
katex: "npm:0.16.21"
|
||||
katex: "npm:0.16.22"
|
||||
markdown-it: "npm:13.0.2"
|
||||
markdown-it-abbr: "npm:1.0.4"
|
||||
markdown-it-anchor: "npm:5.3.0"
|
||||
@@ -9529,7 +9529,7 @@ __metadata:
|
||||
jquery: "npm:3.7.1"
|
||||
jsdom: "npm:25.0.1"
|
||||
knex: "npm:3.1.0"
|
||||
koa: "npm:2.16.0"
|
||||
koa: "npm:2.16.1"
|
||||
ldapts: "npm:7.3.3"
|
||||
markdown-it: "npm:13.0.2"
|
||||
mustache: "npm:4.2.0"
|
||||
@@ -9538,7 +9538,7 @@ __metadata:
|
||||
node-os-utils: "npm:1.3.7"
|
||||
nodemailer: "npm:6.10.0"
|
||||
nodemon: "npm:3.1.9"
|
||||
pg: "npm:8.13.3"
|
||||
pg: "npm:8.14.1"
|
||||
pm2: "npm:5.4.3"
|
||||
pretty-bytes: "npm:5.6.0"
|
||||
prettycron: "npm:0.10.0"
|
||||
@@ -9625,7 +9625,7 @@ __metadata:
|
||||
jest: "npm:29.7.0"
|
||||
jest-expect-message: "npm:1.1.3"
|
||||
knex: "npm:3.1.0"
|
||||
koa: "npm:2.16.0"
|
||||
koa: "npm:2.16.1"
|
||||
koa-body: "npm:6.0.1"
|
||||
pg-boss: "npm:10.1.6"
|
||||
sqlite3: "npm:5.1.6"
|
||||
@@ -9651,7 +9651,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@adobe/css-tools": "npm:4.4.2"
|
||||
"@rollup/plugin-commonjs": "npm:28.0.3"
|
||||
"@rollup/plugin-node-resolve": "npm:15.3.1"
|
||||
"@rollup/plugin-node-resolve": "npm:16.0.1"
|
||||
"@rollup/plugin-replace": "npm:6.0.2"
|
||||
browserify: "npm:14.5.0"
|
||||
html-entities: "npm:1.4.0"
|
||||
@@ -12428,9 +12428,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/plugin-node-resolve@npm:15.3.1":
|
||||
version: 15.3.1
|
||||
resolution: "@rollup/plugin-node-resolve@npm:15.3.1"
|
||||
"@rollup/plugin-node-resolve@npm:16.0.1":
|
||||
version: 16.0.1
|
||||
resolution: "@rollup/plugin-node-resolve@npm:16.0.1"
|
||||
dependencies:
|
||||
"@rollup/pluginutils": "npm:^5.0.1"
|
||||
"@types/resolve": "npm:1.20.2"
|
||||
@@ -12442,7 +12442,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
checksum: 10/874494c0daca8fb0d633a237dd9df0d30609b374326e57508710f2b6d7ddaa93d203d8daa0257960b2b6723f56dfec1177573126f31ff9604700303b6f5fdbe3
|
||||
checksum: 10/88fee8c003a5730cca2c06edd200ec6a46c7ab28bed3a99aea6d3070f34f980f575fcbea906946579e41b0be6fd7a2fbc24cdf0ca24f172a555f130726915d8b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -33105,14 +33105,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"katex@npm:0.16.21":
|
||||
version: 0.16.21
|
||||
resolution: "katex@npm:0.16.21"
|
||||
"katex@npm:0.16.22":
|
||||
version: 0.16.22
|
||||
resolution: "katex@npm:0.16.22"
|
||||
dependencies:
|
||||
commander: "npm:^8.3.0"
|
||||
bin:
|
||||
katex: cli.js
|
||||
checksum: 10/db1094c528972ffb881c64969e87cbca465d21f918f4dad8bfe583f68e1bd601438eda3d79e8d74bc7ccc14e7b76616a9053bb21945749a30a73bc68f20e721b
|
||||
checksum: 10/fdb8667d9aa971154502b120ba340766754d202e3d3e322aca0a96de27032ad2dbb8a7295d798d310cd7ce4ddd21ed1f3318895541b61c9b4fdf611166589e02
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -33339,9 +33339,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"koa@npm:2.16.0":
|
||||
version: 2.16.0
|
||||
resolution: "koa@npm:2.16.0"
|
||||
"koa@npm:2.16.1":
|
||||
version: 2.16.1
|
||||
resolution: "koa@npm:2.16.1"
|
||||
dependencies:
|
||||
accepts: "npm:^1.3.5"
|
||||
cache-content-type: "npm:^1.0.0"
|
||||
@@ -33366,7 +33366,7 @@ __metadata:
|
||||
statuses: "npm:^1.5.0"
|
||||
type-is: "npm:^1.6.16"
|
||||
vary: "npm:^1.1.2"
|
||||
checksum: 10/88284e5da49cd54a2db663c818f5370d00f32b6aefbe5ecfc75bdaf7937d3b08cfbb884d07564b8e2b856dfe74d930997a6bdca2e2090dc2bfae0fa8af56a214
|
||||
checksum: 10/f33b95227e48bffd3a682996e6cf72c4ae2992671529c6c914b76d28172219c9cbd8201b16cc028dc25fafc8f1dc9391a6e7e045740a10ee7d89a5631031a974
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -33828,9 +33828,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lint-staged@npm:15.4.3":
|
||||
version: 15.4.3
|
||||
resolution: "lint-staged@npm:15.4.3"
|
||||
"lint-staged@npm:15.5.0":
|
||||
version: 15.5.0
|
||||
resolution: "lint-staged@npm:15.5.0"
|
||||
dependencies:
|
||||
chalk: "npm:^5.4.1"
|
||||
commander: "npm:^13.1.0"
|
||||
@@ -33844,7 +33844,7 @@ __metadata:
|
||||
yaml: "npm:^2.7.0"
|
||||
bin:
|
||||
lint-staged: bin/lint-staged.js
|
||||
checksum: 10/14a6a9cb9b5e8027b1347cb24e114839d618d343d5c724c26def7d45ca9b9a9b813b585531c68f5a3d13332407c2dba198987a73f0350df483d99a876ba69c60
|
||||
checksum: 10/5873584649c5f840b990036c20abd4b58d6b1313dad5505627b4d0cc077f0ec8ac0d6cf4cf4d959e66e0ab085db384bb12dce9490ff29217bf4ed96d0442ed51
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -39269,7 +39269,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pg-pool@npm:^3.10.0, pg-pool@npm:^3.7.1":
|
||||
"pg-pool@npm:^3.10.0":
|
||||
version: 3.10.0
|
||||
resolution: "pg-pool@npm:3.10.0"
|
||||
peerDependencies:
|
||||
@@ -39278,13 +39278,29 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pg-protocol@npm:^1.10.0, pg-protocol@npm:^1.7.1":
|
||||
"pg-pool@npm:^3.8.0":
|
||||
version: 3.10.1
|
||||
resolution: "pg-pool@npm:3.10.1"
|
||||
peerDependencies:
|
||||
pg: ">=8.0"
|
||||
checksum: 10/b389a714be59ebe53ec412cbff513191cc0b7a203faa5d26416b6a038cafdfe30fbf1a5936b77bb76109c49bd7c4a116870a5a46a45796b1b34c96f016d7fbe2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pg-protocol@npm:^1.10.0":
|
||||
version: 1.10.0
|
||||
resolution: "pg-protocol@npm:1.10.0"
|
||||
checksum: 10/975184d9f67dd2325afc8b5e79008c39bbdf6baf43db1158a90a9c624c86d0ca51cff68031759e196739d2e04b90a6a4749b42206ab7b9aca03a25243a7c2094
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pg-protocol@npm:^1.8.0":
|
||||
version: 1.10.3
|
||||
resolution: "pg-protocol@npm:1.10.3"
|
||||
checksum: 10/31da85319084c03f403efee7accce9786964df82a7feb60e6bd77b71f1e622c74a2a644a2bc434389d0ab92e5abdeedea69ebdb53b1897d9f01d2a1f51a8a2fe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pg-types@npm:2.2.0, pg-types@npm:^2.1.0":
|
||||
version: 2.2.0
|
||||
resolution: "pg-types@npm:2.2.0"
|
||||
@@ -39298,14 +39314,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pg@npm:8.13.3":
|
||||
version: 8.13.3
|
||||
resolution: "pg@npm:8.13.3"
|
||||
"pg@npm:8.14.1":
|
||||
version: 8.14.1
|
||||
resolution: "pg@npm:8.14.1"
|
||||
dependencies:
|
||||
pg-cloudflare: "npm:^1.1.1"
|
||||
pg-connection-string: "npm:^2.7.0"
|
||||
pg-pool: "npm:^3.7.1"
|
||||
pg-protocol: "npm:^1.7.1"
|
||||
pg-pool: "npm:^3.8.0"
|
||||
pg-protocol: "npm:^1.8.0"
|
||||
pg-types: "npm:^2.1.0"
|
||||
pgpass: "npm:1.x"
|
||||
peerDependencies:
|
||||
@@ -39316,7 +39332,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
pg-native:
|
||||
optional: true
|
||||
checksum: 10/be1be61fa46f7ccc3441794e390c41fc548f1bbee9744e3e7fae00b3d91e3974c4c51e25c1013075ec3a289d9290cd01ee926357e68ba20fcbb15308dbdef87c
|
||||
checksum: 10/45f2d5719fd74a6a4784c5115c0ff482af92d1e5b101bf423160b6a983e37cc2fad4a7eea2a06f27e6f8bdb8abce23486d2d522c8c52c90f68a2bc897f0553c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -43725,7 +43741,7 @@ __metadata:
|
||||
http-server: "npm:14.1.1"
|
||||
husky: "npm:9.1.7"
|
||||
lerna: "npm:3.22.1"
|
||||
lint-staged: "npm:15.4.3"
|
||||
lint-staged: "npm:15.5.0"
|
||||
madge: "npm:8.0.0"
|
||||
node-gyp: "npm:9.4.1"
|
||||
nodemon: "npm:3.1.9"
|
||||
|