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

Compare commits

...

51 Commits

Author SHA1 Message Date
Laurent Cozic
cea1aeac4b Android 2.9.3 2022-10-07 12:13:34 +01:00
mrkaato0
13ee1c89ea Update fi_FI.po (#6922) 2022-10-07 11:50:07 +01:00
Laurent Cozic
f01ec941b7 Server v2.9.1 2022-10-07 11:48:00 +01:00
Laurent Cozic
0853521bc9 Server: Update email templates 2022-10-06 11:40:11 +01:00
Joplin Bot
e484671a08 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-10-04 12:27:50 +00:00
Joplin Bot
50253d00e7 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-10-04 06:31:07 +00:00
Self Not Found
5364965a69 Desktop: Fixes #6257: Fixed the missing format when pasting text by Ctrl+V in Rich Text editor (#6901) 2022-10-01 15:35:54 +01:00
Self Not Found
50baad3c04 Mobile: Show client ID in log (#6897) 2022-09-30 17:38:22 +01:00
ScriptInfra
cf219762c9 Doc: Update faq.md (#6879) 2022-09-30 17:32:24 +01:00
Laurent Cozic
9e27b0881f Doc: Info about eslint 2022-09-30 17:32:01 +01:00
Laurent Cozic
44a96f347a Tools: Add eslint rule prefer-await-to-then 2022-09-30 17:32:00 +01:00
Self Not Found
cc6620a7e1 Desktop: Fixes #6630: Made autoMatchBraces work on CJK characters (#6858) 2022-09-30 17:03:45 +01:00
asrient
29f1abb666 Desktop: Remove page number box from new PDF Viewer (#6846) 2022-09-30 17:01:55 +01:00
Laurent Cozic
9781a33419 Update CONTRIBUTING.md 2022-09-30 16:19:09 +01:00
Laurent Cozic
0954794195 Chore: Removed build file 2022-09-30 15:22:51 +01:00
Laurent Cozic
a996375b88 Mobile: Fixes #6898: Fixed crash when trying to move note to notebook 2022-09-30 12:13:29 +01:00
Laurent Cozic
129ac1829d Chore: Restore accidentally deleted files 2022-09-30 12:07:26 +01:00
Laurent Cozic
44e60bdda9 Revert: Mobile: Add note bar (#6772)
Revert commit dfd95f8385
Due to UX issues.
Ref https://discourse.joplinapp.org/t/25775/30
2022-09-30 11:46:26 +01:00
Joplin Bot
afc34b44c8 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-09-20 18:24:07 +00:00
Joplin Bot
e08c74ae08 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-09-20 12:27:32 +00:00
Laurent Cozic
e5c669dc7a Doc: Mention that we do not offer bounties 2022-09-20 12:15:13 +03:00
Helmut K. C. Tessarek
f4a7f5914e All: Update Mermaid 8.13.9 to 9.1.7 (#6849) 2022-09-18 21:22:41 +01:00
Self Not Found
62eee4df56 Desktop: Fixes #6860: Made "Open profile directory" work on Windows (#6861) 2022-09-17 20:19:12 +01:00
Joplin Bot
c16445bc2f Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-09-16 06:49:50 +00:00
Self Not Found
e05c5598a0 Mobile: Increase the attachment size limit to 200MB (#6848) 2022-09-14 12:21:21 +01:00
Mayank Bondre
66c9ee0a1a Desktop: Fix missing plugin file error and missing setting key error in dev mode (#6827) 2022-09-12 16:08:06 +01:00
asrient
d07788607c Desktop: Fix pdf text blurry (#6843) 2022-09-12 16:07:39 +01:00
Laurent Cozic
907dc7601b Desktop release v2.9.8 2022-09-12 14:12:39 +01:00
Laurent Cozic
4b9adcde04 Tools: Restore Windows build on CI 2022-09-12 14:12:07 +01:00
Henry Heino
9f3a4e0d99 Mobile: Fix multiple webview instances (#6841) 2022-09-12 10:46:12 +01:00
Henry Heino
ea14488dc3 Tools: Update Joplin plugin generator to Webpack 5, TypeScript 4.8 (#6826) 2022-09-12 10:44:40 +01:00
Laurent Cozic
f59d29f1c5 Desktop release v2.9.7 2022-09-11 20:07:47 +01:00
Laurent Cozic
0a9e919ac7 Merge branch 'release-2.9' into dev 2022-09-11 20:07:21 +01:00
Laurent Cozic
f11b6e8fa9 Tools: Remove desktop Windows build for now (broken due to invalid cert) 2022-09-11 20:06:49 +01:00
Laurent Cozic
167560ff6f Desktop release v2.9.6 2022-09-11 18:53:38 +01:00
Laurent Cozic
4b4e316bf0 Chore: Remove broken default plugin bundler for now 2022-09-11 18:53:05 +01:00
Self Not Found
7809228bd3 Mobile: Supports attaching multiple files to a note at once (#6831) 2022-09-11 16:58:36 +01:00
Laurent Cozic
540fbbc22c Desktop release v2.9.5 2022-09-11 15:04:00 +01:00
Laurent Cozic
2983d4f1a3 Merge branch 'dev' into release-2.9 2022-09-11 15:03:34 +01:00
asrient
f6a8bf9ea2 Desktop: Add PDF full screen viewer (#6821) 2022-09-11 14:58:32 +01:00
BeeverTeeth
e3ba02281b Doc: Update markdown.md (#6834) 2022-09-10 09:35:46 +01:00
Joplin Bot
295b310079 Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-09-09 18:23:10 +00:00
Henry Heino
62346575f8 iOS: Fixes #6805: Add button to reduce space below markdown toolbar (#6823) 2022-09-09 15:11:58 +01:00
chelstad
0a590b7de9 Doc: Update README to work well with Linode (#6830) 2022-09-09 15:11:03 +01:00
Tolulope Malomo
dfd95f8385 Mobile: Add note bar (#6772) 2022-09-09 15:06:03 +01:00
Retrove
6efe8c171a Chore: Seperate allPossibleCategories to @joplin/lib (#6754) 2022-09-09 15:05:08 +01:00
Philipp Tschannen
a7cdcaf25f Doc: Update e2ee.md (#6833)
Fix typo
2022-09-09 12:40:23 +01:00
Joplin Bot
6277958d6a Doc: Auto-update documentation
Auto-updated using release-website.sh
2022-09-06 18:21:44 +00:00
Laurent Cozic
24b4b879f2 Android 2.9.2 2022-09-01 16:19:03 +01:00
Laurent Cozic
86fbf82d36 Merge branch 'dev' into release-2.9 2022-09-01 11:05:10 +01:00
Laurent Cozic
96982849ce Desktop release v2.9.4 2022-08-18 16:29:00 +01:00
104 changed files with 1532 additions and 393 deletions

View File

@@ -333,6 +333,9 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js.map
packages/app-desktop/gui/MainScreen/commands/openNote.d.ts
packages/app-desktop/gui/MainScreen/commands/openNote.js
packages/app-desktop/gui/MainScreen/commands/openNote.js.map
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.d.ts
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js.map
packages/app-desktop/gui/MainScreen/commands/openTag.d.ts
packages/app-desktop/gui/MainScreen/commands/openTag.js
packages/app-desktop/gui/MainScreen/commands/openTag.js.map
@@ -597,6 +600,9 @@ packages/app-desktop/gui/OneDriveLoginScreen.js.map
packages/app-desktop/gui/PasswordInput/PasswordInput.d.ts
packages/app-desktop/gui/PasswordInput/PasswordInput.js
packages/app-desktop/gui/PasswordInput/PasswordInput.js.map
packages/app-desktop/gui/PdfViewer.d.ts
packages/app-desktop/gui/PdfViewer.js
packages/app-desktop/gui/PdfViewer.js.map
packages/app-desktop/gui/ResizableLayout/MoveButtons.d.ts
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
@@ -921,6 +927,9 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js.map
@@ -2001,6 +2010,9 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js
packages/lib/versionInfo.js.map
packages/pdf-viewer/FullViewer.d.ts
packages/pdf-viewer/FullViewer.js
packages/pdf-viewer/FullViewer.js.map
packages/pdf-viewer/Page.d.ts
packages/pdf-viewer/Page.js
packages/pdf-viewer/Page.js.map
@@ -2025,9 +2037,15 @@ packages/pdf-viewer/hooks/useScaledSize.js.map
packages/pdf-viewer/hooks/useScrollSaver.d.ts
packages/pdf-viewer/hooks/useScrollSaver.js
packages/pdf-viewer/hooks/useScrollSaver.js.map
packages/pdf-viewer/hooks/useVisibleOnSelect.d.ts
packages/pdf-viewer/hooks/useVisibleOnSelect.js
packages/pdf-viewer/hooks/useVisibleOnSelect.js.map
packages/pdf-viewer/main.d.ts
packages/pdf-viewer/main.js
packages/pdf-viewer/main.js.map
packages/pdf-viewer/messageService.d.ts
packages/pdf-viewer/messageService.js
packages/pdf-viewer/messageService.js.map
packages/pdf-viewer/miniViewer.d.ts
packages/pdf-viewer/miniViewer.js
packages/pdf-viewer/miniViewer.js.map
@@ -2037,6 +2055,9 @@ packages/pdf-viewer/pdfSource.test.js.map
packages/pdf-viewer/types.d.ts
packages/pdf-viewer/types.js
packages/pdf-viewer/types.js.map
packages/pdf-viewer/ui/GotoPage.d.ts
packages/pdf-viewer/ui/GotoPage.js
packages/pdf-viewer/ui/GotoPage.js.map
packages/pdf-viewer/ui/IconButtons.d.ts
packages/pdf-viewer/ui/IconButtons.js
packages/pdf-viewer/ui/IconButtons.js.map

View File

@@ -90,6 +90,8 @@ module.exports = {
// Disable because of this: https://github.com/facebook/react/issues/16265
// "react-hooks/exhaustive-deps": "warn",
'promise/prefer-await-to-then': 'error',
// -------------------------------
// Formatting
// -------------------------------
@@ -141,6 +143,7 @@ module.exports = {
'@seiyab/eslint-plugin-react-hooks',
// 'react-hooks',
'import',
'promise',
],
'overrides': [
{

View File

@@ -175,8 +175,8 @@ cd "$ROOT_DIR/packages/app-desktop"
if [[ $GIT_TAG_NAME = v* ]]; then
echo "Step: Building and publishing desktop application..."
cd "$ROOT_DIR/packages/tools"
node bundleDefaultPlugins.js
# cd "$ROOT_DIR/packages/tools"
# node bundleDefaultPlugins.js
cd "$ROOT_DIR/packages/app-desktop"
USE_HARD_LINKS=false yarn run dist
elif [[ $IS_LINUX = 1 ]] && [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then

View File

@@ -5,7 +5,6 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
# Removed windows-2016 for now - discontinued by GitHub
os: [macos-latest, ubuntu-latest, windows-2019]
steps:

21
.gitignore vendored
View File

@@ -321,6 +321,9 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js.map
packages/app-desktop/gui/MainScreen/commands/openNote.d.ts
packages/app-desktop/gui/MainScreen/commands/openNote.js
packages/app-desktop/gui/MainScreen/commands/openNote.js.map
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.d.ts
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js.map
packages/app-desktop/gui/MainScreen/commands/openTag.d.ts
packages/app-desktop/gui/MainScreen/commands/openTag.js
packages/app-desktop/gui/MainScreen/commands/openTag.js.map
@@ -585,6 +588,9 @@ packages/app-desktop/gui/OneDriveLoginScreen.js.map
packages/app-desktop/gui/PasswordInput/PasswordInput.d.ts
packages/app-desktop/gui/PasswordInput/PasswordInput.js
packages/app-desktop/gui/PasswordInput/PasswordInput.js.map
packages/app-desktop/gui/PdfViewer.d.ts
packages/app-desktop/gui/PdfViewer.js
packages/app-desktop/gui/PdfViewer.js.map
packages/app-desktop/gui/ResizableLayout/MoveButtons.d.ts
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
@@ -909,6 +915,9 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js.map
@@ -1989,6 +1998,9 @@ packages/lib/uuid.js.map
packages/lib/versionInfo.d.ts
packages/lib/versionInfo.js
packages/lib/versionInfo.js.map
packages/pdf-viewer/FullViewer.d.ts
packages/pdf-viewer/FullViewer.js
packages/pdf-viewer/FullViewer.js.map
packages/pdf-viewer/Page.d.ts
packages/pdf-viewer/Page.js
packages/pdf-viewer/Page.js.map
@@ -2013,9 +2025,15 @@ packages/pdf-viewer/hooks/useScaledSize.js.map
packages/pdf-viewer/hooks/useScrollSaver.d.ts
packages/pdf-viewer/hooks/useScrollSaver.js
packages/pdf-viewer/hooks/useScrollSaver.js.map
packages/pdf-viewer/hooks/useVisibleOnSelect.d.ts
packages/pdf-viewer/hooks/useVisibleOnSelect.js
packages/pdf-viewer/hooks/useVisibleOnSelect.js.map
packages/pdf-viewer/main.d.ts
packages/pdf-viewer/main.js
packages/pdf-viewer/main.js.map
packages/pdf-viewer/messageService.d.ts
packages/pdf-viewer/messageService.js
packages/pdf-viewer/messageService.js.map
packages/pdf-viewer/miniViewer.d.ts
packages/pdf-viewer/miniViewer.js
packages/pdf-viewer/miniViewer.js.map
@@ -2025,6 +2043,9 @@ packages/pdf-viewer/pdfSource.test.js.map
packages/pdf-viewer/types.d.ts
packages/pdf-viewer/types.js
packages/pdf-viewer/types.js.map
packages/pdf-viewer/ui/GotoPage.d.ts
packages/pdf-viewer/ui/GotoPage.js
packages/pdf-viewer/ui/GotoPage.js.map
packages/pdf-viewer/ui/IconButtons.d.ts
packages/pdf-viewer/ui/IconButtons.js
packages/pdf-viewer/ui/IconButtons.js.map

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 08 Aug 2022 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 08 Aug 2022 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin first meetup on 30 August!]]></title><description><![CDATA[<p>We are glad to announce <a href="https://www.meetup.com/joplin/events/287611873/">the first Joplin Meetup</a> that will take place on 30 August 2022 in London!</p>
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Tue, 06 Sep 2022 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Tue, 06 Sep 2022 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin interview on Website Planet]]></title><description><![CDATA[<p>Website Planet has recently conducted an interview about Joplin - it may give you some insight on the current status of the project, our priorities, and future plans! More on the article page - <a href="https://www.websiteplanet.com/blog/interview-joplin/">Organise Your Thoughts with Open Source Note-Taking App, Joplin</a></p>
]]></description><link>https://joplinapp.org/news/20220906-interview-websiteplanet/</link><guid isPermaLink="false">20220906-interview-websiteplanet</guid><pubDate>Tue, 06 Sep 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin first meetup on 30 August!]]></title><description><![CDATA[<p>We are glad to announce <a href="https://www.meetup.com/joplin/events/287611873/">the first Joplin Meetup</a> that will take place on 30 August 2022 in London!</p>
<p>This is an opportunity to meet other Joplin users as well as some of the main contributors, to discuss the apps, or to ask questions and exchange tips and tricks on how to use the app, develop plugins or contribute to the application. Everybody, technical or not, is welcome!</p>
<p>We will meet at the Old Thameside Inn next to London Bridge. If the weather allows we will be on the terrace outside, if not inside.</p>
<p>More information on the official Meetup page:</p>
@@ -258,7 +259,4 @@
<p>Also many thanks to everyone who voted and contributed to the tagline discussion! It helped narrow down what the tagline should be, along with the equally important description below. If you have any question or notice any issue with the website let me know!</p>
]]></description><link>https://joplinapp.org/news/20210711-095626/</link><guid isPermaLink="false">20210711-095626</guid><pubDate>Sun, 11 Jul 2021 09:56:26 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Poll: What should Joplin tagline be?]]></title><description><![CDATA[<p>Thanks everyone for your tagline suggestions - there were lots of good ideas in there. I've compiled a few of them and create a poll in the forum, so please cast your vote! And if you have any other suggestions on what would make a good tagline, feel free to post over there or here.</p>
<p><a href="https://discourse.joplinapp.org/t/poll-what-should-joplin-tagline-be/18487">https://discourse.joplinapp.org/t/poll-what-should-joplin-tagline-be/18487</a></p>
]]></description><link>https://joplinapp.org/news/20210706-140228/</link><guid isPermaLink="false">20210706-140228</guid><pubDate>Tue, 06 Jul 2021 14:02:28 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Any ideas for a Joplin tagline?]]></title><description><![CDATA[<p>I'm going to update the website front page to better showcase the application. I have most of the sections right, but the part I'm still not sure about is the top tagline, so I'm wondering if anyone had any suggestion about it?</p>
<p>From what I can see on Google Keep or Evernote for example it should be something like &quot;Use our app to get X or Y benefit&quot;, it should be a sentence that directly speaks to the user essentially.</p>
<p>So far I have &quot;Your notes, anywhere you are&quot; but I'm not certain that's particularly inspiring. Any other idea about what tagline could be used?</p>
]]></description><link>https://joplinapp.org/news/20210705-094247/</link><guid isPermaLink="false">20210705-094247</guid><pubDate>Mon, 05 Jul 2021 09:42:47 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
]]></description><link>https://joplinapp.org/news/20210706-140228/</link><guid isPermaLink="false">20210706-140228</guid><pubDate>Tue, 06 Jul 2021 14:02:28 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>

View File

@@ -31,7 +31,7 @@ Joplin is available in multiple languages thanks to the help of its users. You c
If you want to start contributing to the project's code, please follow these guidelines before creating a pull request:
- Explain WHY you want to add this change. Explain it inside the pull request and you may link to an issue for additional information, but the PR should give a clear overview of why you want to add this.
- The top post of the pull request should contain a full, self-contained explanation of the feature: what it does, how it does it, with examples of usage and screenshots. Also explain why you want to add this - what problem does it solve. Do not simply add a text `Implement feature #4345` or link to forum posts, because the information there will most likely be outdated or confusing (multiple discussions and opinions). The pull request needs to be self-contained.
- Bug fixes are always welcome. Start by reviewing the [list of bugs](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
- A good way to easily start contributing is to pick and work on a [good first issue](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). We try to make these issues as clear as possible and provide basic info on how the code should be changed, and if something is unclear feel free to ask for more information on the issue.
- Before adding a new feature, ask about it in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue) or the [Joplin Forum](https://discourse.joplinapp.org/), or check if existing discussions exist to make sure the new functionality is desired.

View File

@@ -346,8 +346,8 @@ If you provide a configuration and you receive "success!" on the "check config"
- Force Path Style: unchecked
### Linode
- URL: https://<region>.linodeobjects.com
- Region: empty
- URL: https://regionName.linodeobjects.com (regionName is the region on the URL provided by Linode; this URL is also the same as the URL provided by Linode with the bucket name removed)
- Region: Anything you want to type, can't be left empty
- Force Path Style: unchecked
### UpCloud

View File

@@ -9,3 +9,7 @@ Only the latest version is supported with security updates.
Please [contact support](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/AdresseSupport.png) **with a proof of concept** that shows the security vulnerability. Please do not contact us without this proof of concept, as we cannot fix anything without this.
For general opinions on what makes an app more or less secure, please use the forum.
## Bounty
We **do not** offer a bounty for discovering vulnerabilities, please do not ask. We can however credit you and link to your website in the changelog and release announcement.

View File

@@ -69,6 +69,7 @@
"eslint": "^8.22.0",
"eslint-interactive": "^10.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.0.1",
"eslint-plugin-react": "^7.30.1",
"fs-extra": "^8.1.0",
"glob": "^7.1.6",

View File

@@ -124,6 +124,7 @@ async function handleAutocompletionPromise(line) {
return line;
}
function handleAutocompletion(str, callback) {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
handleAutocompletionPromise(str).then(function(res) {
callback(undefined, res);
});

View File

@@ -36,6 +36,7 @@ async function createClients() {
const client = createClient(clientId);
promises.push(fs.remove(client.profileDir));
promises.push(
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
execCommand(client, 'config sync.target 2').then(() => {
return execCommand(client, `config sync.2.path ${syncDir}`);
})
@@ -2324,10 +2325,12 @@ async function main() {
clients[clientId].activeCommandCount++;
execRandomCommand(clients[clientId])
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.catch(error => {
logger.info(`Client ${clientId}:`);
logger.error(error);
})
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then(r => {
if (r) {
logger.info(`Client ${clientId}:\n${r.trim()}`);

View File

@@ -50,6 +50,7 @@ export default class PluginRunner extends BasePluginRunner {
const callId = `${pluginId}::${path}::${uuid.createNano()}`;
this.activeSandboxCalls_[callId] = true;
const promise = executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler);
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
promise.finally(() => {
delete this.activeSandboxCalls_[callId];
});

View File

@@ -1,7 +1,7 @@
import { installDefaultPlugins, getDefaultPluginsInstallState, setSettingsForDefaultPlugins, checkPreInstalledDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
import PluginRunner from '../../../app/services/plugins/PluginRunner';
import { pathExists } from 'fs-extra';
import { setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
import { checkThrow, setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
import PluginService, { defaultPluginSetting, DefaultPluginsInfo, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import Setting from '@joplin/lib/models/Setting';
@@ -213,4 +213,57 @@ describe('defaultPluginsUtils', function() {
await service.destroy();
});
it('should not throw error on missing setting key', async () => {
const service = newPluginService();
const pluginScript = `
/* joplin-manifest:
{
"id": "io.github.jackgruber.backup",
"manifest_version": 1,
"app_min_version": "1.4",
"name": "JS Bundle test",
"version": "1.0.0"
}
*/
joplin.plugins.register({
onStart: async function() {
await joplin.settings.registerSettings({
path: {
value: "initial-path",
type: 2,
section: "backupSection",
public: true,
label: "Backup path",
},
})
},
});`;
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
await service.runPlugin(plugin);
const defaultPluginsInfo: DefaultPluginsInfo = {
'io.github.jackgruber.backup': {
version: '1.0.2',
settings: {
'path': `${Setting.value('profileDir')}`,
'missing-key1': 'someValue',
},
},
'plugin.calebjohn.rich-markdown': {
version: '0.8.3',
settings: {
'missing-key2': 'someValue',
},
},
};
Setting.setValue('installedDefaultPlugins', ['']);
expect(checkThrow(() => setSettingsForDefaultPlugins(defaultPluginsInfo))).toBe(false);
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`);
await service.destroy();
});
});

View File

@@ -278,8 +278,8 @@ class Application extends BaseApplication {
checkPreInstalledDefaultPlugins(defaultPluginsId, pluginSettings);
try {
const pluginsDir = path.join(bridge().buildDir(), 'defaultPlugins');
pluginSettings = await installDefaultPlugins(service, pluginsDir, defaultPluginsId, pluginSettings);
const defaultPluginsDir = path.join(bridge().buildDir(), 'defaultPlugins');
pluginSettings = await installDefaultPlugins(service, defaultPluginsDir, defaultPluginsId, pluginSettings);
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
}
@@ -502,6 +502,7 @@ class Application extends BaseApplication {
if (Setting.value('env') === 'dev') {
void AlarmService.updateAllNotifications();
} else {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
void reg.scheduleSync(1000).then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.

View File

@@ -246,7 +246,7 @@ export class Bridge {
}
async openItem(fullPath: string) {
return require('electron').shell.openPath(fullPath);
return require('electron').shell.openPath(toSystemSlashes(fullPath));
}
screen() {

View File

@@ -45,6 +45,7 @@ class ClipperConfigScreenComponent extends React.Component {
if (confirm(_('Are you sure you want to renew the authorisation token?'))) {
void EncryptionService.instance()
.generateApiToken()
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then((token) => {
Setting.setValue('api.token', token);
});

View File

@@ -15,6 +15,7 @@ import * as openFolder from './openFolder';
import * as openFolderDialog from './openFolderDialog';
import * as openItem from './openItem';
import * as openNote from './openNote';
import * as openPdfViewer from './openPdfViewer';
import * as openTag from './openTag';
import * as print from './print';
import * as renameFolder from './renameFolder';
@@ -55,6 +56,7 @@ const index:any[] = [
openFolderDialog,
openItem,
openNote,
openPdfViewer,
openTag,
print,
renameFolder,

View File

@@ -0,0 +1,28 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import Resource from '@joplin/lib/models/Resource';
export const declaration: CommandDeclaration = {
name: 'openPdfViewer',
label: () => _('Open PDF viewer'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext, resourceId: string, pageNo: number) => {
const resource = await Resource.load(resourceId);
if (!resource) throw new Error(`No such resource: ${resourceId}`);
if (resource.mime !== 'application/pdf') throw new Error(`Not a PDF: ${resource.mime}`);
console.log('Opening PDF', resource);
context.dispatch({
type: 'DIALOG_OPEN',
name: 'pdfViewer',
props: {
resource,
pageNo: pageNo,
},
});
},
};
};

View File

@@ -848,6 +848,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
function renderEditor() {
const matchBracesOptions = Setting.value('editor.autoMatchingBraces') ? { override: true, pairs: '<>()[]{}\'\'""‘’“”()《》「」『』【】〔〕〖〗〘〙〚〛' } : false;
return (
<div style={cellEditorStyle}>
<Editor
@@ -858,7 +860,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
codeMirrorTheme={styles.editor.codeMirrorTheme}
style={styles.editor}
readOnly={props.visiblePanes.indexOf('editor') < 0}
autoMatchBraces={Setting.value('editor.autoMatchingBraces')}
autoMatchBraces={matchBracesOptions}
keyMap={props.keyboardMode}
plugins={props.plugins}
onChange={codeMirror_change}

View File

@@ -86,7 +86,7 @@ export interface EditorProps {
style: any;
codeMirrorTheme: any;
readOnly: boolean;
autoMatchBraces: boolean;
autoMatchBraces: boolean | object;
keyMap: string;
plugins: PluginStates;
onChange: any;

View File

@@ -1008,7 +1008,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true }));
editor.insertContent(result.html);
} else { // Paste regular text
const pastedHtml = event.clipboardData.getData('text/html');
// event.clipboardData.getData('text/html') wraps the content with <html><body></body></html>,
// which seems to be not supported in editor.insertContent().
const pastedHtml = clipboard.readHTML();
if (pastedHtml) { // Handles HTML
const modifiedHtml = await processPastedHtml(pastedHtml);
editor.insertContent(modifiedHtml);

View File

@@ -52,6 +52,8 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
void CommandService.instance().execute(commandName, ...commandArgs);
} else if (msg === 'postMessageService.message') {
void PostMessageService.instance().postMessage(arg0);
} else if (msg === 'openPdfViewer') {
await CommandService.instance().execute('openPdfViewer', arg0.resourceId, arg0.pageNo);
} else {
await CommandService.instance().execute('openItem', msg);
// bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));

View File

@@ -0,0 +1,101 @@
import * as React from 'react';
import { useCallback, useRef, useEffect } from 'react';
import Resource from '@joplin/lib/models/Resource';
import bridge from '../services/bridge';
import contextMenu from './NoteEditor/utils/contextMenu';
import { ContextMenuItemType, ContextMenuOptions } from './NoteEditor/utils/contextMenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import styled from 'styled-components';
import { themeStyle } from '@joplin/lib/theme';
const Window = styled.div`
height: 100%;
width: 100%;
position: fixed;
top: 0px;
left: 0px;
z-index: 999;
background-color: ${(props: any) => props.theme.backgroundColor};
color: ${(props: any) => props.theme.color};
`;
const IFrame = styled.iframe`
height: 100%;
width: 100%;
border: none;
`;
interface Props {
themeId: number;
dispatch: Function;
resource: any;
pageNo: number;
}
export default function PdfViewer(props: Props) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const onClose = useCallback(() => {
props.dispatch({
type: 'DIALOG_CLOSE',
name: 'pdfViewer',
});
}, [props.dispatch]);
const openExternalViewer = useCallback(async () => {
await CommandService.instance().execute('openItem', `joplin://${props.resource.id}`);
}, [props.resource.id]);
const textSelected = useCallback(async (text: string) => {
if (!text) return;
const itemType = ContextMenuItemType.Text;
const menu = await contextMenu({
itemType,
resourceId: null,
filename: null,
mime: 'text/plain',
textToCopy: text,
linkToCopy: null,
htmlToCopy: '',
insertContent: () => { console.warn('insertContent() not implemented'); },
} as ContextMenuOptions, props.dispatch);
menu.popup(bridge().window());
}, [props.dispatch]);
useEffect(() => {
const onMessage_ = async (event: any) =>{
if (!event.data || !event.data.name) {
return;
}
if (event.data.name === 'close') {
onClose();
} else if (event.data.name === 'externalViewer') {
await openExternalViewer();
} else if (event.data.name === 'textSelected') {
await textSelected(event.data.text);
} else {
console.error('Unknown event received', event.data.name);
}
};
const iframe = iframeRef.current;
iframe.contentWindow.addEventListener('message', onMessage_);
return () => {
iframe.contentWindow.removeEventListener('message', onMessage_);
};
}, [onClose, openExternalViewer, textSelected]);
const theme = themeStyle(props.themeId);
return (
<Window theme={theme}>
<IFrame src="./vendor/lib/@joplin/pdf-viewer/index.html" x-url={Resource.fullPath(props.resource)}
x-appearance={theme.appearance} ref={iframeRef}
x-title={props.resource.title}
x-anchorpage={props.pageNo}
x-type="full"></IFrame>
</Window>
);
}

View File

@@ -174,9 +174,11 @@ class ResourceScreenComponent extends React.Component<Props, State> {
return;
}
Resource.delete(resource.id)
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.catch((error: Error) => {
bridge().showErrorMessageBox(error.message);
})
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.finally(() => {
void this.reloadResources(this.state.sorting);
});

View File

@@ -22,6 +22,7 @@ import Dialog from './Dialog';
import SyncWizardDialog from './SyncWizard/Dialog';
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
import EditFolderDialog from './EditFolderDialog/Dialog';
import PdfViewer from './PdfViewer';
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
const { ImportScreen } = require('./ImportScreen.min.js');
const { ResourceScreen } = require('./ResourceScreen.js');
@@ -75,6 +76,11 @@ const registeredDialogs: Record<string, RegisteredDialog> = {
return <EditFolderDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
},
},
pdfViewer: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <PdfViewer key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
},
},
};
const GlobalStyle = createGlobalStyle`

View File

@@ -579,6 +579,17 @@
}
}
ipc.textSelected = function(event) {
ipcProxySendToHost('contextMenu', {
type: 'text',
textToCopy: event.text,
});
}
ipc.openPdfViewer = function(event) {
ipcProxySendToHost('openPdfViewer', { resourceId: event.resourceId, mime: 'application/pdf', pageNo: event.pageNo || 1 });
}
window.addEventListener('hashchange', webviewLib.logEnabledEventHandler(e => {
if (!window.location.hash) return;

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.9.4",
"version": "2.9.8",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

View File

@@ -146,8 +146,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097670
versionName "2.9.2"
versionCode 2097671
versionName "2.9.3"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -122,7 +122,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
};
const itemRenderer = (item: DropdownListItem) => {
const key = item.value.toString();
const key = item.value ? item.value.toString() : '__null'; // The top item ("Move item to notebook...") has a null value.
return (
<TouchableOpacity
style={itemWrapperStyle}

View File

@@ -36,6 +36,10 @@ type OnFileUpdateCallback = (event: SourceFileUpdateEvent)=> void;
interface Props {
themeId: number;
// A name to be associated with the WebView (e.g. NoteEditor)
// This name should be unique.
webviewInstanceId: string;
// If HTML is still being loaded, [html] should be an empty string.
html: string;
@@ -81,7 +85,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
useEffect(() => {
let cancelled = false;
async function createHtmlFile() {
const tempFile = `${Setting.value('resourceDir')}/NoteEditor.html`;
const tempFile = `${Setting.value('resourceDir')}/${props.webviewInstanceId}.html`;
await shim.fsDriver().writeFile(tempFile, props.html, 'utf8');
if (cancelled) return;
@@ -110,7 +114,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
return () => {
cancelled = true;
};
}, [props.html, props.onFileUpdate]);
}, [props.html, props.webviewInstanceId, props.onFileUpdate]);
// - `setSupportMultipleWindows` must be `true` for security reasons:
// https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0

View File

@@ -89,6 +89,7 @@ export default function NoteBodyViewer(props: Props) {
return (
<View style={props.style}>
<ExtendedWebView
webviewInstanceId='NoteBodyViewer'
themeId={props.themeId}
style={webViewStyle}
html={html}

View File

@@ -1,7 +1,7 @@
// A toolbar for the markdown editor.
const React = require('react');
import { Platform, StyleSheet, View } from 'react-native';
import { Platform, StyleSheet } from 'react-native';
import { useMemo, useState, useCallback } from 'react';
// See https://oblador.github.io/react-native-vector-icons/ for a list of
@@ -20,6 +20,7 @@ import { ButtonSpec, StyleSheetData } from './types';
import Toolbar from './Toolbar';
import { buttonSize } from './ToolbarButton';
import { Theme } from '@joplin/lib/themes/type';
import ToggleSpaceButton from './ToggleSpaceButton';
type OnAttachCallback = ()=> void;
@@ -277,7 +278,11 @@ const MarkdownToolbar = (props: MarkdownToolbarProps) => {
};
return (
<>
<ToggleSpaceButton
spaceApplicable={ Platform.OS === 'ios' && keyboardVisible }
themeId={props.editorSettings.themeId}
style={styles.container}
>
<Toolbar
styleSheet={styleData}
buttons={[
@@ -299,21 +304,16 @@ const MarkdownToolbar = (props: MarkdownToolbarProps) => {
},
]}
/>
<View style={{
// The keyboard on iOS can overlap the markdown toolbar.
// Add additional padding to prevent this.
height: (
Platform.OS === 'ios' && keyboardVisible ? 16 : 0
),
}}/>
</>
</ToggleSpaceButton>
);
};
const useStyles = (styleProps: any, theme: Theme) => {
return useMemo(() => {
return StyleSheet.create({
container: {
...styleProps,
},
button: {
width: buttonSize,
height: buttonSize,
@@ -348,10 +348,8 @@ const useStyles = (styleProps: any, theme: Theme) => {
// Add a small amount of additional padding for button borders
height: buttonSize + 6,
...styleProps,
},
toolbarContainer: {
maxHeight: '65%',
flexShrink: 1,
},
toolbarContent: {

View File

@@ -0,0 +1,96 @@
// On some devices, the SafeAreaView conflicts with the KeyboardAvoidingView, creating
// additional (or a lack of additional) space at the bottom of the screen. Because this
// is different on different devices, this button allows toggling additional space a the bottom
// of the screen to compensate.
// Works around https://github.com/facebook/react-native/issues/13393 by adding additional
// space below the given component when the keyboard is visible unless a button is pressed.
import Setting from '@joplin/lib/models/Setting';
import { themeStyle } from '@joplin/lib/theme';
import { Theme } from '@joplin/lib/themes/type';
import * as React from 'react';
import { ReactNode, useCallback, useState, useEffect } from 'react';
import { View, ViewStyle } from 'react-native';
import CustomButton from '../../CustomButton';
const AntIcon = require('react-native-vector-icons/AntDesign').default;
interface Props {
children: ReactNode;
spaceApplicable: boolean;
themeId: number;
style?: ViewStyle;
}
const ToggleSpaceButton = (props: Props) => {
const [additionalSpace, setAdditionalSpace] = useState(0);
const [decreaseSpaceBtnVisible, setDecreaseSpaceBtnVisible] = useState(true);
// Some devices need space added, others need space removed.
const additionalPositiveSpace = 14;
const additionalNegativeSpace = -14;
// Switch from adding +14px to -14px.
const onDecreaseSpace = useCallback(() => {
setAdditionalSpace(additionalNegativeSpace);
setDecreaseSpaceBtnVisible(false);
Setting.setValue('editor.mobile.removeSpaceBelowToolbar', true);
}, [setAdditionalSpace, setDecreaseSpaceBtnVisible, additionalNegativeSpace]);
useEffect(() => {
if (Setting.value('editor.mobile.removeSpaceBelowToolbar')) {
onDecreaseSpace();
}
}, [onDecreaseSpace]);
const theme: Theme = themeStyle(props.themeId);
const decreaseSpaceButton = (
<>
<View style={{
height: additionalPositiveSpace,
zIndex: -2,
}} />
<CustomButton
themeId={props.themeId}
description={'Move toolbar to bottom of screen'}
style={{
height: additionalPositiveSpace,
width: '100%',
// Ensure that the icon is near the bottom of the screen,
// and thus invisible on devices where it isn't necessary.
position: 'absolute',
bottom: 0,
// Don't show the button on top of views with content.
zIndex: -1,
alignItems: 'center',
}}
onPress={onDecreaseSpace}
>
<AntIcon name='down' style={{
color: theme.color,
}}/>
</CustomButton>
</>
);
const style: ViewStyle = {
marginBottom: props.spaceApplicable ? additionalSpace : 0,
...props.style,
};
return (
<View style={style}>
{props.children}
{ decreaseSpaceBtnVisible && props.spaceApplicable ? decreaseSpaceButton : null }
</View>
);
};
export default ToggleSpaceButton;

View File

@@ -2,7 +2,7 @@ const React = require('react');
import { _ } from '@joplin/lib/locale';
import { ReactElement, useCallback, useState } from 'react';
import { AccessibilityInfo, LayoutChangeEvent, ScrollView, View } from 'react-native';
import { AccessibilityInfo, LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native';
import ToggleOverflowButton from './ToggleOverflowButton';
import ToolbarButton, { buttonSize } from './ToolbarButton';
import ToolbarOverflowRows from './ToolbarOverflowRows';
@@ -11,6 +11,7 @@ import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
interface ToolbarProps {
buttons: ButtonGroup[];
styleSheet: StyleSheetData;
style?: ViewStyle;
}
// Displays a list of buttons with an overflow menu.
@@ -88,7 +89,7 @@ const Toolbar = (props: ToolbarProps) => {
const styles = props.styleSheet.styles;
const mainButtonRow = (
<View style={styles.toolbarRow}>
{!overflowButtonsVisible ? mainButtons : null }
{ mainButtons }
</View>
);
@@ -101,6 +102,8 @@ const Toolbar = (props: ToolbarProps) => {
// container. As such, we can't base the container's width on the
// size of its content.
width: '100%',
...props.style,
}}
onLayout={onContainerLayout}
>
@@ -111,8 +114,8 @@ const Toolbar = (props: ToolbarProps) => {
visible={overflowButtonsVisible}
onToggleOverflow={onToggleOverflowVisible}
/>
{ !overflowButtonsVisible ? mainButtonRow : null }
</ScrollView>
{ !overflowButtonsVisible ? mainButtonRow : null }
</View>
);
};

View File

@@ -111,6 +111,7 @@ const ToolbarOverflowRows = (props: OverflowPopupProps) => {
style={{
height: props.buttonGroups.length * buttonSize,
flexDirection: 'column',
flexGrow: 1,
}}
onLayout={onContainerLayout}
>

View File

@@ -374,6 +374,7 @@ function NoteEditor(props: Props, ref: any) {
...props.contentStyle,
}}>
<ExtendedWebView
webviewInstanceId='NoteEditor'
themeId={props.themeId}
ref={webviewRef}
html={html}

View File

@@ -41,6 +41,7 @@ const DocumentPicker = require('react-native-document-picker').default;
const ImageResizer = require('react-native-image-resizer').default;
const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
const ImagePicker = require('react-native-image-picker').default;
import { ImagePickerResponse } from 'react-native-image-picker';
import SelectDateTimeDialog from '../SelectDateTimeDialog';
import ShareExtension from '../../utils/ShareExtension.js';
import CameraView from '../CameraView';
@@ -541,13 +542,14 @@ class NoteScreenComponent extends BaseScreenComponent {
});
}
async pickDocument() {
private async pickDocuments() {
try {
const result = await DocumentPicker.pick();
// the result is an array
const result = await DocumentPicker.pickMultiple();
return result;
} catch (error) {
if (DocumentPicker.isCancel(error)) {
console.info('pickDocument: user has cancelled');
console.info('pickDocuments: user has cancelled');
return null;
} else {
throw error;
@@ -632,16 +634,6 @@ class NoteScreenComponent extends BaseScreenComponent {
return;
}
if (pickerResponse.error) {
reg.logger().warn('Got error from picker', pickerResponse.error);
return;
}
if (pickerResponse.didCancel) {
reg.logger().info('User cancelled picker');
return;
}
const localFilePath = Platform.select({
android: pickerResponse.uri,
ios: decodeURI(pickerResponse.uri),
@@ -688,9 +680,9 @@ class NoteScreenComponent extends BaseScreenComponent {
await shim.fsDriver().copy(localFilePath, targetPath);
const stat = await shim.fsDriver().stat(targetPath);
if (stat.size >= 10000000) {
if (stat.size >= 200 * 1024 * 1024) {
await shim.fsDriver().remove(targetPath);
throw new Error('Resources larger than 10 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.');
throw new Error('Resources larger than 200 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.');
}
}
}
@@ -735,9 +727,23 @@ class NoteScreenComponent extends BaseScreenComponent {
this.scheduleSave();
}
async attachPhoto_onPress() {
const response = await this.showImagePicker({ mediaType: 'photo', noData: true });
await this.attachFile(response, 'image');
private async attachPhoto_onPress() {
// the selection Limit should be specfied. I think 200 is enough?
const response: ImagePickerResponse = await this.showImagePicker({ mediaType: 'photo', includeBase64: false, selectionLimit: 200 });
if (response.errorCode) {
reg.logger().warn('Got error from picker', response.errorCode);
return;
}
if (response.didCancel) {
reg.logger().info('User cancelled picker');
return;
}
for (const asset of response.assets) {
await this.attachFile(asset, 'image');
}
}
takePhoto_onPress() {
@@ -748,8 +754,6 @@ class NoteScreenComponent extends BaseScreenComponent {
void this.attachFile(
{
uri: data.uri,
didCancel: false,
error: null,
type: 'image/jpg',
},
'image'
@@ -762,9 +766,11 @@ class NoteScreenComponent extends BaseScreenComponent {
this.setState({ showCamera: false });
}
async attachFile_onPress() {
const response = await this.pickDocument();
await this.attachFile(response, 'all');
private async attachFile_onPress() {
const response = await this.pickDocuments();
for (const asset of response) {
await this.attachFile(asset, 'all');
}
}
toggleIsTodo_onPress() {

View File

@@ -50,6 +50,7 @@ class FolderScreenComponent extends BaseScreenComponent {
lastSavedFolder: Object.assign({}, folder),
});
} else {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
Folder.load(this.props.folderId).then(folder => {
this.setState({
folder: folder,

View File

@@ -151,10 +151,12 @@ class NotesScreenComponent extends BaseScreenComponent {
}
deleteFolder_onPress(folderId) {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
dialogs.confirm(this, _('Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.')).then(ok => {
if (!ok) return;
Folder.delete(folderId)
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then(() => {
this.props.dispatch({
type: 'NAV_GO',
@@ -162,6 +164,7 @@ class NotesScreenComponent extends BaseScreenComponent {
smartFilterId: 'c3176726992c11e9ac940492261af972',
});
})
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.catch(error => {
alert(error.message);
});

View File

@@ -229,7 +229,7 @@ PODS:
- React
- react-native-get-random-values (1.7.1):
- React-Core
- react-native-image-picker (2.3.4):
- react-native-image-picker (4.10.0):
- React-Core
- react-native-image-resizer (1.4.5):
- React-Core
@@ -533,7 +533,7 @@ SPEC CHECKSUMS:
react-native-document-picker: 20f652c2402d3ddc81f396d8167c3bd978add4a2
react-native-geolocation: c956aeb136625c23e0dce0467664af2c437888c9
react-native-get-random-values: 2c4ff6b44cb71291dabe9a8ae87d3877dcf387da
react-native-image-picker: c6d75c4ab2cf46f9289f341242b219cb3c1180d3
react-native-image-picker: 4bc9ed38c8be255b515d8c88babbaf74973f91a8
react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f
react-native-netinfo: 3d3769f0d65de15c83a9bf1346f8be71de5a24bf
react-native-rsa-native: 1f6bba06dd02f0e652a66a384c75c270f7a0062f

View File

@@ -47,7 +47,7 @@
"react-native-file-viewer": "^2.1.4",
"react-native-fs": "^2.16.6",
"react-native-get-random-values": "^1.7.0",
"react-native-image-picker": "^2.3.4",
"react-native-image-picker": "^4.10.0",
"react-native-image-resizer": "^1.3.0",
"react-native-modal-datetime-picker": "^9.0.0",
"react-native-popup-menu": "^0.15.13",

View File

@@ -1,5 +1,5 @@
module.exports = {
hash:"ea13a22d0df59339b671f6b5700e2914", files: {
hash:"abde4aa0d823e75ca24be9dd0e8c971a", 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' },

File diff suppressed because one or more lines are too long

View File

@@ -489,6 +489,7 @@ async function initialize(dispatch: Function) {
await migrateMasterPassword();
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
reg.logger().info(`Client ID: ${Setting.value('clientId')}`);
if (Setting.value('firstStart')) {
let locale = NativeModules.I18nManager.localeIdentifier;
@@ -633,6 +634,7 @@ async function initialize(dispatch: Function) {
// start almost immediately to get the latest data.
// doWifiConnectionCheck set to true so initial sync
// doesn't happen on mobile data
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
void reg.scheduleSync(1000, null, true).then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.

View File

@@ -39,6 +39,7 @@ export default (dispatch: Function, folderId: string) => {
void Note.save({
parent_id: folderId,
is_todo: isTodo,
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
}, { provisional: true }).then((newNote: any) => {
dispatch({
type: 'NAV_GO',
@@ -51,6 +52,7 @@ export default (dispatch: Function, folderId: string) => {
DeviceEventEmitter.addListener('quickActionShortcut', handleQuickAction);
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
QuickActions.popInitialAction().then(handleQuickAction).catch((reason: any) => reg.logger().error(reason));
};

View File

@@ -41,6 +41,7 @@ class DatabaseDriverReactNative {
}
selectAll(sql, params = null) {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
return this.exec(sql, params).then(r => {
const output = [];
for (let i = 0; i < r.rows.length; i++) {

View File

@@ -2,7 +2,7 @@
"name": "<%= packageName %>",
"version": "1.0.0",
"scripts": {
"dist": "webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive",
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
"prepare": "npm run dist",
"update": "npm install -g generator-joplin && yo joplin --update"
},
@@ -14,17 +14,16 @@
"publish"
],
"devDependencies": {
"@types/node": "^14.0.14",
"@types/node": "^18.7.13",
"chalk": "^4.1.0",
"copy-webpack-plugin": "^6.1.0",
"fs-extra": "^9.0.1",
"glob": "^7.1.6",
"on-build-webpack": "^0.1.0",
"tar": "^6.0.5",
"ts-loader": "^7.0.5",
"typescript": "^3.9.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"yargs": "^16.2.0"
"copy-webpack-plugin": "^11.0.0",
"fs-extra": "^10.1.0",
"glob": "^8.0.3",
"tar": "^6.1.11",
"ts-loader": "^9.3.1",
"typescript": "^4.8.2",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"@joplin/lib": "~2.9"
}
}

View File

@@ -11,10 +11,10 @@ const crypto = require('crypto');
const fs = require('fs-extra');
const chalk = require('chalk');
const CopyPlugin = require('copy-webpack-plugin');
const WebpackOnBuildPlugin = require('on-build-webpack');
const tar = require('tar');
const glob = require('glob');
const execSync = require('child_process').execSync;
const allPossibleCategories = require('@joplin/lib/pluginCategories.json');
const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
@@ -29,12 +29,21 @@ const userConfig = Object.assign({}, {
const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`;
const allPossibleCategories = ['appearance', 'developer tools', 'productivity', 'themes', 'integrations', 'viewer', 'search', 'tags', 'editor', 'files', 'personal knowledge management'];
const allPossibleScreenshotsType = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const manifest = readManifest(manifestPath);
const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`);
const { builtinModules } = require('node:module');
// Webpack5 doesn't polyfill by default and displays a warning when attempting to require() built-in
// node modules. Set these to false to prevent Webpack from warning about not polyfilling these modules.
// We don't need to polyfill because the plugins run in Electron's Node environment.
const moduleFallback = {};
for (const moduleName of builtinModules) {
moduleFallback[moduleName] = false;
}
function validatePackageJson() {
const content = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) {
@@ -73,7 +82,7 @@ function validateCategories(categories) {
if (!categories) return null;
if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed');
categories.forEach(category => {
if (!allPossibleCategories.includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid Categories are: \n${allPossibleCategories}\n`);
if (!allPossibleCategories.map(category => { return category.name; }).includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid categories are: \n${allPossibleCategories.map(category => { return category.name; })}\n`);
});
}
@@ -164,6 +173,7 @@ const pluginConfig = Object.assign({}, baseConfig, {
alias: {
api: path.resolve(__dirname, 'api'),
},
fallback: moduleFallback,
// JSON files can also be required from scripts so we include this.
// https://github.com/joplin/plugin-bibtex/pull/2
extensions: ['.js', '.tsx', '.ts', '.json'],
@@ -198,6 +208,7 @@ const extraScriptConfig = Object.assign({}, baseConfig, {
alias: {
api: path.resolve(__dirname, 'api'),
},
fallback: moduleFallback,
extensions: ['.js', '.tsx', '.ts', '.json'],
},
});
@@ -205,11 +216,18 @@ const extraScriptConfig = Object.assign({}, baseConfig, {
const createArchiveConfig = {
stats: 'errors-only',
entry: './dist/index.js',
resolve: {
fallback: moduleFallback,
},
output: {
filename: 'index.js',
path: publishDir,
},
plugins: [new WebpackOnBuildPlugin(onBuildCompleted)],
plugins: [{
apply(compiler) {
compiler.hooks.done.tap('archiveOnBuildListener', onBuildCompleted);
},
}],
};
function resolveExtraScriptPath(name) {
@@ -250,11 +268,8 @@ function buildExtraScriptConfigs(userConfig) {
return output;
}
function main(processArgv) {
const yargs = require('yargs/yargs');
const argv = yargs(processArgv).argv;
const configName = argv['joplin-plugin-config'];
function main(environ) {
const configName = environ['joplin-plugin-config'];
if (!configName) throw new Error('A config file must be specified via the --joplin-plugin-config flag');
// Webpack configurations run in parallel, while we need them to run in
@@ -292,19 +307,22 @@ function main(processArgv) {
return configs[configName];
}
let exportedConfigs = [];
try {
exportedConfigs = main(process.argv);
} catch (error) {
console.error(chalk.red(error.message));
process.exit(1);
}
module.exports = (env) => {
let exportedConfigs = [];
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
try {
exportedConfigs = main(env);
} catch (error) {
console.error(error.message);
process.exit(1);
}
module.exports = exportedConfigs;
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
return exportedConfigs;
};

View File

@@ -880,6 +880,7 @@ export default class BaseApplication {
if (!Setting.value('api.token')) {
void EncryptionService.instance()
.generateApiToken()
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then((token: string) => {
Setting.setValue('api.token', token);
});

View File

@@ -248,6 +248,7 @@ class BaseModel {
if (options.where) sql += ` WHERE ${options.where}`;
return this.db()
.selectOne(sql)
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then((r: any) => {
return r ? r['total'] : 0;
});
@@ -335,6 +336,7 @@ class BaseModel {
if (params === null) params = [];
return this.db()
.selectOne(sql, params)
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then((model: any) => {
return this.filter(this.addModelMd(model));
});
@@ -344,6 +346,7 @@ class BaseModel {
if (params === null) params = [];
return this.db()
.selectAll(sql, params)
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then((models: any[]) => {
return this.filterArray(this.addModelMd(models));
});

View File

@@ -292,6 +292,7 @@ export default class JoplinDatabase extends Database {
queries.push(this.wrapQuery('DELETE FROM table_fields'));
return this.selectAll('SELECT name FROM sqlite_master WHERE type="table"')
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then(tableRows => {
const chain = [];
for (let i = 0; i < tableRows.length; i++) {
@@ -303,6 +304,7 @@ export default class JoplinDatabase extends Database {
if (tableName === 'notes_spellfix') continue;
if (tableName === 'search_aux') continue;
chain.push(() => {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
return this.selectAll(`PRAGMA table_info("${tableName}")`).then(pragmas => {
for (let i = 0; i < pragmas.length; i++) {
const item = pragmas[i];
@@ -325,6 +327,7 @@ export default class JoplinDatabase extends Database {
return promiseChain(chain);
})
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then(() => {
queries.push({ sql: 'UPDATE version SET table_fields_version = ?', params: [newVersion] });
return this.transactionExecBatch(queries);

View File

@@ -231,11 +231,14 @@ class Logger {
// when many log operations are being done (eg. during sync in
// dev mode).
let release: Function = null;
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
writeToFileMutex_.acquire().then((r: Function) => {
release = r;
return Logger.fsDriver().appendFile(target.path, `${line.join(': ')}\n`, 'utf8');
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
}).catch((error: any) => {
console.error('Cannot write to log file:', error);
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
}).finally(() => {
if (release) release();
});

View File

@@ -70,9 +70,11 @@ export default class TaskQueue {
task
.callback()
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then((result: any) => {
completeTask(task, result, null);
})
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.catch((error: Error) => {
if (!error) error = new Error('Unknown error');
completeTask(task, null, error);

View File

@@ -102,6 +102,7 @@ shared.saveNoteButton_press = async function(comp, folderId = null, options = nu
comp.setState(newState);
if (isProvisionalNote) {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
Note.updateGeolocation(note.id).then(geoNote => {
const stateNote = comp.state.note;
if (!stateNote || !geoNote) return;

View File

@@ -588,6 +588,7 @@ export default async function importEnex(parentFolderId: string, filePath: strin
notes.push(note);
if (notes.length >= 10) {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
processNotes().catch(error => {
importOptions.onError(createErrorWithNoteTitle(this, error));
});
@@ -648,6 +649,7 @@ export default async function importEnex(parentFolderId: string, filePath: strin
saxStream.on('end', handleSaxStreamEvent(function() {
// Wait till there is no more notes to process.
const iid = shim.setInterval(() => {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
void processNotes().then(allDone => {
if (allDone) {
shim.clearTimeout(iid);

View File

@@ -55,6 +55,7 @@ export default class Folder extends BaseItem {
return this.db()
.selectAll(`SELECT id FROM notes WHERE ${where.join(' AND ')}`, [parentId])
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then((rows: any[]) => {
const output = [];
for (let i = 0; i < rows.length; i++) {
@@ -759,6 +760,7 @@ export default class Folder extends BaseItem {
syncDebugLog.info('Folder Save:', o);
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
return super.save(o, options).then((folder: FolderEntity) => {
this.dispatch({
type: 'FOLDER_UPDATE_ONE',

View File

@@ -1039,6 +1039,21 @@ class Setting extends BaseModel {
isGlobal: true,
},
// Works around a bug in which additional space is visible beneath the toolbar on some devices.
// See https://github.com/laurent22/joplin/pull/6823
'editor.mobile.removeSpaceBelowToolbar': {
value: false,
type: SettingItemType.Bool,
section: 'note',
public: true,
appTypes: [AppType.Mobile],
show: (settings: any) => settings['editor.mobile.removeSpaceBelowToolbar'],
label: () => 'Remove extra space below the markdown toolbar',
description: () => 'Works around bug on some devices where the markdown toolbar does not touch the bottom of the screen.',
storage: SettingStorage.File,
isGlobal: true,
},
newTodoFocus: {
value: 'title',
type: SettingItemType.String,

View File

@@ -205,6 +205,7 @@ export default class Tag extends BaseItem {
}
}
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
return super.save(o, options).then((tag: TagEntity) => {
if (options.dispatchUpdateAction) {
this.dispatch({

View File

@@ -82,12 +82,14 @@ class OneDriveApiNodeUtils {
this.api()
.execTokenRequest(query.code, `http://localhost:${port.toString()}`)
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then(() => {
writeResponse(200, _('The application has been authorised - you may now close this browser tab.'));
targetConsole.log('');
targetConsole.log(_('The application has been successfully authorised.'));
waitAndDestroy();
})
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.catch(error => {
writeResponse(400, error.message);
targetConsole.log('');

View File

@@ -0,0 +1,35 @@
[
{
"name": "appearance"
},
{
"name": "developer tools"
},
{
"name": "productivity"
},
{
"name": "themes"
},
{
"name": "integrations"
},
{
"name": "viewer"
},
{
"name": "search"
},
{
"name": "tags"
},
{
"name": "editor"
},
{
"name": "files"
},
{
"name": "personal knowledge management"
}
]

View File

@@ -4,6 +4,7 @@ function promiseChain(chain, defaultValue = null) {
});
for (let i = 0; i < chain.length; i++) {
const f = chain[i];
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
output = output.then(f);
}
return output;

View File

@@ -49,6 +49,7 @@ describe('Registry', function() {
it('should sync if do wifi check is false', done => {
void reg.scheduleSync(1, null, false)
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then(() =>{
expect(sync.start).toHaveBeenCalled();
done();

View File

@@ -181,11 +181,13 @@ export default class ResourceFetcher extends BaseService {
fileApi
.get(remoteResourceContentPath, { path: localResourceContentPath, target: 'file' })
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then(async () => {
await Resource.setLocalState(resource, { fetch_status: Resource.FETCH_STATUS_DONE });
this.logger().debug(`ResourceFetcher: Resource downloaded: ${resource.id}`);
await completeDownload(true, localResourceContentPath);
})
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.catch(async (error: any) => {
this.logger().error(`ResourceFetcher: Could not download resource: ${resource.id}`, error);
await Resource.setLocalState(resource, { fetch_status: Resource.FETCH_STATUS_ERROR, fetch_error: error.message });

View File

@@ -77,6 +77,7 @@ export default async function populateDatabase(db: any, options: Options = null)
let tagBatch = [];
for (let i = 0; i < options.tagCount; i++) {
const tagTitle = randomElement(wordList); // `tag${i}`
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
tagBatch.push(Tag.save({ title: tagTitle }, { dispatchUpdateAction: false }).then((savedTag: any) => {
createdTagIds.push(savedTag.id);
if (!options.silent) console.info(`Tags: ${i} / ${options.tagCount}`);
@@ -99,6 +100,7 @@ export default async function populateDatabase(db: any, options: Options = null)
const parentIndex = randomIndex(createdFolderIds);
note.parent_id = createdFolderIds[parentIndex];
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
noteBatch.push(Note.save(note, { dispatchUpdateAction: false }).then((savedNote: any) => {
createdNoteIds.push(savedNote.id);
console.info(`Notes: ${i} / ${options.noteCount}`);

View File

@@ -36,6 +36,7 @@ export default class JoplinPlugins {
// We don't use `await` when calling onStart because the plugin might be awaiting
// in that call too (for example, when opening a dialog on startup) so we don't
// want to get stuck here.
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
void script.onStart({}).catch((error: any) => {
// For some reason, error thrown from the executed script do not have the type "Error"
// but are instead plain object. So recreate the Error object here so that it can
@@ -43,6 +44,7 @@ export default class JoplinPlugins {
const newError: Error = new Error(error.message);
newError.stack = error.stack;
logger.error(`Uncaught exception in plugin "${this.plugin.id}":`, newError);
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
}).then(() => {
logger.info(`Finished running onStart handler: ${this.plugin.id} (Took ${Date.now() - startTime}ms)`);
this.plugin.emit('started');

View File

@@ -3,9 +3,12 @@ import path = require('path');
import Setting from '../../../models/Setting';
import shim from '../../../shim';
import PluginService, { defaultPluginSetting, DefaultPluginsInfo, PluginSettings } from '../PluginService';
import Logger from '@joplin/lib/Logger';
import * as React from 'react';
const shared = require('@joplin/lib/components/shared/config-shared.js');
const logger = Logger.create('defaultPluginsUtils');
export function checkPreInstalledDefaultPlugins(defaultPluginsId: string[],pluginSettings: PluginSettings) {
const installedDefaultPlugins: Array<string> = Setting.value('installedDefaultPlugins');
for (const pluginId of defaultPluginsId) {
@@ -15,8 +18,17 @@ export function checkPreInstalledDefaultPlugins(defaultPluginsId: string[],plugi
}
}
export async function installDefaultPlugins(service: PluginService, pluginsDir: string, defaultPluginsId: string[], pluginSettings: PluginSettings): Promise<PluginSettings> {
const defaultPluginsPaths = await shim.fsDriver().readDirStats(pluginsDir);
export async function installDefaultPlugins(service: PluginService, defaultPluginsDir: string, defaultPluginsId: string[], pluginSettings: PluginSettings): Promise<PluginSettings> {
if (!await shim.fsDriver().exists(defaultPluginsDir)) {
logger.info(`Could not find default plugins' directory: ${defaultPluginsDir} - skipping installation.`);
return pluginSettings;
}
const defaultPluginsPaths = await shim.fsDriver().readDirStats(defaultPluginsDir);
if (defaultPluginsPaths.length <= 0) {
logger.info(`Default plugins' directory is empty: ${defaultPluginsDir} - skipping installation.`);
return pluginSettings;
}
const installedPlugins = Setting.value('installedDefaultPlugins');
for (let pluginId of defaultPluginsPaths) {
@@ -24,7 +36,7 @@ export async function installDefaultPlugins(service: PluginService, pluginsDir:
// if pluginId is present in 'installedDefaultPlugins' array or it doesn't have default plugin ID, then we won't install it again as default plugin
if (installedPlugins.includes(pluginId) || !defaultPluginsId.includes(pluginId)) continue;
const defaultPluginPath: string = path.join(pluginsDir, pluginId, 'plugin.jpl');
const defaultPluginPath: string = path.join(defaultPluginsDir, pluginId, 'plugin.jpl');
await service.installPlugin(defaultPluginPath, false);
pluginSettings = produce(pluginSettings, (draft: PluginSettings) => {
@@ -41,7 +53,7 @@ export function setSettingsForDefaultPlugins(defaultPluginsInfo: DefaultPluginsI
for (const pluginId of Object.keys(defaultPluginsInfo)) {
if (!defaultPluginsInfo[pluginId].settings) continue;
for (const settingName of Object.keys(defaultPluginsInfo[pluginId].settings)) {
if (!installedDefaultPlugins.includes(pluginId)) {
if (!installedDefaultPlugins.includes(pluginId) && Setting.keyExists(`plugin-${pluginId}.${settingName}`)) {
Setting.setValue(`plugin-${pluginId}.${settingName}`, defaultPluginsInfo[pluginId].settings[settingName]);
}
}

View File

@@ -502,7 +502,9 @@ function shimInit(options = null) {
const cleanUpOnError = error => {
// We ignore any unlink error as we only want to report on the main error
fs.unlink(filePath)
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.catch(() => {})
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
.then(() => {
if (file) {
file.close(() => {

View File

@@ -0,0 +1,127 @@
import { useRef, useState, useCallback } from 'react';
import * as React from 'react';
import usePdfDocument from './hooks/usePdfDocument';
import VerticalPages from './VerticalPages';
import MessageService from './messageService';
import { DownloadButton, PrintButton, OpenLinkButton, CloseButton } from './ui/IconButtons';
import ZoomControls from './ui/ZoomControls';
import styled from 'styled-components';
import GotoInput from './ui/GotoPage';
require('./fullScreen.css');
const TitleWrapper = styled.div`
font-size: 0.7rem;
font-weight: 400;
display: flex;
align-items: start;
flex-direction: column;
min-width: 10rem;
max-width: 18rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--secondary);
padding: 0.2rem 0.6rem;
height: 100%;
width: 100%;
justify-content: center;
`;
const Title = styled.div`
font-size: 0.9rem;
font-weight: bold;
margin-bottom: 0.2rem;
color: var(--primary);
overflow: hidden;
`;
export interface FullViewerProps {
pdfPath: string;
isDarkTheme: boolean;
messageService: MessageService;
startPage: number;
title: string;
}
export default function FullViewer(props: FullViewerProps) {
const pdfDocument = usePdfDocument(props.pdfPath);
const [zoom, setZoom] = useState<number>(1);
const [startPage, setStartPage] = useState<number>(props.startPage || 1);
const [selectedPage, setSelectedPage] = useState<number>(startPage);
const mainViewerRef = useRef<HTMLDivElement>(null);
const thubmnailRef = useRef<HTMLDivElement>(null);
const onActivePageChange = useCallback((pageNo: number) => {
setSelectedPage(pageNo);
}, []);
const goToPage = useCallback((pageNo: number) => {
if (pageNo < 1 || pageNo > pdfDocument.pageCount || pageNo === selectedPage) return;
setSelectedPage(pageNo);
setStartPage(pageNo);
}, [pdfDocument, selectedPage]);
if (!pdfDocument) {
return (
<div className="full-app loading">
<div>Loading pdf..</div>
</div>);
}
return (
<div className="full-app">
<div className="top-bar">
<div>
<TitleWrapper>
<Title title={props.title}>{props.title}</Title>
<div>{selectedPage} of {pdfDocument.pageCount} pages</div>
</TitleWrapper>
</div>
<div>
<ZoomControls onChange={setZoom} zoom={zoom} size={1} />
<OpenLinkButton onClick={props.messageService.openExternalViewer} size={1.3} />
<PrintButton onClick={pdfDocument?.printPdf} size={1.3}/>
<DownloadButton onClick={pdfDocument?.downloadPdf} size={1.3}/>
<GotoInput onChange={goToPage} size={1.3} pageCount={pdfDocument.pageCount} currentPage={selectedPage} />
</div>
<div>
<CloseButton onClick={props.messageService.close} size={1.3} />
</div>
</div>
<div className="viewers dark-bg">
<div className="pane thumbnail-pane" ref={thubmnailRef}>
<VerticalPages
pdfDocument={pdfDocument}
isDarkTheme={true}
rememberScroll={false}
container={thubmnailRef}
pageGap={16}
widthPercent={86}
showPageNumbers={true}
selectedPage={selectedPage}
onPageClick={goToPage}
textSelectable={false}
zoom={1}
/>
</div>
<div className="pane main-pane" ref={mainViewerRef}>
<VerticalPages
pdfDocument={pdfDocument}
isDarkTheme={true}
rememberScroll={false}
container={mainViewerRef}
zoom={zoom}
pageGap={5}
anchorPage={startPage}
onActivePageChange={onActivePageChange}
textSelectable={true}
onTextSelect={props.messageService.textSelected}
showPageNumbers={false}
/>
</div>
</div>
</div>
);
}

View File

@@ -1,32 +1,35 @@
import { useEffect, useRef, useState, MutableRefObject } from 'react';
import { useEffect, useRef, useState, MutableRefObject, useCallback } from 'react';
import * as React from 'react';
import useIsVisible from './hooks/useIsVisible';
import useVisibleOnSelect, { VisibleOnSelect } from './hooks/useVisibleOnSelect';
import PdfDocument from './PdfDocument';
import { ScaledSize } from './types';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import { ScaledSize, RenderRequest } from './types';
import styled from 'styled-components';
const PageWrapper = styled.div`
require('./textLayer.css');
const PageWrapper = styled.div<{ isSelected?: boolean }>`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
border: solid thin rgba(120, 120, 120, 0.498);
border: ${props => props.isSelected ? 'solid 5px #0079ff' : 'solid thin rgba(120, 120, 120, 0.498)'};
background: rgb(233, 233, 233);
position: relative;
border-radius: 0px;
border-radius: ${props => props.isSelected ? '0.3rem' : '0px'};
`;
const PageInfo = styled.div`
const PageInfo = styled.div<{ isSelected?: boolean }>`
position: absolute;
top: 0.5rem;
left: 0.5rem;
padding: 0.3rem;
background: rgba(203, 203, 203, 0.509);
background: ${props => props.isSelected ? '#0079ff' : 'rgba(203, 203, 203, 0.509)'};
border-radius: 0.3rem;
font-size: 0.8rem;
color: rgba(91, 91, 91, 0.829);
color: ${props => props.isSelected ? 'white' : 'rgba(91, 91, 91, 0.829)'};
backdrop-filter: blur(0.5rem);
cursor: default;
user-select: none;
@@ -43,61 +46,68 @@ export interface PageProps {
scaledSize: ScaledSize;
isDarkTheme: boolean;
container: MutableRefObject<HTMLElement>;
showPageNumbers?: boolean;
showPageNumbers: boolean;
isSelected: boolean;
textSelectable: boolean;
onTextSelect?: (text: string)=> void;
onClick?: (page: number)=> void;
onDoubleClick?: (page: number)=> void;
}
export default function Page(props: PageProps) {
const [error, setError] = useState(null);
const [page, setPage] = useState(null);
const scaleRef = useRef<number>(null);
const timestampRef = useRef<number>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const textRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const isVisible = useIsVisible(canvasRef, props.container);
const isVisible = useIsVisible(wrapperRef, props.container);
useVisibleOnSelect({
isVisible,
isSelected: props.isSelected,
container: props.container,
wrapperRef,
} as VisibleOnSelect);
useEffect(() => {
if (!isVisible || !page || !props.scaledSize || (scaleRef.current && props.scaledSize.scale === scaleRef.current)) return;
try {
const viewport = page.getViewport({ scale: props.scaledSize.scale || 1.0 });
const canvas = canvasRef.current;
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
const pageTimestamp = new Date().getTime();
timestampRef.current = pageTimestamp;
page.render({
canvasContext: ctx,
viewport,
// Used so that the page rendering is throttled to some extent.
// https://stackoverflow.com/questions/18069448/halting-pdf-js-page-rendering
continueCallback: function(cont: any) {
if (timestampRef.current !== pageTimestamp) {
return;
}
cont();
},
});
const isCancelled = () => props.scaledSize.scale !== scaleRef.current;
const renderPage = async () => {
try {
if (canvasRef.current) {
canvasRef.current.style.height = '100%';
canvasRef.current.style.width = '100%';
}
const renderRequest: RenderRequest = {
pageNo: props.pageNo,
scaledSize: props.scaledSize,
getTextLayer: props.textSelectable,
isCancelled,
};
const { canvas, textLayerDiv } = await props.pdfDocument.renderPage(renderRequest);
wrapperRef.current.appendChild(canvas);
if (textLayerDiv) wrapperRef.current.appendChild(textLayerDiv);
if (canvasRef.current) canvasRef.current.remove();
if (textRef.current) textRef.current.remove();
canvasRef.current = canvas;
if (textLayerDiv) textRef.current = textLayerDiv;
} catch (error) {
if (isCancelled()) return;
error.message = `Error rendering page no. ${props.pageNo}: ${error.message}`;
setError(error);
throw error;
}
};
if (isVisible && props.scaledSize && (props.scaledSize.scale !== scaleRef.current)) {
scaleRef.current = props.scaledSize.scale;
} catch (error) {
error.message = `Error rendering page no. ${props.pageNo}: ${error.message}`;
setError(error);
throw error;
void renderPage();
}
}, [page, props.scaledSize, isVisible, props.pageNo]);
useAsyncEffect(async (event: AsyncEffectEvent) => {
if (page || !isVisible || !props.pdfDocument) return;
try {
const _page = await props.pdfDocument.getPage(props.pageNo);
if (event.cancelled) return;
setPage(_page);
} catch (error) {
console.error('Page load error', props.pageNo, error);
setError(error);
}
}, [page, props.scaledSize, isVisible]);
}, [props.scaledSize, isVisible, props.textSelectable, props.pageNo, props.pdfDocument]);
useEffect(() => {
if (props.focusOnLoad) {
@@ -106,6 +116,11 @@ export default function Page(props: PageProps) {
}
}, [props.container, props.focusOnLoad]);
const onClick = useCallback(async (_e: React.MouseEvent<HTMLDivElement>) => {
if (props.onClick) props.onClick(props.pageNo);
}, [props.onClick, props.pageNo]);
let style: any = {};
if (props.scaledSize) {
style = {
@@ -115,15 +130,23 @@ export default function Page(props: PageProps) {
};
}
const onContextMenu = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
if (!props.textSelectable || !props.onTextSelect || !window.getSelection()) return;
const text = window.getSelection().toString();
if (!text) return;
props.onTextSelect(text);
e.preventDefault();
e.stopPropagation();
}, [props.textSelectable, props.onTextSelect]);
const onDoubleClick = useCallback(() => {
if (props.onDoubleClick) props.onDoubleClick(props.pageNo);
}, [props.onDoubleClick, props.pageNo]);
return (
<PageWrapper ref={wrapperRef} style={style}>
<canvas ref={canvasRef} className="page-canvas" style={style}>
<div>
{error ? 'ERROR' : 'Loading..'}
</div>
Page {props.pageNo}
</canvas>
{props.showPageNumbers && <PageInfo>{props.isAnchored ? '📌' : ''} Page {props.pageNo}</PageInfo>}
<PageWrapper onDoubleClick={onDoubleClick} isSelected={!!props.isSelected} onContextMenu={onContextMenu} onClick={onClick} ref={wrapperRef} style={style}>
{ error && <div>Error: {error}</div> }
{props.showPageNumbers && <PageInfo isSelected={!!props.isSelected}>{props.isAnchored ? '📌' : ''} Page {props.pageNo}</PageInfo>}
</PageWrapper>
);
}

View File

@@ -1,11 +1,14 @@
import * as pdfjsLib from 'pdfjs-dist';
import { ScaledSize } from './types';
import { ScaledSize, RenderRequest, RenderResult } from './types';
import { Mutex, MutexInterface, withTimeout } from 'async-mutex';
export default class PdfDocument {
public url: string | Uint8Array;
private doc: any = null;
public pageCount: number = null;
private pages: any = {};
private rendererMutex: MutexInterface = null;
private pageSize: {
height: number;
width: number;
@@ -14,6 +17,7 @@ export default class PdfDocument {
public constructor(document: HTMLDocument) {
this.document = document;
this.rendererMutex = withTimeout(new Mutex(), 40 * 1000);
}
public loadDoc = async (url: string | Uint8Array) => {
@@ -74,6 +78,64 @@ export default class PdfDocument {
return Math.min(pageNo, this.pageCount);
};
private renderPageImpl = async ({ pageNo, scaledSize, getTextLayer, isCancelled }: RenderRequest): Promise<RenderResult> => {
const checkCancelled = () => {
if (isCancelled()) {
throw new Error(`Render cancelled, page: ${pageNo}`);
}
};
const page = await this.getPage(pageNo);
checkCancelled();
const canvas = this.document.createElement('canvas');
canvas.classList.add('page-canvas');
const viewport = page.getViewport({ scale: scaledSize.scale || 1.0 });
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Could not get canvas context');
}
await page.render({
canvasContext: ctx,
viewport,
}).promise;
checkCancelled();
let textLayerDiv = null;
if (getTextLayer) {
textLayerDiv = this.document.createElement('div');
textLayerDiv.classList.add('textLayer');
const textFragment = this.document.createDocumentFragment();
const txtContext = await page.getTextContent();
checkCancelled();
// Pass the data to the method for rendering of text over the pdf canvas.
textLayerDiv.style.height = `${viewport.height}px`;
textLayerDiv.style.width = `${viewport.width}px`;
await pdfjsLib.renderTextLayer({
textContent: txtContext,
enhanceTextSelection: true,
container: textFragment,
viewport: viewport,
textDivs: [],
}).promise;
textLayerDiv.appendChild(textFragment);
}
return { canvas, textLayerDiv };
};
public async renderPage(task: RenderRequest): Promise<RenderResult> {
// We're using a render mutex to avoid rendering too many pages at the same time
// Which can cause the pdfjs library to abandon some of the in-progress rendering unexpectedly
return await this.rendererMutex.runExclusive(async () => {
return await this.renderPageImpl(task);
});
}
public printPdf = () => {
const frame = this.document.createElement('iframe');
frame.style.position = 'fixed';

View File

@@ -12,8 +12,8 @@ const PagesHolder = styled.div<{ pageGap: number }>`
justify-content: center;
align-items: center;
flex-flow: column;
width: fit-content;
min-width: 100%;
width: fit-content;
min-height: fit-content;
row-gap: ${(props)=> props.pageGap || 2}px;
`;
@@ -22,13 +22,19 @@ export interface VerticalPagesProps {
pdfDocument: PdfDocument;
isDarkTheme: boolean;
anchorPage?: number;
rememberScroll?: boolean;
rememberScroll: boolean;
pdfId?: string;
zoom?: number;
zoom: number;
container: MutableRefObject<HTMLElement>;
pageGap: number;
showPageNumbers?: boolean;
onActivePageChange: (page: number)=> void;
widthPercent?: number;
showPageNumbers: boolean;
selectedPage?: number;
textSelectable: boolean;
onTextSelect?: (text: string)=> void;
onPageClick?: (page: number)=> void;
onActivePageChange?: (page: number)=> void;
onDoubleClick?: (page: number)=> void;
}
export default function VerticalPages(props: VerticalPagesProps) {
@@ -63,7 +69,8 @@ export default function VerticalPages(props: VerticalPagesProps) {
const updateWidth = () => {
if (cancelled) return;
setContainerWidth(props.container.current.clientWidth);
const factor = (props.widthPercent || 100) / 100;
setContainerWidth(props.container.current.clientWidth * factor);
};
const onResize = () => {
@@ -85,7 +92,7 @@ export default function VerticalPages(props: VerticalPagesProps) {
resizeTimer = null;
}
};
}, [props.container, props.pdfDocument]);
}, [props.container, props.pdfDocument, props.widthPercent]);
return (<PagesHolder pageGap={props.pageGap || 2} ref={innerContainerEl} >
{scaledSize ?
@@ -94,6 +101,11 @@ export default function VerticalPages(props: VerticalPagesProps) {
return <Page pdfDocument={props.pdfDocument} pageNo={i + 1} focusOnLoad={scaledSize && props.anchorPage && props.anchorPage === i + 1}
isAnchored={props.anchorPage && props.anchorPage === i + 1}
showPageNumbers={props.showPageNumbers}
isSelected={scaledSize && props.selectedPage && props.selectedPage === i + 1}
onClick={props.onPageClick}
textSelectable={props.textSelectable}
onTextSelect={props.onTextSelect}
onDoubleClick={props.onDoubleClick}
isDarkTheme={props.isDarkTheme} scaledSize={scaledSize} container={props.container} key={i} />;
}
) : 'Calculating size...'

View File

@@ -84,4 +84,3 @@ hr {
[data-theme="dark"] .dark-bg {
background-color: rgb(54, 54, 54);
}

View File

@@ -0,0 +1,64 @@
.full-app{
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
display: grid;
grid-template-rows: 2.8rem auto;
}
.viewers{
width: 100%;
height: calc(100vh - 2.8rem);
overflow: hidden;
display: grid;
grid-template-columns: 11rem auto;
padding-right: 0.4rem;
}
.pane{
display: block;
margin: 0px auto;
overflow-x: auto;
overflow-y: auto;
width: 100%;
height: 100%;
padding: 0rem 0.2rem;
position: relative;
}
.thumbnail-pane{
background-color: var(--tertiary);
overflow-y: scroll;
}
.top-bar{
display: grid;
grid-template-columns: 18rem auto 3rem;
align-items: center;
overflow: hidden;
background-color: var(--tertiary);
flex-direction: row;
}
.top-bar > div{
display: flex;
justify-content: space-around;
align-items: center;
padding: 0.2rem;
flex-direction: row;
column-gap: 0.6rem;
height: 100%;
width: 100%;
max-width: 30rem;
margin: 0rem auto;
overflow: hidden;
}
@media screen and (max-width: 840px) {
.viewers{
grid-template-columns: 9rem auto;
}
.top-bar{
grid-template-columns: 10rem auto 3rem;
}
}

View File

@@ -1,21 +1,37 @@
import { useEffect, useState, MutableRefObject } from 'react';
import { useEffect, useState, MutableRefObject, useRef } from 'react';
const useIsVisible = (elementRef: MutableRefObject<HTMLElement>, rootRef: MutableRefObject<HTMLElement>) => {
const [isVisible, setIsVisible] = useState(false);
const lastVisible = useRef(0);
const invisibleOn = useRef(0);
useEffect(() => {
let observer: IntersectionObserver = null;
let timeout: number = null;
if (elementRef.current) {
observer = new IntersectionObserver((entries, _observer) => {
let visible = false;
entries.forEach((entry) => {
if (entry.isIntersecting) {
visible = true;
setIsVisible(true);
lastVisible.current = Date.now();
if ((invisibleOn.current - lastVisible.current) > 300) {
setIsVisible(true);
} else {
if (!timeout) {
timeout = window.setTimeout(() => {
if (invisibleOn.current < lastVisible.current) {
setIsVisible(true);
}
timeout = null;
}, 300);
}
}
}
});
if (!visible) {
invisibleOn.current = Date.now();
setIsVisible(false);
}
}, {
@@ -29,6 +45,10 @@ const useIsVisible = (elementRef: MutableRefObject<HTMLElement>, rootRef: Mutabl
if (observer) {
observer.disconnect();
}
if (timeout) {
window.clearTimeout(timeout);
timeout = null;
}
};
}, [elementRef, rootRef]);

View File

@@ -0,0 +1,27 @@
import { useRef, useEffect, MutableRefObject } from 'react';
export interface VisibleOnSelect {
container: MutableRefObject<HTMLElement>;
wrapperRef: MutableRefObject<HTMLElement>;
isVisible: boolean;
isSelected: boolean;
}
// Used in thumbnail view, to scroll to the newly selected page.
const useVisibleOnSelect = ({ container, wrapperRef, isVisible, isSelected }: VisibleOnSelect) => {
const isVisibleRef = useRef(isVisible);
useEffect(() => {
if (isSelected && !isVisibleRef.current) {
container.current.scrollTop = wrapperRef.current.offsetTop;
}
}, [isSelected, isVisibleRef, container, wrapperRef]);
useEffect(() => {
isVisibleRef.current = isVisible;
} , [isVisible]);
};
export default useVisibleOnSelect;

View File

@@ -4,6 +4,8 @@ shim.setReact(React);
import { render } from 'react-dom';
import * as pdfjsLib from 'pdfjs-dist';
import MiniViewerApp from './miniViewer';
import MessageService from './messageService';
import FullViewer from './FullViewer';
require('./common.css');
@@ -15,15 +17,27 @@ const type = window.frameElement.getAttribute('x-type');
const appearance = window.frameElement.getAttribute('x-appearance');
const anchorPage = Number(window.frameElement.getAttribute('x-anchorPage')) || null;
const pdfId = window.frameElement.getAttribute('id');
const resourceId = window.frameElement.getAttribute('x-resourceid');
const title = window.frameElement.getAttribute('x-title');
document.documentElement.setAttribute('data-theme', appearance);
const messageService = new MessageService(type);
function App() {
if (type === 'mini') {
return <MiniViewerApp pdfPath={url}
isDarkTheme={appearance === 'dark'}
anchorPage={anchorPage}
pdfId={pdfId} />;
pdfId={pdfId}
resourceId={resourceId}
messageService={messageService}/>;
} else if (type === 'full') {
return <FullViewer pdfPath={url}
isDarkTheme={appearance === 'dark'}
startPage={anchorPage || 1}
title={title}
messageService={messageService} />;
}
return <div>Error: Unknown app type "{type}"</div>;
}

View File

@@ -0,0 +1,35 @@
export default class MessageService {
private viewerType: string;
public constructor(type: string) {
this.viewerType = type;
}
private sendMessage = (name: string, data?: any) => {
if (this.viewerType === 'full') {
const message = {
name,
...data,
};
window.postMessage(message, '*');
} else if (this.viewerType === 'mini') {
const message = {
name,
data,
target: 'webview',
};
window.parent.postMessage(message, '*');
}
};
public textSelected = (text: string) => {
this.sendMessage('textSelected', { text });
};
public close = () => {
this.sendMessage('close');
};
public openExternalViewer = () => {
this.sendMessage('externalViewer');
};
public openFullScreenViewer = (resourceId: string, pageNo: number) => {
this.sendMessage('openPdfViewer', { resourceId, pageNo });
};
}

View File

@@ -3,6 +3,7 @@ import useIsFocused from './hooks/useIsFocused';
import usePdfDocument from './hooks/usePdfDocument';
import VerticalPages from './VerticalPages';
import ZoomControls from './ui/ZoomControls';
import MessageService from './messageService';
import { DownloadButton, PrintButton } from './ui/IconButtons';
require('./miniViewer.css');
@@ -12,6 +13,8 @@ export interface MiniViewerAppProps {
isDarkTheme: boolean;
anchorPage: number;
pdfId: string;
resourceId?: string;
messageService: MessageService;
}
export default function MiniViewerApp(props: MiniViewerAppProps) {
@@ -25,6 +28,10 @@ export default function MiniViewerApp(props: MiniViewerAppProps) {
setActivePage(page);
}, []);
const onDoubleClick = useCallback((pageNo: number) => {
props.messageService.openFullScreenViewer(props.resourceId, pageNo);
}, [props.messageService, props.resourceId]);
if (!pdfDocument) {
return (
<div className="mini-app loading">
@@ -44,6 +51,9 @@ export default function MiniViewerApp(props: MiniViewerAppProps) {
container={containerEl}
showPageNumbers={true}
zoom={zoom}
textSelectable={true}
onTextSelect={props.messageService.textSelected}
onDoubleClick={onDoubleClick}
pageGap={2}
onActivePageChange={onActivePageChange} />
</div>

View File

@@ -40,6 +40,7 @@
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@joplin/lib": "workspace:^",
"async-mutex": "^0.4.0",
"pdfjs-dist": "^2.14.305",
"react": "16.13.1",
"react-dom": "16.9.0",

View File

@@ -0,0 +1,91 @@
/* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.textLayer {
position: absolute;
text-align: initial;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
line-height: 1;
text-size-adjust: none;
}
.textLayer span,
.textLayer br {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
/* Only necessary in Google Chrome, see issue 14205, and most unfortunately
* the problem doesn't show up in "text" reference tests. */
.textLayer span.markedContent {
top: 0;
height: 0;
}
.textLayer .highlight {
margin: -1px;
padding: 1px;
background-color: rgba(180, 0, 170, 1);
border-radius: 4px;
}
.textLayer .highlight.appended {
position: initial;
}
.textLayer .highlight.begin {
border-radius: 4px 0 0 4px;
}
.textLayer .highlight.end {
border-radius: 0 4px 4px 0;
}
.textLayer .highlight.middle {
border-radius: 0;
}
.textLayer .highlight.selected {
background-color: rgba(0, 100, 0, 1);
}
/* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
.textLayer br::selection {
background: transparent;
}
.textLayer .endOfContent {
display: block;
position: absolute;
left: 0;
top: 100%;
right: 0;
bottom: 0;
z-index: -1;
cursor: default;
user-select: none;
}
.textLayer .endOfContent.active {
top: 0;
}

View File

@@ -9,3 +9,15 @@ export interface IconButtonProps {
size?: number;
color?: string;
}
export interface RenderRequest {
pageNo: number;
scaledSize: ScaledSize;
getTextLayer: boolean;
isCancelled: ()=> boolean;
}
export interface RenderResult {
canvas: HTMLCanvasElement;
textLayerDiv: HTMLDivElement;
}

View File

@@ -0,0 +1,67 @@
import React, { useCallback, useEffect, useRef } from 'react';
import styled from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faAngleRight, faAngleLeft } from '@fortawesome/free-solid-svg-icons';
const Group = styled.div<{ size: number }>`
display: flex;
justify-content: center;
align-items: center;
flex-flow: row;
color: var(--grey);
cursor: initial;
font-size: ${props => props.size}rem;
padding: 0.2rem 0.4rem;
svg:hover {
color: var(--secondary);
}
`;
const GoToInput = styled.input`
ont-size: 0.7rem;
font-weight: 500;
max-width: 3rem;
padding: 0.4rem 0.3rem;
background: var(--bg);
border: solid 2px transparent;
border-radius: 3.5rem;
color: var(--tertialry);
text-align: center;
margin: auto 0.6rem;
&:focus {
outline: none;
border: solid 2px var(--blue);
}
`;
export interface GotoInputProps {
onChange: (pageNo: number)=> void;
size?: number;
pageCount: number;
currentPage: number;
}
export default function GotoInput(props: GotoInputProps) {
const inputRef = useRef<HTMLInputElement>(null);
const inputFocus = useCallback(() => {
inputRef.current?.select();
} , []);
const onPageNoInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value.length <= 0) return;
const pageNo = parseInt(e.target.value, 10);
if (pageNo < 1 || pageNo > props.pageCount || pageNo === props.currentPage) return;
props.onChange(pageNo);
}, [props.onChange, props.pageCount, props.currentPage]);
useEffect(() => {
inputRef.current.value = props.currentPage.toString();
} , [props.currentPage]);
return (<Group size={props.size || 1}>
<FontAwesomeIcon icon={faAngleLeft} title="Previous Page" style={{ cursor: 'pointer' }} onClick={() => props.onChange(props.currentPage - 1)} />
<GoToInput onChange={onPageNoInput} placeholder="Page" ref={inputRef} onFocus={inputFocus} />
<FontAwesomeIcon icon={faAngleRight} title="Next Page" style={{ cursor: 'pointer' }} onClick={() => props.onChange(props.currentPage + 1)} />
</Group>);
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import styled from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPrint, faDownload, IconDefinition } from '@fortawesome/free-solid-svg-icons';
import { faPrint, faDownload, faSquareArrowUpRight, faXmark, IconDefinition } from '@fortawesome/free-solid-svg-icons';
import { IconButtonProps } from '../types';
@@ -43,6 +43,18 @@ function BaseButton({ onClick, icon, name, size, color, hoverColor }: BaseButton
);
}
export function OpenLinkButton({ onClick, size, color }: IconButtonProps) {
return (
<BaseButton onClick={onClick} icon={faSquareArrowUpRight} name='Open in another app' size={size} color={color} />
);
}
export function CloseButton({ onClick, size, color }: IconButtonProps) {
return (
<BaseButton onClick={onClick} icon={faXmark} name='Close' size={size} color={color} hoverColor={'red'} />
);
}
export function DownloadButton({ onClick, size, color }: IconButtonProps) {
return (
<BaseButton onClick={onClick} icon={faDownload} name='Download' size={size} color={color} />

View File

@@ -69,7 +69,7 @@ export default function(link: Link, options: Options, linkIndexes: LinkIndexes)
return `<iframe src="${src}" x-url="${escapedResourcePath}"
x-appearance="${options.theme.appearance}" ${anchorPageNo ? `x-anchorPage="${anchorPageNo}"` : ''} id="${id}"
x-type="mini"
x-type="mini" x-resourceid="${resourceId}"
class="media-player media-pdf"></iframe>`;
}

File diff suppressed because one or more lines are too long

View File

@@ -46,7 +46,7 @@
"markdown-it-sup": "^1.0.0",
"markdown-it-toc-done-right": "^4.1.0",
"md5": "^2.2.1",
"mermaid": "^8.13.9"
"mermaid": "^9.1.7"
},
"gitHead": "eb4b0e64eab40a51b0895d3a40a9d8c3cb7b1b14"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.9.0",
"version": "2.9.1",
"private": true,
"scripts": {
"start-dev": "yarn run build && JOPLIN_IS_TESTING=1 nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",

View File

@@ -1,19 +1,24 @@
import markdownUtils from '@joplin/lib/markdownUtils';
import config from '../../config';
import { EmailSubjectBody } from '../../models/EmailModel';
import { stripePortalUrl } from '../../utils/urlUtils';
import { stripePortalUrl, helpUrl } from '../../utils/urlUtils';
export default (): EmailSubjectBody => {
return {
subject: `Your ${config().appName} payment could not be processed`,
body: `
Your last ${config().appName} payment could not be processed. As a result your account has disabled.
Your last ${config().appName} payment could not be processed. As a result your account has been disabled.
To re-activate your account, please update your payment details, or contact us for more details.
To re-activate your account, please update your payment details.
[Manage your subscription](${markdownUtils.escapeLinkUrl(stripePortalUrl())})
Following this link to [manage your subscription](${markdownUtils.escapeLinkUrl(stripePortalUrl())}).
For more information please see the [help page](${helpUrl()}).
Thank you,
Joplin Cloud Team
`.trim(),
};
};

View File

@@ -1,7 +1,7 @@
import markdownUtils from '@joplin/lib/markdownUtils';
import config from '../../config';
import { EmailSubjectBody } from '../../models/EmailModel';
import { stripePortalUrl } from '../../utils/urlUtils';
import { stripePortalUrl, helpUrl } from '../../utils/urlUtils';
export default function(): EmailSubjectBody {
return {
@@ -13,7 +13,7 @@ We were not able to process your last payment. Please follow this URL to update
[Manage your subscription](${markdownUtils.escapeLinkUrl(stripePortalUrl())})
Please answer this email if you have any question.
For more information please see the [help page](${helpUrl()}).
Thank you,

View File

@@ -71,6 +71,7 @@ async function main() {
}
if (require.main === module) {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
main().catch((error) => {
console.error('Fatal error');
console.error(error);

View File

@@ -87,6 +87,7 @@ async function start(): Promise<void> {
}
if (require.main === module) {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
start().catch((error) => {
console.error('Fatal error');
console.error(error);

View File

@@ -48,6 +48,7 @@ const main = async () => {
};
if (require.main === module) {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
main().catch((error) => {
console.error(error);
process.exit(1);

View File

@@ -7,6 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2022-10-06 19:20+0300\n"
"Last-Translator: mrkaato0\n"
"Language-Team: \n"
"Language: fi_FI\n"
@@ -14,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.0.1\n"
"X-Generator: Poedit 3.1.1\n"
#: packages/app-mobile/components/screens/ConfigScreen.tsx:560
msgid "- Camera: to allow taking a picture and attaching it to a note."
@@ -29,7 +31,7 @@ msgid ""
"- Storage: to allow attaching files to notes and to enable filesystem "
"synchronisation."
msgstr ""
"- Tallennustila: sallia tiedostojen liittäminen muistiinpanoihin ja ottaa "
"- Tallennustila: sallia tiedostojen liittämisen muistiinpanoihin ja ottaa "
"tiedostojärjestelmän synkronointi käyttöön."
#: packages/lib/services/KeymapService.ts:314
@@ -296,9 +298,8 @@ msgid "Also displays unset and hidden config variables."
msgstr "Näyttää myös merkitsemättömät ja piilotetut konfigurointimuuttujat."
#: packages/app-desktop/gui/ShareNoteDialog.tsx:203
#, fuzzy
msgid "Also publish linked notes"
msgstr "Peruuta muistiinpanon julkaisu"
msgstr "Julkaise myös linkitetyt muistiinpanot"
#: packages/lib/models/Setting.ts:713
msgid "Always"
@@ -345,7 +346,7 @@ msgstr "Liitä valokuva"
#: packages/app-mobile/components/screens/Note.tsx:892
msgid "Attach..."
msgstr "Liitä..."
msgstr "Liitä..."
#: packages/app-cli/app/command-attach.js:13
msgid "Attaches the given file to the note."
@@ -866,18 +867,16 @@ msgstr ""
"uudelleen, kun olet muodostanut yhteyden Internetiin."
#: packages/app-desktop/gui/PromptDialog.min.js:235
#, fuzzy
msgid "Create"
msgstr "Luotu"
msgstr "Luo"
#: packages/app-mobile/components/note-list.js:101
msgid "Create a notebook"
msgstr "Luo muistikirja"
#: packages/app-desktop/gui/MainScreen/commands/addProfile.ts:9
#, fuzzy
msgid "Create new profile..."
msgstr "Luo uuden muistiinpanon."
msgstr "Luo uusi profiili..."
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:161
msgid "Create notebook"
@@ -1325,7 +1324,7 @@ msgstr "Muokkaa muistikirjaa"
#: packages/app-desktop/commands/editProfileConfig.ts:9
msgid "Edit profile configuration..."
msgstr ""
msgstr "Muokkaa profiilin asetuksia..."
#: packages/app-desktop/gui/NoteContentPropertiesDialog.tsx:138
#: packages/lib/models/Setting.ts:876 packages/lib/models/Setting.ts:877
@@ -1355,9 +1354,8 @@ msgstr "Editorin monospace fonttiperhe"
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:100
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:96
#, fuzzy
msgid "Editor: %s"
msgstr "Editoija"
msgstr "Editori: %s"
#: packages/app-cli/app/command-ls.js:31
msgid "Either \"text\" or \"json\""
@@ -1671,9 +1669,8 @@ msgid "File system"
msgstr "Tiedostojärjestelmä"
#: packages/app-mobile/components/screens/NoteTagsDialog.js:190
#, fuzzy
msgid "Filter tags"
msgstr "Uudet tunnisteet:"
msgstr "Suodata tunnisteet"
#: packages/app-desktop/gui/ExtensionBadge.min.js:10
msgid "Firefox Extension"
@@ -2040,7 +2037,7 @@ msgstr "Virheellinen asetusarvo: \"%s\". Mahdolliset arvot ovat: %s."
#: packages/app-cli/app/command-e2ee.ts:46
msgid "Invalid password"
msgstr "Väärä salasana"
msgstr "Virheellinen salasana"
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:38
msgid "Italic"
@@ -2287,7 +2284,7 @@ msgstr "Pääsalasanan hallinta"
#: packages/lib/commands/openMasterPasswordDialog.ts:6
msgid "Manage master password..."
msgstr "Hallitse pääsalasanaa..."
msgstr "Pääsalasanan hallinta..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx:314
msgid "Manage your plugins"
@@ -2563,7 +2560,7 @@ msgstr "Huomautus on tallennettu."
#: packages/app-desktop/gui/NotePropertiesDialog.min.js:34
#: packages/lib/models/Setting.ts:2303
msgid "Note History"
msgstr "Muistiinpano historia"
msgstr "Muistiinpanohistoria"
#: packages/app-cli/app/command-done.js:21
msgid "Note is not a to-do: \"%s\""
@@ -2793,6 +2790,7 @@ msgid ""
"Please click on \"%s\" to proceed, or set the passwords in the \"%s\" list "
"below."
msgstr ""
"Jatka napsauttamalla \"%s\" tai aseta salasana oheiseen \"%s\" luetteloon."
#: packages/lib/components/EncryptionConfigScreen/utils.ts:65
msgid ""
@@ -2952,9 +2950,8 @@ msgid "Profile"
msgstr "Profiili"
#: packages/app-desktop/gui/MainScreen/commands/addProfile.ts:17
#, fuzzy
msgid "Profile name:"
msgstr "Profiili"
msgstr "Profiilin nimi:"
#: packages/lib/versionInfo.ts:26
msgid "Profile Version: %s"
@@ -3181,9 +3178,8 @@ msgstr "Tallenna hälytys"
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:100
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:93
#, fuzzy
msgid "Save as %s"
msgstr "Tallenna nimellä..."
msgstr "Tallenna nimellä %s"
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:80
msgid "Save as..."
@@ -3553,9 +3549,8 @@ msgid "Switch between note and to-do type"
msgstr "Vaihda muistiinpanon ja tehtävätyypin välillä"
#: packages/app-desktop/gui/MenuBar.tsx:437
#, fuzzy
msgid "Switch profile"
msgstr "Vie profiili"
msgstr "Vaihda profiili"
#: packages/app-desktop/gui/utils/NoteListUtils.ts:105
msgid "Switch to note type"
@@ -3564,9 +3559,8 @@ msgstr "Vaihda muistiinpanotyyppiin"
#: packages/app-desktop/commands/switchProfile1.ts:7
#: packages/app-desktop/commands/switchProfile2.ts:7
#: packages/app-desktop/commands/switchProfile3.ts:7
#, fuzzy
msgid "Switch to profile %d"
msgstr "Vaihda muistiinpanotyyppiin"
msgstr "Vaihda profiiliin %d"
#: packages/app-desktop/gui/utils/NoteListUtils.ts:114
msgid "Switch to to-do type"
@@ -3662,7 +3656,7 @@ msgstr "Tabloid"
#: packages/app-mobile/components/screens/NoteTagsDialog.js:179
msgid "tag1, tag2, ..."
msgstr ""
msgstr "tunniste1, tunniste2, ..."
#: packages/app-cli/app/command-import.js:52
#: packages/app-desktop/gui/ImportScreen.min.js:73
@@ -3703,7 +3697,7 @@ msgstr ""
#: packages/app-desktop/app.ts:332
msgid ""
"The application did not close properly. Would you like to start in safe mode?"
msgstr ""
msgstr "Sovellus ei sulkeutunut kunnolla. Haluatko aloittaa vikasietotilassa?"
#: packages/lib/onedrive-api-node-utils.js:86
msgid ""
@@ -4545,9 +4539,8 @@ msgid "Your data is going to be re-encrypted and synced again."
msgstr "Tietosi salataan ja synkronoidaan uudelleen."
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:271
#, fuzzy
msgid "Your password is needed to decrypt some of your data."
msgstr "Pääsalasanaa tarvitaan joidenkin tietojen salauksen purkamiseen."
msgstr "Salasanaasi tarvitaan joidenkin tietojen salauksen purkamiseen."
#: packages/app-cli/app/command-sync.ts:242
msgid ""
@@ -4574,10 +4567,6 @@ msgstr "Lähennä"
msgid "Zoom Out"
msgstr "Loitonna"
#, fuzzy
#~ msgid "Save as SVG"
#~ msgstr "Tallenna nimellä..."
#~ msgid "Please click on \"%s\" to proceed"
#~ msgstr "Napsauta \"%s\" jatkaaksesi"

View File

@@ -12,6 +12,7 @@ async function main() {
}
if (require.main === module) {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
main().catch((error) => {
console.error('Fatal error');
console.error(error);

View File

@@ -1,5 +1,17 @@
# Joplin Android app changelog
## [android-v2.9.3](https://github.com/laurent22/joplin/releases/tag/android-v2.9.3) (Pre-release) - 2022-10-07T11:12:56Z
- Improved: Convert empty bolded regions to bold-italic regions in beta editor (#6807) (#6808 by Henry Heino)
- Improved: Increase the attachment size limit to 200MB (#6848 by Self Not Found)
- Improved: Show client ID in log (#6897 by Self Not Found)
- Improved: Supports attaching multiple files to a note at once (#6831 by Self Not Found)
- Improved: Update Mermaid 8.13.9 to 9.1.7 (#6849 by Helmut K. C. Tessarek)
- Fixed: Double/triple-tap selection doesn't show context menu (#6803) (#6802 by Henry Heino)
- Fixed: Fix multiple webview instances (#6841 by Henry Heino)
- Fixed: Fix resources sync when proxy is set (#6817) (#6688 by Self Not Found)
- Fixed: Fixed crash when trying to move note to notebook (#6898)
## [android-v2.9.2](https://github.com/laurent22/joplin/releases/tag/android-v2.9.2) (Pre-release) - 2022-09-01T11:14:58Z
- New: Add Markdown toolbar (#6753 by Henry Heino)

View File

@@ -1,5 +1,21 @@
# Joplin Server Changelog
## [server-v2.9.1](https://github.com/laurent22/joplin/releases/tag/server-v2.9.1) - 2022-10-07T10:46:43Z
- New: Add support for sidebar in user pages (053dbab)
- Improved: Automatically delete expired sessions (d5dfecc)
- Improved: Cannot sort user deletions by email (8e18024)
- Improved: Do not make checkboxes in published notes clickable (cb637e8)
- Improved: Improve admin email UI (e3c9bcb)
- Improved: Process user deletions once an hour (f99b8df)
- Improved: Use apt to install tini to enable multi-platform support (#6097 by Erik Thomsen)
- Fixed: Could not manually start task (#6491)
- Fixed: Fixed Unsupported File Type error when sharing certain notes (#6531)
- Fixed: Fixed removal of user deletion tasks (8f8cc12)
- Fixed: Fixed sidebar menu selection (422a5bf)
- Fixed: Fixed user deletion schedule (bfe5ee8)
- Fixed: Published note must be scrollable when it contains a large table (#6370)
## [server-v2.7.4](https://github.com/laurent22/joplin/releases/tag/server-v2.7.4) - 2022-02-02T19:23:34Z
- New: Add task to automate deletion of disabled accounts (1afcb27)

View File

@@ -2,6 +2,16 @@
Coding style is mostly enforced by a pre-commit hook that runs `eslint`. This hook is installed whenever running `yarn install` on any of the application directory. If for some reason the pre-commit hook didn't get installed, you can manually install it by running `yarn install` at the root of the repository.
## Enforcing rules using eslint
Whenever possible, coding style should be enforced using an eslint rule. To do so, add the relevant rule or plugin to `eslintrc.js`. To manually run the linter, run `yarn run linter ./` from the root of the project.
When adding a rule, you will often find that many files will no longer pass the linter. In that case, you have two options:
- Fix the files one by one. If there aren't too many files, and the changes are simple (they are unlikely to introduce regressions), this is the preferred solution.
- Or use `yarn run linter-interactive ./` to disable existing errors. The interactive tool will process all the files and you can then choose to disable any existing error that it finds (by adding a `eslint-disable-next-line` comment above it). This allows keeping the existing, working codebase as it is, and enforcing that new code follows the rule. When using this method, add the comment "Old code before rule was applied" so that we can easily find back all the lines that have been automatically disabled.
## Use TypeScript for new files
### Creating a new `.ts` file

View File

@@ -107,7 +107,7 @@ Unfortunately it is not possible. Joplin synchronises with file systems using an
The end to end encryption that Joplin implements is to protect the data during transmission and on the cloud service so that only you can access it.
On the local device it is assumed that the data is safe due to the OS built-in security features. If additional security is needed it's always possible to put the notes on an encrypted Truecrypt drive for instance.
On the local device it is assumed that the data is safe due to the OS built-in security features. If additional security is needed it's always possible to put the notes on an encrypted VeraCrypt drive for instance.
For these reasons, because the OS or yourself can easily protect the local data, no PIN or password is currently supported to access Joplin.

View File

@@ -30,7 +30,7 @@ This is a quick summary of the Markdown syntax.
### Tables
Tables are created using pipes `|` and and hyphens `-`. This is a Markdown table:
Tables are created using pipes `|` and hyphens `-`. This is a Markdown table:
| First Header | Second Header |
| ------------- | ------------- |

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