1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-04-02 10:51:52 +02:00

Compare commits

...

178 Commits

Author SHA1 Message Date
Laurent Cozic
f6cd988939 disable edition if no multi profiles 2022-04-11 16:36:23 +01:00
Laurent Cozic
a99eec7cdf 1 based 2022-04-11 16:24:41 +01:00
Laurent Cozic
7ef09dfa77 enter to valid 2022-04-11 16:19:27 +01:00
Laurent Cozic
b11573a2a7 Disable welcome notes 2022-04-11 12:30:54 +01:00
Laurent Cozic
f4034b1ff0 Merge branch 'dev' into multi_profiles 2022-04-11 12:22:48 +01:00
Henry Heino
58bf93a112 iOS: Fixes #6318: Remove white border around Beta Editor (#6326) 2022-04-11 11:57:49 +01:00
Henry Heino
5962b0813e Mobile: Fixes #6324: Support inserting attachments from Beta Editor (#6325) 2022-04-11 11:56:45 +01:00
Ayush Srivastava
cffea3ea1e Mobile: Fixes #3564: "Move Note" dropdown menu can be very narrow (#6306) 2022-04-11 11:53:20 +01:00
Laurent Cozic
dfadacd7f4 save settings 2022-04-10 19:13:01 +01:00
Laurent Cozic
ecc7b17708 loading settings 2022-04-10 18:19:56 +01:00
Laurent Cozic
ee6ab55649 setting loading 2022-04-10 16:50:11 +01:00
Laurent Cozic
b0d64e2f51 Merge branch 'dev' into multi_profiles 2022-04-10 15:23:19 +01:00
Kenichi Kobayashi
f6e21e0180 Desktop: Fixes #6074: Scroll jumps when typing if heavy scripts or many large elements are used (#6383) 2022-04-10 11:31:17 +01:00
reportxx
e02422070e Update Swedish translation (#6382) 2022-04-10 11:30:03 +01:00
Tolulope Malomo
727d64b646 Android: Fixes #6026: Long path in "Export profile" prevents tapping OK button (#6359) 2022-04-10 11:22:30 +01:00
Henry Heino
23e54a60d9 Android: Fixes #5987: Cursor hard to see in dark mode (#6307) 2022-04-10 10:58:11 +01:00
ScriptInfra
0d4978223e Update README.md (#6295) 2022-04-10 10:53:44 +01:00
Mayank Bondre
0b32a29cce Plugins: Resolves #5867: Add support for "categories" manifest field (#6109) 2022-04-10 10:52:31 +01:00
Laurent Cozic
f322d40910 tests 2022-04-09 18:29:20 +01:00
Laurent Cozic
557cb9a6c3 edit profile 2022-04-09 17:38:39 +01:00
Laurent Cozic
d5a55c7908 ui 2022-04-09 17:21:48 +01:00
Laurent Cozic
7308bbd3ca switch logic 2022-04-09 16:42:49 +01:00
Laurent Cozic
c1e8f9befd Multi profile support 2022-04-09 15:37:14 +01:00
Laurent Cozic
a0d77d10ba Tools: Allow setting website build environment from config file 2022-04-09 14:43:59 +01:00
Joplin Bot
bdd9c6cf35 Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-04-09 12:19:45 +00:00
Laurent Cozic
f2bfa30e04 Api: Fixed updating resource content 2022-04-09 11:58:08 +01:00
Laurent Cozic
8077117e65 Doc: Ignore latest post in updateNews script 2022-04-08 13:16:43 +01:00
Laurent Cozic
7e8927398a Doc: Fixed typo 2022-04-07 19:19:53 +01:00
Laurent Cozic
09dcee876c Doc: Fixed env 2022-04-07 19:04:36 +01:00
Laurent Cozic
23b56f4f70 Tools: Fixed script name 2022-04-07 16:00:00 +01:00
Laurent Cozic
b3d09ce776 Doc: Add Joplin Cloud Teams offer to website 2022-04-07 15:35:15 +01:00
Laurent Cozic
84d40b805e Tools: Added tool to automatically post news from local Markdown folder to forum 2022-04-07 15:15:48 +01:00
Laurent Cozic
c097a82b7b Doc: Fixed news title 2022-04-07 11:07:48 +01:00
Joplin Bot
dfa22b560e Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-04-06 06:17:47 +00:00
Joplin Bot
7d31a3fe90 Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-04-06 00:38:27 +00:00
妙呀
79aabc2d06 updata zh_CN.po (#6355) 2022-04-05 19:17:20 +01:00
rnbastos
70e82ca64f Update pt_BR.po (#6353) 2022-04-05 19:16:56 +01:00
Laurent Cozic
a0662412b2 Tools: Removed Windows build from CI for now - discontinued by GitHub 2022-04-05 17:30:27 +01:00
Laurent Cozic
220b48ef02 Doc: Add news about GSoC Contributor Proposals phase 2022-04-05 15:47:22 +01:00
Laurent Cozic
cb637e817b Server: Do not make checkboxes in published notes clickable 2022-04-05 15:42:06 +01:00
Laurent Cozic
27198a16a4 Chore: Make it easier to test note publishing on desktop 2022-04-05 15:37:57 +01:00
Laurent Cozic
1a5bff3bf4 Doc: Move info to Joplin Cloud FAQ 2022-04-05 15:16:48 +01:00
Laurent Cozic
571147acbb Tools: Fixed git changelog tool 2022-04-03 19:27:10 +01:00
Laurent Cozic
9d9420a35c Desktop: Support for Joplin Cloud recursive linked notes 2022-04-03 19:19:24 +01:00
kik0220
a79bc69604 Translation: Update ja_JP.po (#6345)
Co-authored-by: kik0220 <kik0220@gmail.com>
2022-03-31 10:50:29 +01:00
Bernard Tyers
3153d3a1b6 Doc: Improve debugging.md (#6342) 2022-03-29 19:25:27 +01:00
Joplin Bot
df8c265ee4 Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-03-28 18:20:22 +00:00
Laurent Cozic
3725b14e04 Revert "Desktop: Fixes #5686: Fixed Tags Order (#6136)"
This reverts commit 07f128ae95.

Due to regression: https://github.com/laurent22/joplin/issues/6301
2022-03-28 17:40:51 +01:00
Retrove
a73d822998 Desktop, Cli: Fixes #6197: Fixed creation of empty notebooks when importing directory of files (#6274) 2022-03-28 17:13:13 +01:00
asrient
a62e1fba96 Desktop: Resolves #6100: Allow saving a Mermaid graph as a PNG or SVG via context menu (#6126) 2022-03-28 17:10:29 +01:00
Laurent Cozic
37d51c3b58 Plugins: Allow updating a resource via the data API 2022-03-28 16:35:41 +01:00
Laurent Cozic
d5dfecc19f Server: Automatically delete expired sessions 2022-03-28 15:51:44 +01:00
Laurent Cozic
8f8cc12d79 Server: Fixed removal of user deletion tasks 2022-03-28 15:51:43 +01:00
Laurent Cozic
8e1802409f Server: Cannot sort user deletions by email 2022-03-28 15:51:43 +01:00
reportxx
42b2f2146c Update Swedish translation (#6331)
* Update Swedish translation

Please merge to update the Swedish translation.

* Update sv.po
2022-03-27 14:19:10 +01:00
mrkaato0
2ba1563d92 Update fi_FI.po (#6323) 2022-03-27 14:19:02 +01:00
Milo Ivir
a679b21119 Update Crotian translation (#6319) 2022-03-27 14:18:53 +01:00
Xavi Ivars
dd10b6ac65 Update ca.po (#6308) 2022-03-23 19:54:22 +00:00
Laurent
32600df7ce Update pull_request_guidelines.md 2022-03-23 18:24:58 +00:00
Laurent
8a1cfabfc6 Doc: Allow plugins for GSoC 2022-03-23 18:17:43 +00:00
Joplin Bot
1b2046f2fa Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-03-20 00:37:39 +00:00
Laurent Cozic
a1d5168918 Doc: Updated PR guideline for GSoC 2022-03-18 11:44:49 +00:00
Bishoy
047c1fb1a5 Desktop: Fixes right click menu on Markdown Editor (#6132) 2022-03-18 11:07:59 +00:00
Andrew
e83d662555 Doc: Update docker-compose.server.yml (#6283) 2022-03-18 11:06:40 +00:00
Joplin Bot
5c3b2671bf Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-03-17 18:17:04 +00:00
Laurent
02a2dce605 Update pull_request_guidelines.md 2022-03-16 12:07:35 +00:00
Laurent
c4e158d0fb Doc: Slightly relaxed rule 5 of GSoC guidelines 2022-03-16 12:04:40 +00:00
Daniel Aleksandersen
fa8a1c2122 Desktop: Resolves #4155: Don’t unpin app from taskbar on update (#6271) 2022-03-15 10:06:00 +00:00
JackGruber
3f732939d0 All: Resolves #6266: Make search engine filter keywords case insensitive (#6267) 2022-03-15 10:03:56 +00:00
Ayush Srivastava
fb8886db4b Mobile: Color of Date-Time text changed to match theme (#6279) 2022-03-15 10:00:17 +00:00
Laurent Cozic
eb86e9c896 Doc: Fixed GSoC links 2022-03-14 11:02:03 +00:00
PackElend
85e3a44276 Doc: fixed link togeneral Summer of Code introduction (#6269) 2022-03-13 14:44:31 +00:00
Laurent Cozic
addcfb0129 All: Fixes #6261: Ensure that note revision markup type is set correctly 2022-03-12 16:24:07 +00:00
Laurent Cozic
f4ec73ab0e Desktop release v2.7.14 2022-03-12 15:59:09 +00:00
Laurent Cozic
19ba939f0f Doc: Added doc about desktop application styling 2022-03-12 13:38:52 +00:00
Laurent Cozic
780b58ada4 Doc: Remove option to create a plugin as first GSoC contribution 2022-03-12 10:33:03 +00:00
Joplin Bot
b3aed81bea Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-03-11 12:19:26 +00:00
Laurent Cozic
74dd2d1194 Doc: Fixed Twitter OpenGraph tags 2022-03-10 17:49:50 +00:00
Laurent Cozic
3c11445db0 Doc: Fixed news dates 2022-03-10 17:43:06 +00:00
Laurent Cozic
50890a7b2b Tools: Added tool to get the list of package licenses 2022-03-10 17:29:28 +00:00
Laurent
dc60da219a Update CONTRIBUTING.md 2022-03-09 14:56:55 +00:00
Laurent Cozic
bef93e1375 Doc: Update test unit documentation 2022-03-09 14:51:05 +00:00
Daeraxa
45e02a6b3f Doc: Update theme of index and a typo correction (#6245) 2022-03-08 22:52:33 +00:00
Laurent
0c5f64207f Update technical_spec.md 2022-03-08 22:50:21 +00:00
Laurent
8e88686bb1 Doc: Make it clear that test units are not optional for GSoC 2022-03-08 22:49:10 +00:00
Joplin Bot
9594508c73 Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-03-08 14:38:05 +00:00
Laurent Cozic
de7d3b6b8a Doc: Update for GSoC 2022 2022-03-08 14:27:00 +00:00
Laurent Cozic
f5b6398d07 Doc: Add GSoC 2O22 annoucement 2022-03-08 14:27:00 +00:00
Joplin Bot
62a5cf520a Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-03-08 14:10:52 +00:00
Laurent Cozic
90bfffed0f Doc: Add GSoC 2O22 annoucement 2022-03-08 13:58:41 +00:00
Laurent Cozic
a5b0d594c3 Doc: Updated GSoC 2022 mentors 2022-03-07 12:02:00 +00:00
Laurent Cozic
1547256af4 Doc: Update GSoC 2022 themes 2022-03-07 11:44:36 +00:00
Shing Lyu
c3e7fcb471 Mobile: Fix: the camera button remains clickable after taking a photo bug (#6222) 2022-03-07 10:25:08 +00:00
Roman Musin
261302d5c4 Doc: Update BUILD.md (#6241)
Add a note that the path may not contain spaces
2022-03-07 10:23:06 +00:00
Laurent
38e65253ad Tools: Add fastlane structure for F-Droid support (#6224) 2022-03-07 10:22:27 +00:00
Laurent Cozic
eb8d0daa3d Fixed tests 2022-03-07 10:19:47 +00:00
Joplin Bot
5a6c03fbac Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-03-06 18:15:01 +00:00
Laurent Cozic
21f9dab908 Doc: add sponsor 2022-03-06 12:37:05 +00:00
Harris Arvanitis
6dba4730b0 Translation: Update el_GR.po (#6229) 2022-03-03 18:22:40 +00:00
Laurent Cozic
9599d4ef7e Doc: Fixed news 2022-03-03 14:09:03 +00:00
Laurent Cozic
252f08e828 Doc: Improved rendering of news items 2022-03-03 14:06:37 +00:00
Laurent Cozic
94dc216add Desktop: Fixes #6214: Undo and redo on note title did not work in some cases 2022-03-03 13:53:11 +00:00
Laurent Cozic
32de63fad3 Desktop: Fixes #5584: Clicking on folder button was no longer jumping to the right folder
Regression introduced by https://github.com/laurent22/joplin/issues/4697
2022-03-03 13:33:52 +00:00
Laurent Cozic
299a14755a All: Resolves #6209: Handle invalid revision patches 2022-03-03 13:20:29 +00:00
Laurent Cozic
053dbabf74 Server: Add support for sidebar in user pages 2022-03-03 11:33:46 +00:00
OmGole
07f128ae95 Desktop: Fixes #5686: Fixed Tags Order (#6136) 2022-03-02 18:11:14 +00:00
Shing Lyu
98f9ed641c Clipper: Add a button to open the newly clipped note (#6204) 2022-03-02 17:42:29 +00:00
Shing Lyu
d814fd6965 Mobile: Allow filtering tags in tag dialog (#6221) 2022-03-02 17:41:07 +00:00
Shunya Ueta
d44aade075 Doc: update pre-release docs (#6225) 2022-03-02 17:34:34 +00:00
Joplin Bot
4884c9ef87 Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-03-01 18:14:23 +00:00
Daeraxa
3e518ff2c7 Doc: Replaced Privacy welcome note with website privacy md (#6208) 2022-03-01 15:07:29 +00:00
Alexander Hagen Huse
195c84ff59 Update translation [nb_NO] (#6220)
Update nb_NO.po
2022-03-01 14:51:06 +00:00
asahiocean
6881259501 Update ru_RU.po (#6215) 2022-03-01 14:50:43 +00:00
reportxx
6d7f6e29bd Update the Swedish translation (#6207) 2022-03-01 14:50:14 +00:00
Joplin Bot
dc991d4c0f Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-02-27 12:17:53 +00:00
Laurent Cozic
88ac664e37 Deskop, Cli: Fixes #6203: Note export could fail in some cases (regression) 2022-02-27 10:49:18 +00:00
Laurent Cozic
20784b0e99 Desktop: Resolves #5531: Prevent certain errors from stopping the revision service 2022-02-27 10:30:40 +00:00
Laurent Cozic
a325bf6dc6 All: Improve error message when revision metadata cannot be decoded, to improve debugging 2022-02-26 18:20:23 +00:00
Laurent Cozic
ff79745d28 Doc: Added missing hours on GSoC idea list 2022-02-26 10:03:06 +00:00
Laurent Cozic
82109a3132 Doc: Add mentors and complete expected outcomes and project sizes for GSoC 2022 ideas 2022-02-26 09:45:18 +00:00
Helmut K. C. Tessarek
4e901436cc All: Translation: Update da_DK.po (thanks ERYpTION) 2022-02-24 21:03:14 -05:00
Laurent Cozic
f0113c0673 Tools: Add Open Graph tags to website 2022-02-24 19:37:34 +00:00
Joplin Bot
0a1947a712 Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-02-24 18:16:25 +00:00
Laurent Cozic
39aee65eee Doc: Add release 2.7 news item 2022-02-24 17:25:49 +00:00
Laurent Cozic
d37b820bd3 Desktop release v2.7.13 2022-02-24 17:11:14 +00:00
Laurent Cozic
06b95bb718 Update translations 2022-02-24 15:10:03 +00:00
Laurent Cozic
8e87e64dea Tools: Fixed changelog tool 2022-02-23 14:20:38 +00:00
Laurent Cozic
422a5bfa91 Server: Fixed sidebar menu selection 2022-02-23 14:03:02 +00:00
Laurent Cozic
bfe5ee8ba3 Server: Fixed user deletion schedule 2022-02-23 14:03:01 +00:00
Daeraxa
00e0f6b97c Docs: Updated GSoC to replace student refs with contributor (#6180) 2022-02-23 13:44:43 +00:00
Laurent Cozic
46490113a2 Tools: Make server build more configurable 2022-02-23 09:02:13 +00:00
Laurent Cozic
cc69cabf9b Doc: Fixed minor website issues and added Twitter link in mobile view 2022-02-22 19:08:23 +00:00
Laurent Cozic
da88ddb6bd Doc: tweak 2022-02-22 09:35:40 +00:00
Miucci
554fd73054 All: Translation: Update fi_FI.po (#6178) 2022-02-20 14:29:02 -05:00
Laurent
c48a0c6e79 Update CONTRIBUTING.md 2022-02-20 11:21:20 +00:00
Erik Thomsen
f8b7db40c7 Server: Use apt to install tini to enable multi-platform support (#6097) 2022-02-20 11:14:42 +00:00
Laurent
051eac09ec Update ideas.md 2022-02-18 12:23:23 +00:00
Joplin Bot
7584b203fc Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-02-17 18:19:23 +00:00
Laurent Cozic
440618ef71 Desktop: Fixed search marker background color in Markdown editor 2022-02-16 15:52:23 +00:00
Laurent
166d8da81b Doc: Add news item 2022-02-15 14:21:18 +00:00
Joplin Bot
6b7fd24f9e Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-02-14 18:12:54 +00:00
Laurent Cozic
41dd93bc24 iOS 12.7.1 2022-02-14 14:11:23 +00:00
Laurent Cozic
4906087649 Desktop release v2.7.12 2022-02-14 14:08:29 +00:00
Daeraxa
fe95926fa2 Docs: Removed unsupported installation methods and added link to community wiki page (#6158) 2022-02-14 12:32:21 +00:00
Daeraxa
a0fcd240b0 Docs: Add settings sync idea for GSoC 2022 (#6159) 2022-02-14 12:27:13 +00:00
Daeraxa
ae3a4a3027 Mobile: Fixes #5482: Prevent multiline note titles (#6144) 2022-02-13 15:01:43 +00:00
Joplin Bot
5252fdc8ce Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-02-12 18:15:36 +00:00
Laurent Cozic
fe787d1257 Chore: clean up 2022-02-12 17:46:32 +00:00
Laurent Cozic
a70f9b1a13 Desktop: Fixes #6129: Exported JEX notebook should not contain share metadata 2022-02-12 17:44:07 +00:00
Laurent Cozic
ed20604ad2 Android 2.7.2 2022-02-12 12:58:28 +00:00
Laurent Cozic
105a61c1ee Desktop release v2.7.11 2022-02-12 12:26:51 +00:00
Laurent Cozic
38fbaa9acf Update translations 2022-02-12 12:21:40 +00:00
Joplin Bot
0d0231e82c Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-02-12 00:31:27 +00:00
Laurent Cozic
b79270990b Tools: Fixed tests 2022-02-11 19:53:42 +00:00
Laurent Cozic
064891d097 Desktop: Resize custom icon down to 256px when it is too large 2022-02-11 19:24:37 +00:00
Laurent Cozic
9036bc4fd1 Android 2.7.1 2022-02-11 18:24:59 +00:00
Laurent Cozic
0f41a2d00a Desktop release v2.7.10 2022-02-11 11:21:21 +00:00
Laurent Cozic
3ee0c7f440 Desktop release v2.7.9 2022-02-11 11:19:59 +00:00
Laurent Cozic
89ada6432b Desktop: Fixes #6075: Login field was sometimes disabled on Sync Wizard dialog 2022-02-11 11:04:10 +00:00
Laurent Cozic
445d691103 Doc: Update readme intro 2022-02-11 10:38:45 +00:00
Laurent Cozic
de757026d4 All: Fixes #6092: Shared resource was not encrypted with correct encryption key 2022-02-11 10:38:45 +00:00
Helmut K. C. Tessarek
5e8ed8bc24 Desktop: Fixes #6108: Add "Other applications" import menu item (#6118) 2022-02-10 10:54:23 +00:00
Laurent Cozic
0a739e099d Doc: Fixed website links 2022-02-09 15:24:08 +00:00
Laurent Cozic
f3b1f07a75 Doc: Added GSoC 2022 documents 2022-02-09 15:14:57 +00:00
Laurent Cozic
1b91646d92 Doc: Added GSoC 2022 documents 2022-02-09 15:14:00 +00:00
Laurent Cozic
885f0e1557 Desktop: Improved custom icon selection 2022-02-07 17:23:20 +00:00
Peng Ding
2933b7eb2a All: Translation: Update zh_CN.po 2022-02-06 16:32:22 -05:00
Laurent
9f252ea673 Desktop: Add support for custom notebook icons (#6110) 2022-02-06 16:42:00 +00:00
Laurent Cozic
db497ee0a5 Tools: Fixed fork-uslug issue for mobile app 2022-02-06 16:40:28 +00:00
Laurent Cozic
31fcd0ed1d Server: Only use Stripe "customer.subscription.created" event to provision subscriptions 2022-02-05 18:32:45 +00:00
Joplin Bot
5d7d597147 Doc: Updated Markdown files
Auto-updated using release-website.sh
2022-02-05 18:12:49 +00:00
Laurent
6ebec0cbf8 Update sponsors.json 2022-02-05 14:39:10 +00:00
Laurent
f4b39633ea Delete style.min.css 2022-02-05 14:38:30 +00:00
Mr-Kanister
4c8e2fba7d All: Translation: Update de_DE.po (#6102) 2022-02-04 18:26:08 -05:00
Vincent Jo
288ae1b463 All: Add additional time format HH.mm (#6086) 2022-02-03 18:52:24 +00:00
Laurent Cozic
e3c9bcbec6 Server: Improve admin email UI 2022-02-03 11:25:13 +00:00
Laurent Cozic
49180fbf88 Remove printing of version numbers in Docker image 2022-02-03 11:25:12 +00:00
Helmut K. C. Tessarek
486ac6db7b Update translations 2022-02-02 18:42:45 -05:00
250 changed files with 22602 additions and 16656 deletions

View File

@@ -148,6 +148,9 @@ packages/app-desktop/checkForUpdates.js.map
packages/app-desktop/commands/copyDevCommand.d.ts
packages/app-desktop/commands/copyDevCommand.js
packages/app-desktop/commands/copyDevCommand.js.map
packages/app-desktop/commands/editProfileConfig.d.ts
packages/app-desktop/commands/editProfileConfig.js
packages/app-desktop/commands/editProfileConfig.js.map
packages/app-desktop/commands/exportFolders.d.ts
packages/app-desktop/commands/exportFolders.js
packages/app-desktop/commands/exportFolders.js.map
@@ -175,6 +178,18 @@ packages/app-desktop/commands/startExternalEditing.js.map
packages/app-desktop/commands/stopExternalEditing.d.ts
packages/app-desktop/commands/stopExternalEditing.js
packages/app-desktop/commands/stopExternalEditing.js.map
packages/app-desktop/commands/switchProfile.d.ts
packages/app-desktop/commands/switchProfile.js
packages/app-desktop/commands/switchProfile.js.map
packages/app-desktop/commands/switchProfile1.d.ts
packages/app-desktop/commands/switchProfile1.js
packages/app-desktop/commands/switchProfile1.js.map
packages/app-desktop/commands/switchProfile2.d.ts
packages/app-desktop/commands/switchProfile2.js
packages/app-desktop/commands/switchProfile2.js.map
packages/app-desktop/commands/switchProfile3.d.ts
packages/app-desktop/commands/switchProfile3.js
packages/app-desktop/commands/switchProfile3.js.map
packages/app-desktop/commands/toggleExternalEditing.d.ts
packages/app-desktop/commands/toggleExternalEditing.js
packages/app-desktop/commands/toggleExternalEditing.js.map
@@ -235,6 +250,9 @@ packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
packages/app-desktop/gui/ErrorBoundary.d.ts
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ErrorBoundary.js.map
packages/app-desktop/gui/FolderIconBox.d.ts
packages/app-desktop/gui/FolderIconBox.js
packages/app-desktop/gui/FolderIconBox.js.map
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.d.ts
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js.map
@@ -256,6 +274,9 @@ packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js.map
packages/app-desktop/gui/MainScreen/MainScreen.d.ts
packages/app-desktop/gui/MainScreen/MainScreen.js
packages/app-desktop/gui/MainScreen/MainScreen.js.map
packages/app-desktop/gui/MainScreen/commands/addProfile.d.ts
packages/app-desktop/gui/MainScreen/commands/addProfile.js
packages/app-desktop/gui/MainScreen/commands/addProfile.js.map
packages/app-desktop/gui/MainScreen/commands/commandPalette.d.ts
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
packages/app-desktop/gui/MainScreen/commands/commandPalette.js.map
@@ -487,6 +508,12 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js.map
packages/app-desktop/gui/NoteEditor/utils/contextMenu.d.ts
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js.map
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.d.ts
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js.map
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.d.ts
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js.map
packages/app-desktop/gui/NoteEditor/utils/index.d.ts
packages/app-desktop/gui/NoteEditor/utils/index.js
packages/app-desktop/gui/NoteEditor/utils/index.js.map
@@ -721,6 +748,9 @@ packages/app-desktop/gui/utils/convertToScreenCoordinates.js.map
packages/app-desktop/gui/utils/loadScript.d.ts
packages/app-desktop/gui/utils/loadScript.js
packages/app-desktop/gui/utils/loadScript.js.map
packages/app-desktop/loadResources.testEnv.d.ts
packages/app-desktop/loadResources.testEnv.js
packages/app-desktop/loadResources.testEnv.js.map
packages/app-desktop/plugins/GotoAnything.d.ts
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/plugins/GotoAnything.js.map
@@ -1147,6 +1177,9 @@ packages/lib/models/NoteTag.js.map
packages/lib/models/Resource.d.ts
packages/lib/models/Resource.js
packages/lib/models/Resource.js.map
packages/lib/models/Resource.test.d.ts
packages/lib/models/Resource.test.js
packages/lib/models/Resource.test.js.map
packages/lib/models/ResourceLocalState.d.ts
packages/lib/models/ResourceLocalState.js
packages/lib/models/ResourceLocalState.js.map
@@ -1231,6 +1264,9 @@ packages/lib/services/DecryptionWorker.js.map
packages/lib/services/ExternalEditWatcher.d.ts
packages/lib/services/ExternalEditWatcher.js
packages/lib/services/ExternalEditWatcher.js.map
packages/lib/services/ExternalEditWatcher/utils.d.ts
packages/lib/services/ExternalEditWatcher/utils.js
packages/lib/services/ExternalEditWatcher/utils.js.map
packages/lib/services/ItemChangeUtils.d.ts
packages/lib/services/ItemChangeUtils.js
packages/lib/services/ItemChangeUtils.js.map
@@ -1555,6 +1591,24 @@ packages/lib/services/plugins/utils/validatePluginVersion.js.map
packages/lib/services/plugins/utils/validatePluginVersion.test.d.ts
packages/lib/services/plugins/utils/validatePluginVersion.test.js
packages/lib/services/plugins/utils/validatePluginVersion.test.js.map
packages/lib/services/profileConfig/index.d.ts
packages/lib/services/profileConfig/index.js
packages/lib/services/profileConfig/index.js.map
packages/lib/services/profileConfig/index.test.d.ts
packages/lib/services/profileConfig/index.test.js
packages/lib/services/profileConfig/index.test.js.map
packages/lib/services/profileConfig/initProfile.d.ts
packages/lib/services/profileConfig/initProfile.js
packages/lib/services/profileConfig/initProfile.js.map
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.d.ts
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js.map
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.d.ts
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js.map
packages/lib/services/profileConfig/types.d.ts
packages/lib/services/profileConfig/types.js
packages/lib/services/profileConfig/types.js.map
packages/lib/services/rest/Api.d.ts
packages/lib/services/rest/Api.js
packages/lib/services/rest/Api.js.map
@@ -1633,6 +1687,9 @@ packages/lib/services/searchengine/SearchEngineUtils.js.map
packages/lib/services/searchengine/SearchEngineUtils.test.d.ts
packages/lib/services/searchengine/SearchEngineUtils.test.js
packages/lib/services/searchengine/SearchEngineUtils.test.js.map
packages/lib/services/searchengine/SearchFilter.test.d.ts
packages/lib/services/searchengine/SearchFilter.test.js
packages/lib/services/searchengine/SearchFilter.test.js.map
packages/lib/services/searchengine/filterParser.d.ts
packages/lib/services/searchengine/filterParser.js
packages/lib/services/searchengine/filterParser.js.map
@@ -1960,6 +2017,9 @@ packages/tools/generate-images.js.map
packages/tools/git-changelog.d.ts
packages/tools/git-changelog.js
packages/tools/git-changelog.js.map
packages/tools/licenseChecker.d.ts
packages/tools/licenseChecker.js
packages/tools/licenseChecker.js.map
packages/tools/release-android.d.ts
packages/tools/release-android.js
packages/tools/release-android.js.map
@@ -2002,9 +2062,21 @@ packages/tools/website/build.js.map
packages/tools/website/updateDownloadPage.d.ts
packages/tools/website/updateDownloadPage.js
packages/tools/website/updateDownloadPage.js.map
packages/tools/website/updateNews.d.ts
packages/tools/website/updateNews.js
packages/tools/website/updateNews.js.map
packages/tools/website/utils/frontMatter.d.ts
packages/tools/website/utils/frontMatter.js
packages/tools/website/utils/frontMatter.js.map
packages/tools/website/utils/openGraph.d.ts
packages/tools/website/utils/openGraph.js
packages/tools/website/utils/openGraph.js.map
packages/tools/website/utils/openGraph.test.d.ts
packages/tools/website/utils/openGraph.test.js
packages/tools/website/utils/openGraph.test.js.map
packages/tools/website/utils/parser.d.ts
packages/tools/website/utils/parser.js
packages/tools/website/utils/parser.js.map
packages/tools/website/utils/pressCarousel.d.ts
packages/tools/website/utils/pressCarousel.js
packages/tools/website/utils/pressCarousel.js.map

View File

@@ -38,6 +38,8 @@ echo "GITHUB_REF=$GITHUB_REF"
echo "RUNNER_OS=$RUNNER_OS"
echo "GIT_TAG_NAME=$GIT_TAG_NAME"
echo "BUILD_SEQUENCIAL=$BUILD_SEQUENCIAL"
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
echo "IS_CONTINUOUS_INTEGRATION=$IS_CONTINUOUS_INTEGRATION"
echo "IS_PULL_REQUEST=$IS_PULL_REQUEST"
@@ -169,10 +171,10 @@ cd "$ROOT_DIR/packages/app-desktop"
if [[ $GIT_TAG_NAME = v* ]]; then
echo "Step: Building and publishing desktop application..."
USE_HARD_LINKS=false yarn run dist
elif [[ $GIT_TAG_NAME = server-v* ]] && [[ $IS_LINUX = 1 ]]; then
elif [[ $IS_LINUX = 1 ]] && [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then
echo "Step: Building Docker Image..."
cd "$ROOT_DIR"
yarn run buildServerDocker --tag-name $GIT_TAG_NAME --push-images
yarn run buildServerDocker --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
else
echo "Step: Building but *not* publishing desktop application..."
USE_HARD_LINKS=false yarn run dist --publish=never

View File

@@ -5,7 +5,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-2016]
# Removed windows-2016 for now - discontinued by GitHub
os: [macos-latest, ubuntu-latest]
steps:
# Silence apt-get update errors (for example when a module doesn't
@@ -98,6 +99,8 @@ jobs:
env:
IS_CONTINUOUS_INTEGRATION: 1
BUILD_SEQUENCIAL: 1
SERVER_REPOSITORY: joplin/server
SERVER_TAG_PREFIX: server
run: |
yarn install && cd packages/app-desktop && yarn run dist --publish=never
@@ -138,5 +141,5 @@ jobs:
BUILD_SEQUENCIAL: 1
run: |
yarn install
yarn run buildServerDocker --tag-name server-v0.0.0
yarn run buildServerDocker --tag-name server-v0.0.0 --repository joplin/server

72
.gitignore vendored
View File

@@ -138,6 +138,9 @@ packages/app-desktop/checkForUpdates.js.map
packages/app-desktop/commands/copyDevCommand.d.ts
packages/app-desktop/commands/copyDevCommand.js
packages/app-desktop/commands/copyDevCommand.js.map
packages/app-desktop/commands/editProfileConfig.d.ts
packages/app-desktop/commands/editProfileConfig.js
packages/app-desktop/commands/editProfileConfig.js.map
packages/app-desktop/commands/exportFolders.d.ts
packages/app-desktop/commands/exportFolders.js
packages/app-desktop/commands/exportFolders.js.map
@@ -165,6 +168,18 @@ packages/app-desktop/commands/startExternalEditing.js.map
packages/app-desktop/commands/stopExternalEditing.d.ts
packages/app-desktop/commands/stopExternalEditing.js
packages/app-desktop/commands/stopExternalEditing.js.map
packages/app-desktop/commands/switchProfile.d.ts
packages/app-desktop/commands/switchProfile.js
packages/app-desktop/commands/switchProfile.js.map
packages/app-desktop/commands/switchProfile1.d.ts
packages/app-desktop/commands/switchProfile1.js
packages/app-desktop/commands/switchProfile1.js.map
packages/app-desktop/commands/switchProfile2.d.ts
packages/app-desktop/commands/switchProfile2.js
packages/app-desktop/commands/switchProfile2.js.map
packages/app-desktop/commands/switchProfile3.d.ts
packages/app-desktop/commands/switchProfile3.js
packages/app-desktop/commands/switchProfile3.js.map
packages/app-desktop/commands/toggleExternalEditing.d.ts
packages/app-desktop/commands/toggleExternalEditing.js
packages/app-desktop/commands/toggleExternalEditing.js.map
@@ -225,6 +240,9 @@ packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
packages/app-desktop/gui/ErrorBoundary.d.ts
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ErrorBoundary.js.map
packages/app-desktop/gui/FolderIconBox.d.ts
packages/app-desktop/gui/FolderIconBox.js
packages/app-desktop/gui/FolderIconBox.js.map
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.d.ts
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js.map
@@ -246,6 +264,9 @@ packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js.map
packages/app-desktop/gui/MainScreen/MainScreen.d.ts
packages/app-desktop/gui/MainScreen/MainScreen.js
packages/app-desktop/gui/MainScreen/MainScreen.js.map
packages/app-desktop/gui/MainScreen/commands/addProfile.d.ts
packages/app-desktop/gui/MainScreen/commands/addProfile.js
packages/app-desktop/gui/MainScreen/commands/addProfile.js.map
packages/app-desktop/gui/MainScreen/commands/commandPalette.d.ts
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
packages/app-desktop/gui/MainScreen/commands/commandPalette.js.map
@@ -477,6 +498,12 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js.map
packages/app-desktop/gui/NoteEditor/utils/contextMenu.d.ts
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js.map
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.d.ts
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js.map
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.d.ts
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js.map
packages/app-desktop/gui/NoteEditor/utils/index.d.ts
packages/app-desktop/gui/NoteEditor/utils/index.js
packages/app-desktop/gui/NoteEditor/utils/index.js.map
@@ -711,6 +738,9 @@ packages/app-desktop/gui/utils/convertToScreenCoordinates.js.map
packages/app-desktop/gui/utils/loadScript.d.ts
packages/app-desktop/gui/utils/loadScript.js
packages/app-desktop/gui/utils/loadScript.js.map
packages/app-desktop/loadResources.testEnv.d.ts
packages/app-desktop/loadResources.testEnv.js
packages/app-desktop/loadResources.testEnv.js.map
packages/app-desktop/plugins/GotoAnything.d.ts
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/plugins/GotoAnything.js.map
@@ -1137,6 +1167,9 @@ packages/lib/models/NoteTag.js.map
packages/lib/models/Resource.d.ts
packages/lib/models/Resource.js
packages/lib/models/Resource.js.map
packages/lib/models/Resource.test.d.ts
packages/lib/models/Resource.test.js
packages/lib/models/Resource.test.js.map
packages/lib/models/ResourceLocalState.d.ts
packages/lib/models/ResourceLocalState.js
packages/lib/models/ResourceLocalState.js.map
@@ -1221,6 +1254,9 @@ packages/lib/services/DecryptionWorker.js.map
packages/lib/services/ExternalEditWatcher.d.ts
packages/lib/services/ExternalEditWatcher.js
packages/lib/services/ExternalEditWatcher.js.map
packages/lib/services/ExternalEditWatcher/utils.d.ts
packages/lib/services/ExternalEditWatcher/utils.js
packages/lib/services/ExternalEditWatcher/utils.js.map
packages/lib/services/ItemChangeUtils.d.ts
packages/lib/services/ItemChangeUtils.js
packages/lib/services/ItemChangeUtils.js.map
@@ -1545,6 +1581,24 @@ packages/lib/services/plugins/utils/validatePluginVersion.js.map
packages/lib/services/plugins/utils/validatePluginVersion.test.d.ts
packages/lib/services/plugins/utils/validatePluginVersion.test.js
packages/lib/services/plugins/utils/validatePluginVersion.test.js.map
packages/lib/services/profileConfig/index.d.ts
packages/lib/services/profileConfig/index.js
packages/lib/services/profileConfig/index.js.map
packages/lib/services/profileConfig/index.test.d.ts
packages/lib/services/profileConfig/index.test.js
packages/lib/services/profileConfig/index.test.js.map
packages/lib/services/profileConfig/initProfile.d.ts
packages/lib/services/profileConfig/initProfile.js
packages/lib/services/profileConfig/initProfile.js.map
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.d.ts
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js.map
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.d.ts
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js.map
packages/lib/services/profileConfig/types.d.ts
packages/lib/services/profileConfig/types.js
packages/lib/services/profileConfig/types.js.map
packages/lib/services/rest/Api.d.ts
packages/lib/services/rest/Api.js
packages/lib/services/rest/Api.js.map
@@ -1623,6 +1677,9 @@ packages/lib/services/searchengine/SearchEngineUtils.js.map
packages/lib/services/searchengine/SearchEngineUtils.test.d.ts
packages/lib/services/searchengine/SearchEngineUtils.test.js
packages/lib/services/searchengine/SearchEngineUtils.test.js.map
packages/lib/services/searchengine/SearchFilter.test.d.ts
packages/lib/services/searchengine/SearchFilter.test.js
packages/lib/services/searchengine/SearchFilter.test.js.map
packages/lib/services/searchengine/filterParser.d.ts
packages/lib/services/searchengine/filterParser.js
packages/lib/services/searchengine/filterParser.js.map
@@ -1950,6 +2007,9 @@ packages/tools/generate-images.js.map
packages/tools/git-changelog.d.ts
packages/tools/git-changelog.js
packages/tools/git-changelog.js.map
packages/tools/licenseChecker.d.ts
packages/tools/licenseChecker.js
packages/tools/licenseChecker.js.map
packages/tools/release-android.d.ts
packages/tools/release-android.js
packages/tools/release-android.js.map
@@ -1992,9 +2052,21 @@ packages/tools/website/build.js.map
packages/tools/website/updateDownloadPage.d.ts
packages/tools/website/updateDownloadPage.js
packages/tools/website/updateDownloadPage.js.map
packages/tools/website/updateNews.d.ts
packages/tools/website/updateNews.js
packages/tools/website/updateNews.js.map
packages/tools/website/utils/frontMatter.d.ts
packages/tools/website/utils/frontMatter.js
packages/tools/website/utils/frontMatter.js.map
packages/tools/website/utils/openGraph.d.ts
packages/tools/website/utils/openGraph.js
packages/tools/website/utils/openGraph.js.map
packages/tools/website/utils/openGraph.test.d.ts
packages/tools/website/utils/openGraph.test.js
packages/tools/website/utils/openGraph.test.js.map
packages/tools/website/utils/parser.d.ts
packages/tools/website/utils/parser.js
packages/tools/website/utils/parser.js.map
packages/tools/website/utils/pressCarousel.d.ts
packages/tools/website/utils/pressCarousel.js
packages/tools/website/utils/pressCarousel.js.map

View File

@@ -307,7 +307,7 @@ p,
div.navbar-mobile-content a.sponsor-button {
padding: 4px 12px;
font-size: 0.9em;
margin-right: 1em;
margin-right: 0.5em;
}
#nav-section.white-bg a {
@@ -670,8 +670,8 @@ footer .bottom-links-row p {
margin-right: auto;
}
.news-page img,
.news-item-page img {
.news-page .main-content img,
.news-item-page .main-content img {
max-width: 100%;
}
@@ -740,7 +740,7 @@ footer .bottom-links-row p {
}
#nav-section .button-link {
padding: 4px 12px;
padding: 4px 10px;
font-size: 15px;
}
}
@@ -937,6 +937,25 @@ footer .bottom-links-row p {
}
}
/*****************************************************************
VERY NARROW VIEW
eg for Galaxy Fold
*****************************************************************/
@media (max-width: 350px) {
#nav-section .navbar-mobile-content a.sponsor-button {
background-color: transparent;
color: #0557ba !important;
font-size: 18px;
}
#nav-section .navbar-mobile-content a.sponsor-button .sponsor-button-label {
display: none;
}
}
/*****************************************************************
PLANS PAGE
*****************************************************************/
@@ -1054,6 +1073,10 @@ footer .bottom-links-row p {
display: none;
}
.joplin-cloud-feature-list table {
width: 100%;
}
.price-row .plan-type {
display: flex;
align-items: center;

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -10,8 +10,8 @@
<link rel="icon" href="{{imageBaseUrl}}/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Joplin, the open source note-taking application" />
<link rel="stylesheet" href="{{{assetUrls.css.fontawesome}}}">
{{> openGraphTags}}
<link
rel="stylesheet"
href="{{cssBaseUrl}}/bootstrap5.0.2.min.css"

View File

@@ -23,7 +23,7 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
<link rel="icon" href="{{imageBaseUrl}}/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Joplin, the open source note-taking application" />
{{> openGraphTags}}
<link
rel="stylesheet"
href="{{cssBaseUrl}}/bootstrap5.0.2.min.css"
@@ -65,11 +65,6 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
{{{contentHtml}}}
{{#showBottomLinks}}
<div class="bottom-links">
{{#discussOnForumLink}}
<a class="bottom-link" href="{{{discussOnForumLink}}}">
<i class="fab fa-discourse"></i></i> Discuss on the forum
</a>
{{/discussOnForumLink}}
{{#showImproveThisDoc}}
<a class="bottom-link" href="https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}">
<i class="fab fa-github"></i> Improve this doc

View File

@@ -12,7 +12,7 @@
</a>
</div>
<div class="col-9 text-right d-none d-md-block">
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500"><i class="fab fa-twitter"></i></a>
{{> twitterLink}}
<a href="{{baseUrl}}/news/" class="fw500">News</a>
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
<a href="{{forumUrl}}" class="fw500">Forum</a>
@@ -22,6 +22,7 @@
{{> supportButton}}
</div>
<div class="col-9 text-right d-block d-md-none navbar-mobile-content">
{{> twitterLink}}
{{> supportButton}}
<span class="pointer"

View File

@@ -0,0 +1,14 @@
{{#openGraph}}
<meta name="description" content="{{openGraph.description}}" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@joplinapp" />
<meta property="og:url" content="{{{openGraph.url}}}" />
<meta property="og:title" content="{{openGraph.title}}" />
<meta property="twitter:title" content="{{openGraph.title}}" />
<meta property="og:description" content="{{openGraph.description}}" />
<meta property="twitter:description" content="{{openGraph.description}}" />
{{#openGraph.image}}
<meta property="og:image" content="{{{openGraph.image}}}" />
<meta property="twitter:image" content="{{{openGraph.image}}}" />
{{/openGraph.image}}
{{/openGraph}}

View File

@@ -6,11 +6,11 @@
</div>
<div class="plan-price plan-price-monthly">
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;/month</sub>
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;/month{{#footnote}} (*){{/footnote}}</sub>
</div>
<div class="plan-price plan-price-yearly">
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;/month</sub>
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;/month{{#footnote}} (*){{/footnote}}</sub>
</div>
</div>
@@ -20,17 +20,19 @@
</div>
</div>
{{#featuresOn}}
{{#featureLabelsOn}}
<p><i class="fas fa-check feature feature-on"></i>{{.}}</p>
{{/featuresOn}}
{{/featureLabelsOn}}
{{#featuresOff}}
{{#featureLabelsOff}}
<p class="unchecked-text"><i class="fas fa-times feature feature-off"></i>{{.}}</p>
{{/featuresOff}}
{{/featureLabelsOff}}
<p class="text-center subscribe-wrapper">
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton">{{cfaLabel}}</a>
</p>
{{#footnote}}<sub>(*) {{.}}</sub>{{/footnote}}
</div>
<script>

View File

@@ -1,3 +1,3 @@
<a class="button-link btn-blue sponsor-button" href="{{baseUrl}}/donate">
<i class="fas fa-heart heart-full"></i><i class="far fa-heart heart-line"></i>&nbsp;Support us
<i class="fas fa-heart heart-full"></i><i class="far fa-heart heart-line"></i><span class="sponsor-button-label">&nbsp;Support us</span>
</a>

View File

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

View File

@@ -42,13 +42,23 @@
{{> plan}}
{{/plans.pro}}
{{#plans.business}}
{{#plans.teams}}
{{> plan}}
{{/plans.business}}
{{/plans.teams}}
<p class="joplin-cloud-login-info">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
</div>
<div class="row">
<div>
<h1>Feature comparison</h1>
<div class="joplin-cloud-feature-list">
{{{featureListHtml}}}
</div>
<p>&nbsp;</p>
</div>
</div>
<div class="row">
{{{faqHtml}}}
</div>

View File

@@ -25,6 +25,8 @@ There are also a few forks of existing packages under the "fork-*" name.
## Building
Make sure the path to the project directory does not contain spaces or the build may fail.
Before doing anything else, from the root of the project, run:
yarn install

View File

@@ -38,7 +38,8 @@ If you want to start contributing to the project's code, please follow these gui
- **Changes that will consist of more than 50 lines of code should be discussed on the [Joplin Forum](https://discourse.joplinapp.org/)**, so that you don't spend too much time implementing something that might not be accepted.
- All the applications share the same backend (database, synchronisation, settings, models, business logic, etc.) so if you change something in the backend in one app, make sure it still works in the other apps. Usually it does, but keep this in mind.
- Pull requests that make many changes using an automated tool, like for spell fixing, styling, etc. will not be accepted. An exception would be if the changes have been discussed in the forum and someone has agreed to review **and test** the pull request.
- Pull requests that make address multiple issues will most likely stall and eventually be closed. This is because we might be fine with one of the changes but not with others and untangling that kind of pull request is too much hassle both for maintainers and the person who submitted it. So most of the time someone gives up and the PR gets closed. So please keep the pull request focused on one issue.
- Pull requests that address multiple issues will most likely stall and eventually be closed. This is because we might be fine with one of the changes but not with others and untangling that kind of pull request is too much hassle both for maintainers and the person who submitted it. So most of the time someone gives up and the PR gets closed. So please keep the pull request focused on one issue.
- **Do not mark your reviewer's comments as "resolved"**. If you do that, the comments will be hidden and the reviewer will not know what are the pending issues in the pull request. Only the reviewer should resolve the comments.
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/dev/BUILD.md) for more details.
@@ -52,37 +53,57 @@ For changes made to the Desktop client that affect the user interface, refer to
## Automated tests
When submitting a pull request for a new feature or a bug fix, please add automated tests for your code whenever possible. Tests in Joplin are divided into **unit tests** and **feature tests**.
When submitting a pull request for a new feature or a bug fix, please add automated tests. We use [Jest](https://jestjs.io/) as a testing framework so you will need to be familiar with it or go through their documentation.
* **Unit tests** are used to test models, services or utility classes - they are relatively low level. Unit tests should be prefixed with the type of class that is being tested - for example "models_Folder" or "services_SearchEngine".
### Running the tests
* **Feature tests** on the other hand are to test higher level functionalities such as interactions with the GUI and how they affect the underlying model. Often these tests would dispatch Redux actions, and inspect how the application state has been changed. The feature tests should be prefixed with "feature_", for example "feature_TagList". There's a good explanation on what qualifies as a feature test in [this post](https://github.com/laurent22/joplin/pull/2819#issuecomment-603502230).
The tests are under packages/app-cli/tests. To get them running, you first need to build the CLI app:
```sh
yarn install
cd packages/app-cli
```
To run all the test units:
To run all the test units, run from the root:
```sh
yarn test
```
Or you can go inside a package folder, and run the tests from there. For example to run all the library tests, go in `packages/lib` and run `yarn test`
To run just one particular file:
```sh
yarn test --filter=markdownUtils # Don't add the .js extension
# Run all the tests in markdownUtils.test.ts
yarn test markdownUtils
```
To filter tests. For example, to run all the test units that contain "should handle conflict" in their description:
To run only a particular test in a file:
```sh
yarn test --filter="should handle conflict"
# Run only the test described as "should handle conflict"
# inside markdownUtils.test.ts:
yarn test markdownUtils --filter="should handle conflict"
```
### Adding a new test file
To add a test, simply create a new file with an extension `.test.ts` in the same directory. For example if you are working on the file `example.ts`, create a file `example.test.ts` for the unit tests. If this file already exist, simply add your tests directly to it.
### Setting the testing environment
Many utility functions are available under the package `@joplin/lib/testing/test-utils`. Have a look for example at [Note.test.ts](https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Note.test.ts) to see how to setup test units with database support and synchroniser support. Note that this is not needed for all tests - if you just have a simple functions to test you won't need that extra setup.
### Testing React Hooks
To test React Hooks please use the package `@testing-library/react-hooks`. See [useLayoutItemSizes.test.ts](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.test.ts) for an example.
### If it is not possible to add tests
More often than not, it is actually possible to add tests - just go back to your code and see if it can be refactored and certain functionalities moved to simple functions (with no dependencies). Once you have a simple function, you can easily add unit tests for it.
Additionally, if the unit tests are not sufficient, please provide a **manual testing plan**, which should include detailed steps on:
- How to test that your feature is working. Include at least 5 tests. Try to think of the possible inputs - if it's a list, how does it work with 0 elements, or 1, or 10, or 100,000. If it's a text input, how does it work with an empty string, or a very large string, etc. Basically don't just put one test that check the best case scenario.
- How to verify that related parts of the applications are not broken. For example if you changed the note loading logic, check that the toolbar is still working as expected (and not modifying the previously loaded note for example), check that switching from one note to another still works. Look at the note list and verify that the note title is updated there too, etc.
A reviewer should be able to run the app with your changes, then do the above steps to verify that everything's working as expected.
## About abandoned pull requests
It happens that a pull request is started but not finished and despite our attempts to contact the contributor, we don't hear from them again.

View File

@@ -1,22 +1,17 @@
### Build stage
# =============================================================================
# Build stage
# =============================================================================
FROM node:16-bullseye AS builder
RUN apt-get update \
&& apt-get install -y \
python \
python tini \
&& rm -rf /var/lib/apt/lists/*
# Download the init tool Tini and make it executable for use in the final image
ADD https://github.com/krallin/tini/releases/download/v0.19.0/tini-static /tini
RUN chmod u+x /tini
# Enables Yarn
RUN corepack enable
RUN echo "Node: $(node --version)"
RUN echo "Npm: $(npm --version)"
RUN echo "Yarn: $(yarn --version)"
WORKDIR /build
COPY .yarn/plugins ./.yarn/plugins
@@ -51,7 +46,11 @@ RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds \
&& yarn cache clean \
&& rm -rf .yarn/berry
### Final image
# =============================================================================
# Final stage - we copy only the relevant files from the build stage and start
# from a smaller base image.
# =============================================================================
FROM node:16-bullseye-slim
ARG user=joplin
@@ -60,7 +59,7 @@ RUN useradd --create-home --shell /bin/bash $user
USER $user
COPY --chown=$user:$user --from=builder /build/packages /home/$user/packages
COPY --chown=$user:$user --from=builder /tini /usr/local/bin/tini
COPY --chown=$user:$user --from=builder /usr/bin/tini /usr/local/bin/tini
ENV NODE_ENV=production
ENV RUNNING_IN_DOCKER=1

129
README.md
View File

@@ -4,13 +4,19 @@
* * *
<img width="64" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png" align="left" /> **Joplin**® is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](#markdown).
🌞 Joplin participates in **Google Summer of Code 2022**! More info on [the announcement post](https://github.com/laurent22/joplin/blob/dev/readme/news/20220308-gsoc2022-start.md). 🌞
Notes exported from Evernote via .enex files [can be imported](#importing) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
* * *
The notes can be [synchronised](#synchronisation) with various cloud services including [Nextcloud](https://nextcloud.com/), Dropbox, OneDrive, WebDAV or the file system (for example with a network directory). When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
<img width="64" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png" align="left" /> **Joplin** is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](#markdown).
The application is available for Windows, Linux, macOS, Android and iOS (the terminal app also works on FreeBSD). A [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
Notes exported from Evernote [can be imported](#importing) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
The notes can be securely [synchronised](#synchronisation) using [end-to-end encryption](#encryption) with various cloud services including Nextcloud, Dropbox, OneDrive and [Joplin Cloud](https://joplinapp.org/plans/).
Full text search is available on all platforms to quickly find the information you need. The app can be customised using plugins and themes, and you can also easily create your own.
The application is available for Windows, Linux, macOS, Android and iOS. A [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
<div class="top-screenshot"><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/home-top-img.png" style="max-width: 100%; max-height: 35em;"></div>
@@ -22,11 +28,11 @@ Three types of applications are available: for **desktop** (Windows, macOS and L
Operating System | Download
---|---
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v2.6.10/Joplin-Setup-2.6.10.exe'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v2.6.10/Joplin-2.6.10.dmg'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v2.6.10/Joplin-2.6.10.AppImage'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v2.7.15/Joplin-Setup-2.7.15.exe'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v2.7.15/Joplin-2.7.15.dmg'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v2.7.15/Joplin-2.7.15.AppImage'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
**On Windows**, you may also use the <a href='https://github.com/laurent22/joplin/releases/download/v2.6.10/JoplinPortable.exe'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Windows**, you may also use the <a href='https://github.com/laurent22/joplin/releases/download/v2.7.15/JoplinPortable.exe'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Linux**, the recommended way is to use the following installation script as it will handle the desktop icon too:
@@ -36,7 +42,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/v2.6.10/J
Operating System | Download | Alt. Download
---|---|---
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.6.9/joplin-v2.6.9.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.6.9/joplin-v2.6.9-32bit.apk)
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.7.2/joplin-v2.7.2.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.7.2/joplin-v2.7.2-32bit.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeIOS.png'/></a> | -
## Terminal application
@@ -49,19 +55,18 @@ To start it, type `joplin`.
For usage information, please refer to the full [Joplin Terminal Application Documentation](https://joplinapp.org/terminal/).
### Unsupported methods
There are other ways to install the terminal application. However, they are not supported and problems must be reported to the upstream projects.
Operating system | Method
-----------------|----------------
Arch Linux | An Arch Linux package is available [here](https://aur.archlinux.org/packages/joplin/). To install it, use an AUR wrapper such as yay: `yay -S joplin`. Both the CLI tool (type `joplin`) and desktop app (type `joplin-desktop`) are packaged. You can also install a compiled version with the [chaotic-aur](https://wiki.archlinux.org/index.php/Unofficial_user_repositories#chaotic-aur) repository. For support, please go to the [GitHub repo](https://github.com/masterkorp/joplin-pkgbuild). If you are interested in [pre-release](https://joplinapp.org/prereleases/) you have [joplin-beta](https://aur.archlinux.org/packages/joplin-beta).
Flatpak | A Flatpak is available on [Flathub](https://flathub.org/apps/details/net.cozic.joplin_desktop). To install it, run `flatpak install net.cozic.joplin_desktop` after [setting up Flathub](https://flatpak.org/setup/). GUI software managers on most distros support Flatpak installation.
## Web Clipper
The Web Clipper is a browser extension that allows you to save web pages and screenshots from your browser. For more information on how to install and use it, see the [Web Clipper Help Page](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md).
## Unofficial Alternative Distributions
There are a number of unofficial alternative Joplin distributions. If you do not want to or cannot use appimages or any of the other officially supported releases then you may wish to consider these.
However these come with a caveat in that they are not officially supported so certain issues may not be supportable by the main project. Rather support requests, bug reports and general advice would need to go to the maintainers of these distributions.
A community maintained list of these distributions can be found here: [Unofficial Joplin distributions](https://discourse.joplinapp.org/t/unofficial-alternative-joplin-distributions/23703)
# Sponsors
<!-- SPONSORS-ORG -->
@@ -75,10 +80,11 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
| :---: | :---: | :---: | :---: |
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/3061769?s=96&v=4"/></br>[c-nagy](https://github.com/c-nagy) | <img width="50" src="https://avatars2.githubusercontent.com/u/70780798?s=96&v=4"/></br>[cabottech](https://github.com/cabottech) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/4862947?s=96&v=4"/></br>[chrootlogin](https://github.com/chrootlogin) | <img width="50" src="https://avatars2.githubusercontent.com/u/82579431?s=96&v=4"/></br>[clmntsl](https://github.com/clmntsl) | <img width="50" src="https://avatars2.githubusercontent.com/u/808091?s=96&v=4"/></br>[cuongtransc](https://github.com/cuongtransc) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[fbloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/38898566?s=96&v=4"/></br>[h4sh5](https://github.com/h4sh5) | <img width="50" src="https://avatars2.githubusercontent.com/u/3266447?s=96&v=4"/></br>[iamwillbar](https://github.com/iamwillbar) | <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) | <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) | |
| <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[fbloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/49439044?s=96&v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars2.githubusercontent.com/u/38898566?s=96&v=4"/></br>[h4sh5](https://github.com/h4sh5) | <img width="50" src="https://avatars2.githubusercontent.com/u/3266447?s=96&v=4"/></br>[iamwillbar](https://github.com/iamwillbar) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) | <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) | | | |
<!-- SPONSORS-GITHUB -->
<!-- TOC -->
@@ -127,6 +133,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
- [How to build the apps](https://github.com/laurent22/joplin/blob/dev/BUILD.md)
- [Writing a technical spec](https://github.com/laurent22/joplin/blob/dev/readme/technical_spec.md)
- [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/e2ee.md)
- [Desktop application styling](https://github.com/laurent22/joplin/blob/dev/readme/spec/desktop_styling.md)
- [Note History spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/history.md)
- [Sync Lock spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_lock.md)
- [Synchronous Scroll spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_scroll.md)
@@ -136,11 +143,11 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
- [Server: Delta Sync](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_delta_sync.md)
- [Server: Sharing](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_sharing.md)
- Google Summer of Code 2021
- Google Summer of Code 2022
- [Google Summer of Code 2021](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2021/index.md)
- [How to submit a GSoC pull request](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2021/pull_request_guidelines.md)
- [Project Ideas](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2021/ideas.md)
- [Google Summer of Code 2022](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2022/index.md)
- [How to submit a GSoC pull request](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2022/pull_request_guidelines.md)
- [Project Ideas](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2022/ideas.md)
- About
@@ -513,47 +520,47 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 90%
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 26%
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 66%
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 52%
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 88%
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 65%
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 51%
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 97%
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 94%
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 87%
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [MrKanister](mailto:s.robin@tutanota.de) | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 51%
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [MrKanister](mailto:pthrp_bnsrv@aleeas.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 50%
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Mora](mailto:francisco.m.collao@gmail.com) | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Mora](mailto:francisco.m.collao@gmail.com) | 96%
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 29%
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | miucci | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 100%
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 33%
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 77%
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 84%
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | Alexander Dawson | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 63%
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 83%
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Felipe Viggiano](mailto:felipeviggiano@gmail.com) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 83%
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 58%
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 42%
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 82%
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 86%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 75%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [SiderealArt](mailto:nelson22768384@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 97%
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 88%
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 87%
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 76%
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 83%
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | Alexander Dawson | 88%
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 62%
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 82%
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Felipe Viggiano](mailto:felipeviggiano@gmail.com) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 82%
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 57%
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 41%
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 88%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 81%
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 84%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 73%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [horaceyoung](mailto:yonghaoharry@gmail.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [SiderealArt](mailto:nelson22768384@gmail.com) | 88%
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 87%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Contributors

View File

@@ -1,8 +1,19 @@
# This is a sample docker-compose file that can be used to run Joplin Server
# along with a PostgreSQL server.
#
# All environment variables are optional. If you don't set them, you will get a
# warning from docker-compose, however the app should use working defaults.
# Update the following fields in the stanza below:
#
# POSTGRES_USER
# POSTGRES_PASSWORD
# APP_BASE_URL
#
# APP_BASE_URL: This is the base public URL where the service will be running.
# - If Joplin Server needs to be accessible over the internet, configure APP_BASE_URL as follows: https://example.com/joplin.
# - If Joplin Server does not need to be accessible over the internet, set the the APP_BASE_URL to your server's hostname.
# For Example: http://[hostname]:22300. The base URL can include the port.
# APP_PORT: The local port on which the Docker container will listen.
# - This would typically be mapped to port to 443 (TLS) with a reverse proxy.
# - If Joplin Server does not need to be accessible over the internet, the port can be mapped to 22300.
version: '3'

View File

@@ -0,0 +1 @@
eine Notiz- und Aufgaben-App mit Sync zwischen Linux, macOS, Windows

View File

@@ -0,0 +1,9 @@
<strong>Joplin</strong> is a note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in <a href="#markdown">Markdown format</a>.
Notes exported from Evernote and other applications <a href="https://joplinapp.org/help/#importing">can be imported</a> into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
The notes can be securely <a href="https://joplinapp.org/help/#synchronisation">synchronised</a> using <a href="https://joplinapp.org/help/#encryption">end-to-end encryption</a> with various cloud services including Nextcloud, Dropbox, OneDrive and <a href="https://joplinapp.org/plans/">Joplin Cloud</a>.
Full text search is available on all platforms to quickly find the information you need. The app can be customised using plugins and themes, and you can also easily create your own.
The application is available for Windows, Linux, macOS, Android and iOS. A <a href="https://joplinapp.org/clipper/">Web Clipper</a>, to save web pages and screenshots from your browser, is also available for <a href="https://addons.mozilla.org/firefox/addon/joplin-web-clipper/">Firefox</a> and <a href="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB">Chrome</a>.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -0,0 +1 @@
a note taking and to-do app with sync between Linux, macOS, Windows, and mobile

View File

@@ -18,6 +18,7 @@
"buildCommandIndex": "gulp buildCommandIndex",
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../joplin-website/docs/api/references/plugin_api packages/lib/services/plugins/api/",
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
"updateNews": "node ./packages/tools/website/updateNews",
"buildSettingJsonSchema": "yarn workspace joplin start settingschema ../../../joplin-website/docs/schema/settings.json",
"buildTranslations": "node packages/tools/build-translation.js",
"buildWebsite": "node ./packages/tools/website/build.js && yarn run buildPluginDoc && yarn run buildSettingJsonSchema",
@@ -31,7 +32,7 @@
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"postinstall": "gulp build",
"publishAll": "git pull && yarn run buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
"releaseAndroid": "yarn run buildParallel && export PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" && node packages/tools/release-android.js",
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" node packages/tools/release-android.js",
"releaseAndroidClean": "node packages/tools/release-android.js",
"releaseCli": "node packages/tools/release-cli.js",
"releaseClipper": "node packages/tools/release-clipper.js",

View File

@@ -313,6 +313,14 @@ async function fetchAllNotes() {
lines.push('');
lines.push('\tcurl -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my resource title"}\' http://localhost:41184/resources');
lines.push('');
lines.push('To **update** the resource content, you can make a PUT request with the same arguments:');
lines.push('');
lines.push('\tcurl -X PUT -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my modified title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696');
lines.push('');
lines.push('Or if you only need to update the resource properties (title, etc.), without changing the content, you can make a regular PUT request:');
lines.push('');
lines.push('\tcurl -X PUT --data \'{"title": "My new title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696');
lines.push('');
lines.push('The "data" field is required, while the "props" one is not. If not specified, default values will be used.');
lines.push('');
lines.push('**From a plugin** the syntax to create a resource is also a bit special:');
@@ -368,6 +376,11 @@ async function fetchAllNotes() {
lines.push(`Sets the properties of the ${singular} with ID :id`);
lines.push('');
if (model.type === BaseModel.TYPE_RESOURCE) {
lines.push('You may also update the file data by specifying a file (See `POST /resources` example).');
lines.push('');
}
lines.push(`## DELETE /${tableName}/:id`);
lines.push('');
lines.push(`Deletes the ${singular} with ID :id`);

View File

@@ -242,9 +242,9 @@ describe('MdToHtml', function() {
{
const input = '# Head\nFruits\n- Apple\n';
const result = await mdToHtml.render(input, null, { bodyOnly: true, mapsToLine: true });
expect(result.html.trim()).toBe('<h1 id="head" class="maps-to-line" source-line="0">Head</h1>\n' +
'<p class="maps-to-line" source-line="1">Fruits</p>\n' +
'<ul>\n<li class="maps-to-line" source-line="2">Apple</li>\n</ul>'
expect(result.html.trim()).toBe('<h1 id="head" class="maps-to-line" source-line="0" source-line-end="1">Head</h1>\n' +
'<p class="maps-to-line" source-line="1" source-line-end="2">Fruits</p>\n' +
'<ul>\n<li class="maps-to-line" source-line="2" source-line-end="3">Apple</li>\n</ul>'
);
}
}));

View File

@@ -63,13 +63,15 @@ class AppComponent extends Component {
contentScriptLoaded: false,
selectedTags: [],
contentScriptError: '',
newNoteId: null,
});
this.confirm_click = () => {
this.confirm_click = async () => {
const content = Object.assign({}, this.props.clippedContent);
content.tags = this.state.selectedTags.join(',');
content.parent_id = this.props.selectedFolderId;
bridge().sendContentToJoplin(content);
const response = await bridge().sendContentToJoplin(content);
this.setState({ newNoteId: response.id });
};
this.contentTitle_change = (event) => {
@@ -402,6 +404,24 @@ class AppComponent extends Component {
);
};
const openNewNoteButton = () => {
if (!this.state.newNoteId) { return null; } else {
return (
// The jopin:// link must be opened in a new tab. When it's opened for the first time, a system dialog will ask for the user's permission.
// The system dialog is too big to fit into the popup so the user will not be able to see the dialog buttons and get stuck.
<a
className="Button"
href={`joplin://x-callback-url/openNote?id=${encodeURIComponent(this.state.newNoteId)}`}
target="_blank"
onClick={() => this.setState({ newNoteId: null })}
>
Open newly created note
</a>
);
}
};
const tagDataListOptions = [];
for (let i = 0; i < this.props.tags.length; i++) {
const tag = this.props.tags[i];
@@ -437,6 +457,7 @@ class AppComponent extends Component {
</div>
{ warningComponent }
{ previewComponent }
{ openNewNoteButton() }
{ clipperStatusComp() }
</div>
);

View File

@@ -463,9 +463,11 @@ class Bridge {
// This is the perfect Heisenbug - it happens always when opening the popup the first time EXCEPT
// when the debugger is open. Then everything is working fine and the bug NEVER EVER happens,
// so it's impossible to understand what's going on.
await this.clipperApiExec('POST', 'notes', { nounce: this.nounce_++ }, content);
const response = await this.clipperApiExec('POST', 'notes', { nounce: this.nounce_++ }, content);
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });
return response;
} catch (error) {
if (error.message === '{"error":"Duplicate Nounce"}') {
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });

View File

@@ -9,6 +9,13 @@ interface LastSelectedPath {
directory: string;
}
interface OpenDialogOptions {
properties?: string[];
defaultPath?: string;
createDirectory?: boolean;
filters?: any[];
}
export class Bridge {
private electronWrapper_: ElectronAppWrapper;
@@ -155,14 +162,14 @@ export class Bridge {
return filePath;
}
async showOpenDialog(options: any = null) {
async showOpenDialog(options: OpenDialogOptions = null) {
const { dialog } = require('electron');
if (!options) options = {};
let fileType = 'file';
if (options.properties && options.properties.includes('openDirectory')) fileType = 'directory';
if (!('defaultPath' in options) && (this.lastSelectedPaths_ as any)[fileType]) options.defaultPath = (this.lastSelectedPaths_ as any)[fileType];
if (!('createDirectory' in options)) options.createDirectory = true;
const { filePaths } = await dialog.showOpenDialog(this.window(), options);
const { filePaths } = await dialog.showOpenDialog(this.window(), options as any);
if (filePaths && filePaths.length) {
(this.lastSelectedPaths_ as any)[fileType] = dirname(filePaths[0]);
}

View File

@@ -0,0 +1,19 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import Setting from '../../lib/models/Setting';
import { openFileWithExternalEditor } from '../../lib/services/ExternalEditWatcher/utils';
import bridge from '../services/bridge';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'editProfileConfig',
label: () => _('Edit profile configuration...'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
await openFileWithExternalEditor(`${Setting.value('rootProfileDir')}/profiles.json`, bridge());
},
enabledCondition: 'hasMultiProfiles',
};
};

View File

@@ -1,5 +1,6 @@
// AUTO-GENERATED using `gulp buildCommandIndex`
import * as copyDevCommand from './copyDevCommand';
import * as editProfileConfig from './editProfileConfig';
import * as exportFolders from './exportFolders';
import * as exportNotes from './exportNotes';
import * as focusElement from './focusElement';
@@ -8,11 +9,16 @@ import * as replaceMisspelling from './replaceMisspelling';
import * as restoreNoteRevision from './restoreNoteRevision';
import * as startExternalEditing from './startExternalEditing';
import * as stopExternalEditing from './stopExternalEditing';
import * as switchProfile from './switchProfile';
import * as switchProfile1 from './switchProfile1';
import * as switchProfile2 from './switchProfile2';
import * as switchProfile3 from './switchProfile3';
import * as toggleExternalEditing from './toggleExternalEditing';
import * as toggleSafeMode from './toggleSafeMode';
const index:any[] = [
copyDevCommand,
editProfileConfig,
exportFolders,
exportNotes,
focusElement,
@@ -21,6 +27,10 @@ const index:any[] = [
restoreNoteRevision,
startExternalEditing,
stopExternalEditing,
switchProfile,
switchProfile1,
switchProfile2,
switchProfile3,
toggleExternalEditing,
toggleSafeMode,
];

View File

@@ -0,0 +1,26 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import Setting from '@joplin/lib/models/Setting';
import { saveProfileConfig } from '@joplin/lib/services/profileConfig';
import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
import bridge from '../services/bridge';
export const declaration: CommandDeclaration = {
name: 'switchProfile',
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext, profileIndex: number) => {
const currentConfig = context.state.profileConfig;
if (currentConfig.currentProfile === profileIndex) return;
const newConfig: ProfileConfig = {
...currentConfig,
currentProfile: profileIndex,
};
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newConfig);
bridge().restart();
},
};
};

View File

@@ -0,0 +1,15 @@
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'switchProfile1',
label: () => _('Switch to profile %d', 1),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
await CommandService.instance().execute('switchProfile', 0);
},
};
};

View File

@@ -0,0 +1,15 @@
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'switchProfile2',
label: () => _('Switch to profile %d', 2),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
await CommandService.instance().execute('switchProfile', 1);
},
};
};

View File

@@ -0,0 +1,15 @@
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'switchProfile3',
label: () => _('Switch to profile %d', 3),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
await CommandService.instance().execute('switchProfile', 2);
},
};
};

View File

@@ -8,9 +8,11 @@ import StyledInput from '../style/StyledInput';
import { IconSelector, ChangeEvent } from './IconSelector';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import Folder from '@joplin/lib/models/Folder';
import { FolderEntity, FolderIcon } from '@joplin/lib/services/database/types';
import { FolderEntity, FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
import Button from '../Button/Button';
import bridge from '../../services/bridge';
import shim from '@joplin/lib/shim';
import FolderIconBox from '../FolderIconBox';
interface Props {
themeId: number;
@@ -93,6 +95,34 @@ export default function(props: Props) {
setFolderIcon(null);
}, []);
const onBrowseClick = useCallback(async () => {
const filePaths = await bridge().showOpenDialog({
filters: [
{
name: _('Images'),
extensions: ['jpg', 'jpeg', 'png'],
},
],
});
if (filePaths.length !== 1) return;
const filePath = filePaths[0];
try {
const dataUrl = await shim.imageToDataUrl(filePath, 256);
setFolderIcon(icon => {
return {
...icon,
emoji: '',
name: '',
type: FolderIconType.DataUrl,
dataUrl,
};
});
} catch (error) {
await bridge().showErrorMessageBox(error.message);
}
}, []);
function renderForm() {
return (
<div>
@@ -105,11 +135,14 @@ export default function(props: Props) {
<div className="form-input-group">
<label>{_('Icon')}</label>
<div className="icon-selector-row">
{ folderIcon && <div className="foldericon"><FolderIconBox folderIcon={folderIcon} /></div> }
<IconSelector
title={_('Select emoji...')}
icon={folderIcon}
onChange={onFolderIconChange}
/>
<Button ml={1} title={_('Clear')} onClick={onClearClick}/>
<Button ml={1} title={_('Select file...')} onClick={onBrowseClick}/>
{ folderIcon && <Button ml={1} title={_('Clear')} onClick={onClearClick}/> }
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@ import { useEffect, useState, useCallback, useRef } from 'react';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import { loadScript } from '../utils/loadScript';
import Button from '../Button/Button';
import { FolderIcon } from '@joplin/lib/services/database/types';
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
import bridge from '../../services/bridge';
export interface ChangeEvent {
@@ -15,6 +15,7 @@ type ChangeHandler = (event: ChangeEvent)=> void;
interface Props {
onChange: ChangeHandler;
icon: FolderIcon | null;
title: string;
}
export const IconSelector = (props: Props) => {
@@ -62,7 +63,7 @@ export const IconSelector = (props: Props) => {
});
const onEmoji = (selection: FolderIcon) => {
props.onChange({ value: selection });
props.onChange({ value: { ...selection, type: FolderIconType.Emoji } });
};
p.on('emoji', onEmoji);
@@ -78,16 +79,25 @@ export const IconSelector = (props: Props) => {
picker.togglePicker(buttonRef.current);
}, [picker]);
const buttonText = props.icon ? props.icon.emoji : '...';
// const buttonText = props.icon ? props.icon.emoji : '...';
return (
<Button
disabled={!picker}
ref={buttonRef}
onClick={onClick}
title={buttonText}
isSquare={true}
fontSize={20}
title={props.title}
/>
);
// return (
// <Button
// disabled={!picker}
// ref={buttonRef}
// onClick={onClick}
// title={buttonText}
// isSquare={true}
// fontSize={20}
// />
// );
};

View File

@@ -1,4 +1,13 @@
.icon-selector-row {
display: flex;
flex-direction: row;
align-items: center;
}
.icon-selector-row > .foldericon {
margin-right: 5px;
display: flex;
border: 1px solid var(--joplin-divider-color);
padding: 5px;
background-color: var(--joplin-background-color);
}

View File

@@ -0,0 +1,17 @@
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
interface Props {
folderIcon: FolderIcon;
}
export default function(props: Props) {
const folderIcon = props.folderIcon;
if (folderIcon.type === FolderIconType.Emoji) {
return <span style={{ fontSize: 20 }}>{folderIcon.emoji}</span>;
} else if (folderIcon.type === FolderIconType.DataUrl) {
return <img style={{ width: 20, height: 20 }} src={folderIcon.dataUrl} />;
} else {
throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
}
}

View File

@@ -0,0 +1,34 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { createNewProfile, saveProfileConfig } from '@joplin/lib/services/profileConfig';
import Setting from '@joplin/lib/models/Setting';
import bridge from '../../../services/bridge';
export const declaration: CommandDeclaration = {
name: 'addProfile',
label: () => _('Create new profile...'),
};
export const runtime = (comp: any): CommandRuntime => {
return {
execute: async (context: CommandContext) => {
comp.setState({
promptOptions: {
label: _('Profile name:'),
buttons: ['create', 'cancel'],
value: '',
onClose: async (answer: string) => {
if (answer) {
const newConfig = await createNewProfile(context.state.profileConfig, answer);
newConfig.currentProfile = newConfig.profiles.length - 1;
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newConfig);
bridge().restart();
}
comp.setState({ promptOptions: null });
},
},
});
},
};
};

View File

@@ -1,4 +1,5 @@
// AUTO-GENERATED using `gulp buildCommandIndex`
import * as addProfile from './addProfile';
import * as commandPalette from './commandPalette';
import * as editAlarm from './editAlarm';
import * as exportPdf from './exportPdf';
@@ -38,6 +39,7 @@ import * as toggleSideBar from './toggleSideBar';
import * as toggleVisiblePanes from './toggleVisiblePanes';
const index:any[] = [
addProfile,
commandPalette,
editAlarm,
exportPdf,

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { AppState } from '../app.reducer';
import InteropService from '@joplin/lib/services/interop/InteropService';
import { stateUtils } from '@joplin/lib/reducer';
@@ -18,9 +18,9 @@ import menuCommandNames from './menuCommandNames';
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
import bridge from '../services/bridge';
import checkForUpdates from '../checkForUpdates';
const { connect } = require('react-redux');
import { reg } from '@joplin/lib/registry';
import { ProfileConfig } from '../../lib/services/profileConfig/types';
const packageInfo = require('../packageInfo.js');
const { clipboard } = require('electron');
const Menu = bridge().Menu;
@@ -39,7 +39,7 @@ function pluginMenuItemsCommandNames(menuItems: MenuItem[]): string[] {
return output;
}
function pluginCommandNames(plugins: PluginStates): string[] {
function getPluginCommandNames(plugins: PluginStates): string[] {
let output: string[] = [];
for (const view of pluginUtils.viewsByType(plugins, 'menu')) {
@@ -70,6 +70,42 @@ function createPluginMenuTree(label: string, menuItems: MenuItem[], onMenuItemCl
return output;
}
const useSwitchProfileMenuItems = (profileConfig: ProfileConfig, menuItemDic: any) => {
return useMemo(() => {
const switchProfileMenuItems: any[] = [];
for (let i = 0; i < profileConfig.profiles.length; i++) {
const profile = profileConfig.profiles[i];
let menuItem: any = {};
const profileNum = i + 1;
if (menuItemDic[`switchProfile${profileNum}`]) {
menuItem = { ...menuItemDic[`switchProfile${profileNum}`] };
} else {
menuItem = {
label: profile.name,
click: () => {
void CommandService.instance().execute('switchProfile', i);
},
};
}
menuItem.label = profile.name;
menuItem.type = 'checkbox';
menuItem.checked = profileConfig.currentProfile === i;
switchProfileMenuItems.push(menuItem);
}
switchProfileMenuItems.push({ type: 'separator' });
switchProfileMenuItems.push(menuItemDic.addProfile);
switchProfileMenuItems.push(menuItemDic.editProfileConfig);
return switchProfileMenuItems;
}, [profileConfig, menuItemDic]);
};
interface Props {
dispatch: Function;
menuItemProps: any;
@@ -90,6 +126,7 @@ interface Props {
plugins: PluginStates;
customCss: string;
locale: string;
profileConfig: ProfileConfig;
}
const commandNames: string[] = menuCommandNames();
@@ -241,6 +278,18 @@ function useMenu(props: Props) {
const onImportModuleClickRef = useRef(null);
onImportModuleClickRef.current = onImportModuleClick;
const pluginCommandNames = useMemo(() => props.pluginMenuItems.map((view: any) => view.commandName), [props.pluginMenuItems]);
const menuItemDic = useMemo(() => {
return menuUtils.commandsToMenuItems(
commandNames.concat(pluginCommandNames),
(commandName: string) => onMenuItemClickRef.current(commandName),
props.locale
);
}, [commandNames, pluginCommandNames, props.locale]);
const switchProfileMenuItems: any[] = useSwitchProfileMenuItems(props.profileConfig, menuItemDic);
useEffect(() => {
let timeoutId: any = null;
@@ -249,13 +298,6 @@ function useMenu(props: Props) {
const keymapService = KeymapService.instance();
const pluginCommandNames = props.pluginMenuItems.map((view: any) => view.commandName);
const menuItemDic = menuUtils.commandsToMenuItems(
commandNames.concat(pluginCommandNames),
(commandName: string) => onMenuItemClickRef.current(commandName),
props.locale
);
const quitMenuItem = {
label: _('Quit'),
accelerator: keymapService.getAccelerator('quit'),
@@ -347,6 +389,12 @@ function useMenu(props: Props) {
}
}
importItems.push({ type: 'separator' });
importItems.push({
label: _('Other applications...'),
click: () => { void bridge().openExternal('https://discourse.joplinapp.org/t/importing-notes-from-other-notebook-applications/22425'); },
});
exportItems.push(
menuItemDic.exportPdf
);
@@ -379,6 +427,10 @@ function useMenu(props: Props) {
const newFolderItem = menuItemDic.newFolder;
const newSubFolderItem = menuItemDic.newSubFolder;
const printItem = menuItemDic.print;
const switchProfileItem = {
label: _('Switch profile'),
submenu: switchProfileMenuItems,
};
let toolsItems: any[] = [];
@@ -493,6 +545,8 @@ function useMenu(props: Props) {
platforms: ['darwin'],
},
shim.isMac() ? noItem : switchProfileItem,
shim.isMac() ? {
label: _('Hide %s', 'Joplin'),
platforms: ['darwin'],
@@ -539,6 +593,7 @@ function useMenu(props: Props) {
type: 'separator',
},
printItem,
switchProfileItem,
],
};
@@ -566,8 +621,15 @@ function useMenu(props: Props) {
menuItemDic.textPaste,
menuItemDic.textSelectAll,
separator(),
menuItemDic['editor.undo'],
menuItemDic['editor.redo'],
// Using the generic "undo"/"redo" roles mean the menu
// item will work in every text fields, whether it's the
// editor or a regular text field.
{
role: 'undo',
},
{
role: 'redo',
},
separator(),
menuItemDic.textBold,
menuItemDic.textItalic,
@@ -835,7 +897,21 @@ function useMenu(props: Props) {
clearTimeout(timeoutId);
timeoutId = null;
};
}, [props.routeName, props.pluginMenuItems, props.pluginMenus, keymapLastChangeTime, modulesLastChangeTime, props['spellChecker.language'], props['spellChecker.enabled'], props.plugins, props.customCss, props.locale]);
}, [
props.routeName,
props.pluginMenuItems,
props.pluginMenus,
keymapLastChangeTime,
modulesLastChangeTime,
props['spellChecker.language'],
props['spellChecker.enabled'],
props.plugins,
props.customCss,
props.locale,
props.profileConfig,
switchProfileMenuItems,
menuItemDic,
]);
useMenuStates(menu, props);
@@ -876,7 +952,7 @@ const mapStateToProps = (state: AppState) => {
const whenClauseContext = stateToWhenClauseContext(state);
return {
menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(pluginCommandNames(state.pluginService.plugins)), whenClauseContext),
menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(getPluginCommandNames(state.pluginService.plugins)), whenClauseContext),
locale: state.settings.locale,
routeName: state.route.routeName,
selectedFolderId: state.selectedFolderId,
@@ -894,6 +970,7 @@ const mapStateToProps = (state: AppState) => {
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
plugins: state.pluginService.plugins,
customCss: state.customCss,
profileConfig: state.profileConfig,
};
};

View File

@@ -515,8 +515,9 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
color: ${theme.searchMarkerColor} !important;
}
/* We need !important because the search marker is overridden by CodeMirror's own text selection marker */
.cm-search-marker-selected {
background: ${theme.selectedColor2};
background: ${theme.selectedColor2} !important;
color: ${theme.color2} !important;
}
@@ -648,6 +649,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
// undefined. Maybe due to the error boundary that unmount components.
// Since we can't do much about it we just print an error.
if (webviewRef.current && webviewRef.current.wrappedInstance) {
// To keep consistency among CodeMirror's editing and scroll percents
// of Editor and Viewer.
const percent = getLineScrollPercent();
setEditorPercentScroll(percent);
options.percent = percent;
webviewRef.current.wrappedInstance.send('setHtml', renderedBody.html, options);
} else {
console.error('Trying to set HTML on an undefined webview ref');
@@ -731,15 +737,19 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
// It might be buggy, refer to the below issue
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
useEffect(() => {
function pointerInsideEditor(x: number, y: number) {
function pointerInsideEditor(params: any) {
const x = params.x, y = params.y, isEditable = params.isEditable, inputFieldType = params.inputFieldType;
const elements = document.getElementsByClassName('codeMirrorEditor');
if (!elements.length) return null;
// inputFieldType: The input field type of CodeMirror is "textarea" so the inputFieldType = "none",
// and any single-line input above codeMirror has inputFieldType value according to the type of input e.g.(text = plainText, password = password, ...).
if (!elements.length || !isEditable || inputFieldType !== 'none') return null;
const rect = convertToScreenCoordinates(Setting.value('windowContentZoomFactor'), elements[0].getBoundingClientRect());
return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y;
}
async function onContextMenu(_event: any, params: any) {
if (!pointerInsideEditor(params.x, params.y)) return;
if (!pointerInsideEditor(params)) return;
const menu = new Menu();

View File

@@ -100,6 +100,7 @@ export interface EditorProps {
function Editor(props: EditorProps, ref: any) {
const [editor, setEditor] = useState(null);
const editorParent = useRef(null);
const lastEditTime = useRef(NaN);
// Codemirror plugins add new commands to codemirror (or change it's behavior)
// This command adds the smartListIndent function which will be bound to tab
@@ -120,6 +121,7 @@ function Editor(props: EditorProps, ref: any) {
const editor_change = useCallback((cm: any, change: any) => {
if (props.onChange && change.origin !== 'setValue') {
props.onChange(cm.getValue());
lastEditTime.current = Date.now();
}
}, [props.onChange]);
@@ -154,7 +156,8 @@ function Editor(props: EditorProps, ref: any) {
}, [props.onResize]);
const editor_update = useCallback((cm: any) => {
props.onUpdate(cm);
const edited = Date.now() - lastEditTime.current <= 100;
props.onUpdate(cm, edited);
}, [props.onUpdate]);
useEffect(() => {

View File

@@ -17,7 +17,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
const now = Date.now();
if (now >= ignoreNextEditorScrollTime_.current) ignoreNextEditorScrollEventCount_.current = 0;
if (ignoreNextEditorScrollEventCount_.current < 10) { // for safety
ignoreNextEditorScrollTime_.current = now + 200;
ignoreNextEditorScrollTime_.current = now + 1000;
ignoreNextEditorScrollEventCount_.current += 1;
}
};
@@ -157,8 +157,9 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
// When heights of lines are updated in CodeMirror, 'update' events are raised.
// If such an update event is raised, scroll position should be restored.
// See https://github.com/laurent22/joplin/issues/5981
const editor_update = useCallback((cm) => {
const editor_update = useCallback((cm: any, edited: boolean) => {
if (isCodeMirrorReady(cm)) {
if (edited) return;
const linesHeight = cm.heightAtLine(cm.lineCount()) - cm.heightAtLine(0);
if (lastLinesHeight_.current !== linesHeight) {
// To avoid cancelling intentional scroll position changes,

View File

@@ -3,7 +3,8 @@ import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import { useEffect } from 'react';
import bridge from '../../../../../services/bridge';
import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenu';
import { ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenuUtils';
import { menuItems } from '../../../utils/contextMenu';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import convertToScreenCoordinates from '../../../../utils/convertToScreenCoordinates';
@@ -67,6 +68,8 @@ export default function(editor: any, plugins: PluginStates, dispatch: Function)
contextMenuActionOptions.current = {
itemType,
resourceId,
filename: null,
mime: null,
linkToCopy,
textToCopy: null,
htmlToCopy: editor.selection ? editor.selection.getContent() : '',

View File

@@ -93,11 +93,11 @@ const declarations: CommandDeclaration[] = [
},
{
name: 'editor.undo',
label: () => _('Undo'),
label: () => _('Editor: %s', _('Undo')),
},
{
name: 'editor.redo',
label: () => _('Redo'),
label: () => _('Editor: %s', _('Redo')),
},
{
name: 'editor.indentLess',

View File

@@ -0,0 +1,41 @@
/** @jest-environment ./loadResources.testEnv */
// eslint-disable-next-line strict, lines-around-directive
'use strict';
// use strict is necessary here so that typescript doesn't place "use strict" above the jest docblock
// https://github.com/microsoft/TypeScript/issues/15819#issuecomment-782235619
import { textToDataUri, svgUriToPng } from './contextMenuUtils';
jest.mock('@joplin/lib/models/Resource');
describe('contextMenu', () => {
it('should provide proper copy path', async () => {
const testCase = [
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">test</svg>',
'image/svg+xml',
];
const expectedText = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIj50ZXN0PC9zdmc+';
expect(textToDataUri(testCase[0], testCase[1])).toBe(expectedText);
});
it('should convert to png binary', async () => {
const testCase = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiIGhlaWdodD0iMTAwcHgiIHdpZHRoPSIxMDBweCI+CjxnPgoJPHBhdGggZD0iTTI4LjEsMzYuNmM0LjYsMS45LDEyLjIsMS42LDIwLjksMS4xYzguOS0wLjQsMTktMC45LDI4LjksMC45YzYuMywxLjIsMTEuOSwzLjEsMTYuOCw2Yy0xLjUtMTIuMi03LjktMjMuNy0xOC42LTMxLjMgICBjLTQuOS0wLjItOS45LDAuMy0xNC44LDEuNEM0Ny44LDE3LjksMzYuMiwyNS42LDI4LjEsMzYuNnoiLz4KCTxwYXRoIGQ9Ik03MC4zLDkuOEM1Ny41LDMuNCw0Mi44LDMuNiwzMC41LDkuNWMtMyw2LTguNCwxOS42LTUuMywyNC45YzguNi0xMS43LDIwLjktMTkuOCwzNS4yLTIzLjFDNjMuNywxMC41LDY3LDEwLDcwLjMsOS44eiIvPgoJPHBhdGggZD0iTTE2LjUsNTEuM2MwLjYtMS43LDEuMi0zLjQsMi01LjFjLTMuOC0zLjQtNy41LTctMTEtMTAuOGMtMi4xLDYuMS0yLjgsMTIuNS0yLjMsMTguN0M5LjYsNTEuMSwxMy40LDUwLjIsMTYuNSw1MS4zeiIvPgoJPHBhdGggZD0iTTksMzEuNmMzLjUsMy45LDcuMiw3LjYsMTEuMSwxMS4xYzAuOC0xLjYsMS43LTMuMSwyLjYtNC42YzAuMS0wLjIsMC4zLTAuNCwwLjQtMC42Yy0yLjktMy4zLTMuMS05LjItMC42LTE3LjYgICBjMC44LTIuNywxLjgtNS4zLDIuNy03LjRjLTUuMiwzLjQtOS44LDgtMTMuMywxMy43QzEwLjgsMjcuOSw5LjgsMjkuNyw5LDMxLjZ6Ii8+Cgk8cGF0aCBkPSJNMTUuNCw1NC43Yy0yLjYtMS02LjEsMC43LTkuNywzLjRjMS4yLDYuNiwzLjksMTMsOCwxOC41QzEzLDY5LjMsMTMuNSw2MS44LDE1LjQsNTQuN3oiLz4KCTxwYXRoIGQ9Ik0zOS44LDU3LjZDNTQuMyw2Ni43LDcwLDczLDg2LjUsNzYuNGMwLjYtMC44LDEuMS0xLjYsMS43LTIuNWM0LjgtNy43LDctMTYuMyw2LjgtMjQuOGMtMTMuOC05LjMtMzEuMy04LjQtNDUuOC03LjcgICBjLTkuNSwwLjUtMTcuOCwwLjktMjMuMi0xLjdjLTAuMSwwLjEtMC4yLDAuMy0wLjMsMC40Yy0xLDEuNy0yLDMuNC0yLjksNS4xQzI4LjIsNDkuNywzMy44LDUzLjksMzkuOCw1Ny42eiIvPgoJPHBhdGggZD0iTTI2LjIsODguMmMzLjMsMiw2LjcsMy42LDEwLjIsNC43Yy0zLjUtNi4yLTYuMy0xMi42LTguOC0xOC41Yy0zLjEtNy4yLTUuOC0xMy41LTktMTcuMmMtMS45LDgtMiwxNi40LTAuMywyNC43ICAgQzIwLjYsODQuMiwyMy4yLDg2LjMsMjYuMiw4OC4yeiIvPgoJPHBhdGggZD0iTTMwLjksNzNjMi45LDYuOCw2LjEsMTQuNCwxMC41LDIxLjJjMTUuNiwzLDMyLTIuMyw0Mi42LTE0LjZDNjcuNyw3Niw1Mi4yLDY5LjYsMzcuOSw2MC43QzMyLDU3LDI2LjUsNTMsMjEuMyw0OC42ICAgYy0wLjYsMS41LTEuMiwzLTEuNyw0LjZDMjQuMSw1Ny4xLDI3LjMsNjQuNSwzMC45LDczeiIvPgo8L2c+Cjwvc3ZnPg==';
const png = await svgUriToPng(document, testCase);
expect(png).toBeInstanceOf(Uint8Array);
});
it('should throw error on invalid svg uri', async () => {
// We are mocking console.error since jsdom throws errors to console when we try to load an invalid img
// https://github.com/facebook/jest/pull/5267#issuecomment-356605468
const consoleError = console.error;
console.error = jest.fn();
const testCases: Array<string> = [
'data:image/svg+xml;base64,error',
'invalid',
];
for (const testCase of testCases) {
await expect(svgUriToPng(document, testCase)).rejects.toBeInstanceOf(Error);
}
console.error = consoleError;
});
});

View File

@@ -2,6 +2,7 @@ import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index'
import { _ } from '@joplin/lib/locale';
import { copyHtmlToClipboard } from './clipboardUtils';
import bridge from '../../../services/bridge';
import { ContextMenuItemType, ContextMenuOptions, ContextMenuItems, resourceInfo, textToDataUri, svgUriToPng } from './contextMenuUtils';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
import Resource from '@joplin/lib/models/Resource';
@@ -10,43 +11,10 @@ import BaseModel from '@joplin/lib/BaseModel';
import { processPastedHtml } from './resourceHandling';
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
const fs = require('fs-extra');
const { writeFile } = require('fs-extra');
const { clipboard } = require('electron');
const { toSystemSlashes } = require('@joplin/lib/path-utils');
export enum ContextMenuItemType {
None = '',
Image = 'image',
Resource = 'resource',
Text = 'text',
Link = 'link',
}
export interface ContextMenuOptions {
itemType: ContextMenuItemType;
resourceId: string;
linkToCopy: string;
textToCopy: string;
htmlToCopy: string;
insertContent: Function;
isReadOnly?: boolean;
}
interface ContextMenuItem {
label: string;
onAction: Function;
isActive: Function;
}
interface ContextMenuItems {
[key: string]: ContextMenuItem;
}
async function resourceInfo(options: ContextMenuOptions): Promise<any> {
const resource = options.resourceId ? await Resource.load(options.resourceId) : null;
const resourcePath = resource ? Resource.fullPath(resource) : '';
return { resource, resourcePath };
}
function handleCopyToClipboard(options: ContextMenuOptions) {
if (options.textToCopy) {
clipboard.writeText(options.textToCopy);
@@ -55,6 +23,12 @@ function handleCopyToClipboard(options: ContextMenuOptions) {
}
}
async function saveFileData(data: any, filename: string) {
const newFilePath = await bridge().showSaveDialog({ defaultPath: filename });
if (!newFilePath) return;
await writeFile(newFilePath, data);
}
export async function openItemById(itemId: string, dispatch: Function, hash: string = '') {
const item = await BaseItem.loadItemById(itemId);
@@ -100,7 +74,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
onAction: async (options: ContextMenuOptions) => {
await openItemById(options.resourceId, dispatch);
},
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && (itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource),
},
saveAs: {
label: _('Save as...'),
@@ -112,7 +86,32 @@ export function menuItems(dispatch: Function): ContextMenuItems {
if (!filePath) return;
await fs.copy(resourcePath, filePath);
},
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
// We handle images received as text seperately as it can be saved in multiple formats
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && (itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource),
},
saveAsSvg: {
label: _('Save as SVG'),
onAction: async (options: ContextMenuOptions) => {
await saveFileData(options.textToCopy, options.filename);
},
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy && itemType === ContextMenuItemType.Image && options.mime?.startsWith('image/svg'),
},
saveAsPng: {
label: _('Save as PNG'),
onAction: async (options: ContextMenuOptions) => {
// First convert it to png then save
if (options.mime != 'image/svg+xml') {
throw new Error(`Unsupported image type: ${options.mime}`);
}
if (!options.filename) {
throw new Error('Filename is needed to save as png');
}
const dataUri = textToDataUri(options.textToCopy, options.mime);
const png = await svgUriToPng(document, dataUri);
const filename = options.filename.replace('.svg', '.png');
await saveFileData(png, filename);
},
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy && itemType === ContextMenuItemType.Image && options.mime?.startsWith('image/svg'),
},
revealInFolder: {
label: _('Reveal file in folder'),
@@ -120,13 +119,20 @@ export function menuItems(dispatch: Function): ContextMenuItems {
const { resourcePath } = await resourceInfo(options);
bridge().showItemInFolder(resourcePath);
},
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
},
copyPathToClipboard: {
label: _('Copy path to clipboard'),
onAction: async (options: ContextMenuOptions) => {
const { resourcePath } = await resourceInfo(options);
clipboard.writeText(toSystemSlashes(resourcePath));
let path = '';
if (options.textToCopy && options.mime) {
path = textToDataUri(options.textToCopy, options.mime);
} else {
const { resourcePath } = await resourceInfo(options);
if (resourcePath) path = toSystemSlashes(resourcePath);
}
if (!path) return;
clipboard.writeText(path);
},
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
},
@@ -137,7 +143,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
const image = bridge().createImageFromPath(resourcePath);
clipboard.writeImage(image);
},
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image,
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && itemType === ContextMenuItemType.Image,
},
cut: {
label: _('Cut'),
@@ -145,14 +151,14 @@ export function menuItems(dispatch: Function): ContextMenuItems {
handleCopyToClipboard(options);
options.insertContent('');
},
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy),
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType != ContextMenuItemType.Image && (!options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy)),
},
copy: {
label: _('Copy'),
onAction: async (options: ContextMenuOptions) => {
handleCopyToClipboard(options);
},
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy || !!options.htmlToCopy,
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType != ContextMenuItemType.Image && (!!options.textToCopy || !!options.htmlToCopy),
},
paste: {
label: _('Paste'),
@@ -184,7 +190,6 @@ export default async function contextMenu(options: ContextMenuOptions, dispatch:
const items = menuItems(dispatch);
if (!('readyOnly' in options)) options.isReadOnly = true;
for (const itemKey in items) {
const item = items[itemKey];

View File

@@ -0,0 +1,92 @@
import Resource from '@joplin/lib/models/Resource';
export enum ContextMenuItemType {
None = '',
Image = 'image',
Resource = 'resource',
Text = 'text',
Link = 'link',
}
export interface ContextMenuOptions {
itemType: ContextMenuItemType;
resourceId: string;
mime: string;
filename: string;
linkToCopy: string;
textToCopy: string;
htmlToCopy: string;
insertContent: Function;
isReadOnly?: boolean;
}
export interface ContextMenuItem {
label: string;
onAction: Function;
isActive: Function;
}
export interface ContextMenuItems {
[key: string]: ContextMenuItem;
}
export async function resourceInfo(options: ContextMenuOptions): Promise<any> {
const resource = options.resourceId ? await Resource.load(options.resourceId) : null;
const filePath = resource ? Resource.fullPath(resource) : null;
const filename = resource ? (resource.filename ? resource.filename : resource.title) : options.filename ? options.filename : '';
return { resource, filePath, filename };
}
export function textToDataUri(text: string, mime: string): string {
return `data:${mime};base64,${Buffer.from(text).toString('base64')}`;
}
export const svgUriToPng = (document: Document, svg: string) => {
return new Promise<Uint8Array>((resolve, reject) => {
let canvas: HTMLCanvasElement;
let img: HTMLImageElement;
const cleanUpAndReject = (e: Error) => {
if (canvas) canvas.remove();
if (img) img.remove();
return reject(e);
};
try {
img = document.createElement('img');
if (!img) throw new Error('Failed to create img element');
} catch (e) {
return cleanUpAndReject(e);
}
img.onload = function() {
try {
canvas = document.createElement('canvas');
if (!canvas) throw new Error('Failed to create canvas element');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Failed to get context');
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
const pngUri = canvas.toDataURL('image/png');
if (!pngUri) throw new Error('Failed to generate png uri');
const pngBase64 = pngUri.split(',')[1];
const byteString = atob(pngBase64);
// write the bytes of the string to a typed array
const buff = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
buff[i] = byteString.charCodeAt(i);
}
canvas.remove();
img.remove();
resolve(buff);
} catch (err) {
cleanUpAndReject(err);
}
};
img.onerror = function(e) {
cleanUpAndReject(new Error(e.toString()));
};
img.src = svg;
});
};

View File

@@ -35,6 +35,8 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
const menu = await contextMenu({
itemType: arg0 && arg0.type,
resourceId: arg0.resourceId,
filename: arg0.filename,
mime: arg0.mime,
textToCopy: arg0.textToCopy,
linkToCopy: arg0.linkToCopy || null,
htmlToCopy: '',

View File

@@ -231,6 +231,13 @@ class PromptDialog extends React.Component {
}
const buttonComps = [];
if (buttonTypes.indexOf('create') >= 0) {
buttonComps.push(
<button key="create" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'create')}>
{_('Create')}
</button>
);
}
if (buttonTypes.indexOf('ok') >= 0) {
buttonComps.push(
<button key="ok" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'ok')}>

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
import { _, _n } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
@@ -15,6 +15,7 @@ import Button from './Button/Button';
import { connect } from 'react-redux';
import { AppState } from '../app.reducer';
import { getEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import SyncTargetRegistry from '../../lib/SyncTargetRegistry';
const { clipboard } = require('electron');
interface Props {
@@ -22,6 +23,7 @@ interface Props {
noteIds: Array<string>;
onClose: Function;
shares: StateShare[];
syncTargetId: number;
}
function styles_(props: Props) {
@@ -69,9 +71,10 @@ export function ShareNoteDialog(props: Props) {
console.info('Render ShareNoteDialog');
const [notes, setNotes] = useState<NoteEntity[]>([]);
const [recursiveShare, setRecursiveShare] = useState<boolean>(false);
const [sharesState, setSharesState] = useState<string>('unknown');
// const [shares, setShares] = useState<SharesMap>({});
const syncTargetInfo = useMemo(() => SyncTargetRegistry.infoById(props.syncTargetId), [props.syncTargetId]);
const noteCount = notes.length;
const theme = themeStyle(props.themeId);
const styles = styles_(props);
@@ -102,7 +105,7 @@ export function ShareNoteDialog(props: Props) {
clipboard.writeText(links.join('\n'));
};
const shareLinkButton_click = async () => {
const shareLinkButton_click = useCallback(async () => {
const service = ShareService.instance();
let hasSynced = false;
@@ -121,7 +124,7 @@ export function ShareNoteDialog(props: Props) {
const newShares: StateShare[] = [];
for (const note of notes) {
const share = await service.shareNote(note.id);
const share = await service.shareNote(note.id, recursiveShare);
newShares.push(share);
}
@@ -149,17 +152,7 @@ export function ShareNoteDialog(props: Props) {
break;
}
};
// const removeNoteButton_click = (event: any) => {
// const newNotes = [];
// for (let i = 0; i < notes.length; i++) {
// const n = notes[i];
// if (n.id === event.noteId) continue;
// newNotes.push(n);
// }
// setNotes(newNotes);
// };
}, [recursiveShare, notes]);
const unshareNoteButton_click = async (event: any) => {
await ShareService.instance().unshareNote(event.noteId);
@@ -171,22 +164,6 @@ export function ShareNoteDialog(props: Props) {
<Button tooltip={_('Unpublish note')} iconName="fas fa-share-alt" onClick={() => unshareNoteButton_click({ noteId: note.id })}/>
);
// const removeButton = notes.length <= 1 ? null : (
// <Button iconName="fa fa-times" onClick={() => removeNoteButton_click({ noteId: note.id })}/>
// );
// const unshareButton = !shares[note.id] ? null : (
// <button onClick={() => unshareNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
// <i style={styles.noteRemoveButtonIcon} className={'fas fa-share-alt'}></i>
// </button>
// );
// const removeButton = notes.length <= 1 ? null : (
// <button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
// <i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
// </button>
// );
return (
<div key={note.id} style={styles.note}>
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
@@ -214,11 +191,26 @@ export function ShareNoteDialog(props: Props) {
return <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}<hr/></div>;
}
function renderContent() {
const onRecursiveShareChange = useCallback(() => {
setRecursiveShare(v => !v);
}, []);
const renderRecursiveShareCheckbox = () => {
if (!syncTargetInfo.supportsRecursiveLinkedNotes) return null;
return (
<div style={styles.root}>
<div className="form-input-group form-input-group-checkbox">
<input id="recursiveShare" name="recursiveShare" type="checkbox" checked={!!recursiveShare} onChange={onRecursiveShareChange} /> <label htmlFor="recursiveShare">{_('Also publish linked notes')}</label>
</div>
);
};
const renderContent = () => {
return (
<div style={styles.root} className="form">
<DialogTitle title={_('Publish Notes')}/>
{renderNoteList(notes)}
{renderRecursiveShareCheckbox()}
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
{renderEncryptionWarningMessage()}
@@ -230,7 +222,7 @@ export function ShareNoteDialog(props: Props) {
/>
</div>
);
}
};
return (
<Dialog renderContent={renderContent}/>
@@ -240,6 +232,7 @@ export function ShareNoteDialog(props: Props) {
const mapStateToProps = (state: AppState) => {
return {
shares: state.shareService.shares.filter(s => !!s.note_id),
syncTargetId: state.settings['sync.target'],
};
};

View File

@@ -17,11 +17,12 @@ import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
import Tag from '@joplin/lib/models/Tag';
import Logger from '@joplin/lib/Logger';
import { FolderEntity } from '@joplin/lib/services/database/types';
import { FolderEntity, FolderIcon } from '@joplin/lib/services/database/types';
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
import { store } from '@joplin/lib/reducer';
import PerFolderSortOrderService from '../../services/sortOrder/PerFolderSortOrderService';
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import FolderIconBox from '../FolderIconBox';
const { connect } = require('react-redux');
const shared = require('@joplin/lib/components/shared/side-menu-shared.js');
const { themeStyle } = require('@joplin/lib/theme');
@@ -77,6 +78,12 @@ function ExpandLink(props: any) {
);
}
const renderFolderIcon = (folderIcon: FolderIcon) => {
if (!folderIcon) return null;
return <div style={{ marginRight: 5, display: 'flex' }}><FolderIconBox folderIcon={folderIcon}/></div>;
};
function FolderItem(props: any) {
const { hasChildren, isExpanded, parentId, depth, selected, folderId, folderTitle, folderIcon, anchorRef, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
@@ -84,8 +91,6 @@ function FolderItem(props: any) {
const shareIcon = shareId && !parentId ? <StyledShareIcon className="fas fa-share-alt"></StyledShareIcon> : null;
const icon = folderIcon ? <span style={{ fontSize: 20, marginRight: 5 }}>{folderIcon.emoji}</span> : null;
return (
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth} ${selected ? 'selected' : ''}`} onDragStart={onFolderDragStart_} onDragOver={onFolderDragOver_} onDrop={onFolderDrop_} draggable={true} data-folder-id={folderId}>
<ExpandLink themeId={props.themeId} hasChildren={hasChildren} folderId={folderId} onClick={onFolderToggleClick_} isExpanded={isExpanded}/>
@@ -105,7 +110,7 @@ function FolderItem(props: any) {
}}
onDoubleClick={onFolderToggleClick_}
>
{icon}<span className="title" style={{ lineHeight: 0 }}>{folderTitle}</span>
{renderFolderIcon(folderIcon)}<span className="title" style={{ lineHeight: 0 }}>{folderTitle}</span>
{shareIcon} {noteCountComp}
</StyledListItemAnchor>
</StyledListItem>

View File

@@ -193,34 +193,36 @@ export default function(props: Props) {
const onJoplinCloudLoginClick = useCallback(async () => {
setJoplinCloudLoginInProgress(true);
let result = null;
try {
const result = await SyncTargetJoplinCloud.checkConfig({
result = await SyncTargetJoplinCloud.checkConfig({
password: () => joplinCloudPassword,
path: () => Setting.value('sync.10.path'),
userContentPath: () => Setting.value('sync.10.userContentPath'),
username: () => joplinCloudEmail,
});
if (result.ok) {
Setting.setValue('sync.target', 10);
Setting.setValue('sync.10.username', joplinCloudEmail);
Setting.setValue('sync.10.password', joplinCloudPassword);
await Setting.saveAll();
alert(_('Thank you! Your Joplin Cloud account is now setup and ready to use.'));
closeDialog(props.dispatch);
props.dispatch({
type: 'NAV_GO',
routeName: 'Main',
});
} else {
alert(_('There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s', result.errorMessage));
}
} finally {
setJoplinCloudLoginInProgress(false);
}
if (result.ok) {
Setting.setValue('sync.target', 10);
Setting.setValue('sync.10.username', joplinCloudEmail);
Setting.setValue('sync.10.password', joplinCloudPassword);
await Setting.saveAll();
alert(_('Thank you! Your Joplin Cloud account is now setup and ready to use.'));
closeDialog(props.dispatch);
props.dispatch({
type: 'NAV_GO',
routeName: 'Main',
});
} else {
alert(_('There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s', result.errorMessage));
}
}, [joplinCloudEmail, joplinCloudPassword, props.dispatch]);
const onJoplinCloudCreateAccountClick = useCallback(() => {
@@ -230,10 +232,10 @@ export default function(props: Props) {
function renderJoplinCloudLoginForm() {
return (
<JoplinCloudLoginForm>
<div>{_('Login below.')} <CreateAccountLink href="#" onClick={onJoplinCloudCreateAccountClick}>{_('Or create an account.')}</CreateAccountLink></div>
<FormLabel>Email</FormLabel>
<div style={{ fontSize: '16px' }}>{_('Login below.')} <CreateAccountLink href="#" onClick={onJoplinCloudCreateAccountClick}>{_('Or create an account.')}</CreateAccountLink></div>
<FormLabel>{_('Email')}</FormLabel>
<StyledInput type="email" onChange={onJoplinCloudEmailChange}/>
<FormLabel>Password</FormLabel>
<FormLabel>{_('Password')}</FormLabel>
<StyledInput type="password" onChange={onJoplinCloudPasswordChange}/>
<SelectButton mt="1.3em" disabled={joplinCloudLoginInProgress} level={ButtonLevel.Primary} title={_('Login')} onClick={onJoplinCloudLoginClick}/>
</JoplinCloudLoginForm>

View File

@@ -40,8 +40,13 @@ export default function() {
'toggleVisiblePanes',
'editor.deleteLine',
'editor.duplicateLine',
'editor.undo',
'editor.redo',
// We cannot put the undo/redo commands in the menu because they are
// editor-specific commands. If we put them there it will break the
// undo/redo in regular text fields.
// https://github.com/laurent22/joplin/issues/6214
// 'editor.undo',
// 'editor.redo',
'editor.indentLess',
'editor.indentMore',
'editor.toggleComment',
@@ -55,5 +60,10 @@ export default function() {
'gotoAnything',
'commandPalette',
'openMasterPasswordDialog',
'addProfile',
'editProfileConfig',
'switchProfile1',
'switchProfile2',
'switchProfile3',
];
}

View File

@@ -116,7 +116,7 @@
const now = Date.now();
if (now >= ignoreNextScrollTime_) ignoreNextScrollEventCount_ = 0;
if (ignoreNextScrollEventCount_ < 10) { // for safety
ignoreNextScrollTime_ = now + 200;
ignoreNextScrollTime_ = now + 1000;
ignoreNextScrollEventCount_ += 1;
}
};
@@ -293,7 +293,7 @@
return;
}
}
if (!heightChanged) return;
if (!heightChanged && cause !== 'dom-changed') return;
const restoreAndRefresh = () => {
scrollmap.refresh();
restorePercentScroll();
@@ -337,7 +337,11 @@
contentElement.innerHTML = html;
scrollmap.create(event.options.markupLineCount);
restorePercentScroll(); // First, a quick treatment is applied.
if (typeof event.options.percent !== 'number') {
restorePercentScroll(); // First, a quick treatment is applied.
} else {
setPercentScroll(event.options.percent);
}
addPluginAssets(event.options.pluginAssets);
@@ -586,9 +590,24 @@
}));
document.addEventListener('contextmenu', webviewLib.logEnabledEventHandler(event => {
let element = event.target;
// To handle right clicks on resource icons
let element = event.target;
// Mermaid svgs are wrapped inside a <pre> with class "mermaid"
let mermaidElement = element.closest(".mermaid")?.children[0];
if (mermaidElement) {
const svgString = new XMLSerializer().serializeToString(mermaidElement);
if (!!svgString) {
ipcProxySendToHost('contextMenu', {
type: 'image',
textToCopy: svgString,
mime: 'image/svg+xml',
filename: mermaidElement.id + '.svg',
});
}
return;
}
if (element && !element.getAttribute('data-resource-id')) element = element.parentElement;
if (element && element.getAttribute('data-resource-id')) {

View File

@@ -45,7 +45,8 @@ scrollmap.get_ = () => {
// Each map entry is total-ordered.
let last = 0;
for (let i = 0; i < elems.length; i++) {
const top = elems[i].getBoundingClientRect().top - offset;
const rect = elems[i].getBoundingClientRect();
const top = rect.top - offset;
const line = Number(elems[i].getAttribute('source-line'));
const percent = Math.max(0, Math.min(1, top / height));
if (map.line[last] < line && map.percent[last] < percent) {
@@ -53,12 +54,20 @@ scrollmap.get_ = () => {
map.percent.push(percent);
last += 1;
}
const bottom = rect.bottom - offset;
const lineEnd = Number(elems[i].getAttribute('source-line-end'));
const percentEnd = Math.max(0, Math.min(1, bottom / height));
if (map.line[last] < lineEnd && map.percent[last] < percentEnd) {
map.line.push(lineEnd);
map.percent.push(percentEnd);
last += 1;
}
}
const lineCount = scrollmap.lineCount_;
if (lineCount) {
map.lineCount = lineCount;
} else {
if (map.lineCount <= map.line[last]) map.lineCount = map.line[last] + 1;
if (map.lineCount < map.line[last]) map.lineCount = map.line[last];
}
if (map.percent[last] < 1) {
map.line.push(lineCount || 1e10);

View File

@@ -0,0 +1,20 @@
/**
* A Jest custom test Environment to load the resources for the tests.
* Use this test envirenment when you work with resources like images, files.
* See gui/NoteEditor/utils/contextMenu.test.ts for an example.
*/
const JSDOMEnvironment = require('jest-environment-jsdom');
import type { EnvironmentContext } from '@jest/environment';
import type { Config } from '@jest/types';
export default class CustomEnvironment extends JSDOMEnvironment {
constructor(config: Config.ProjectConfig, context?: EnvironmentContext) {
// Resources is set to 'usable' to enable fetching of resources like images and fonts while testing
// Which does not happen by default in jest
// https://stackoverflow.com/a/49482563
config.testEnvironmentOptions.resources = 'usable';
super(config, context);
}
}

View File

@@ -112,7 +112,15 @@ document.addEventListener('auxclick', event => event.preventDefault());
// Each link (rendered as a button or list item) has its own custom click event
// so disable the default. In particular this will disable Ctrl+Clicking a link
// which would open a new browser window.
document.addEventListener('click', (event) => event.preventDefault());
document.addEventListener('click', (event) => {
// We don't apply this to labels and inputs because it would break
// checkboxes. Such a global event handler is probably not a good idea
// anyway but keeping it for now, as it doesn't seem to break anything else.
// https://github.com/facebook/react/issues/13477#issuecomment-489274045
if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return;
event.preventDefault();
});
app().start(bridge().processArgv()).then((result) => {
if (!result || !result.action) {

View File

@@ -180,6 +180,22 @@ h2 {
margin-bottom: 10px;
}
.form > .form-input-group-checkbox {
display: flex;
flex-direction: row;
align-items: center;
}
.form > .form-input-group-checkbox > input {
display: flex;
margin-right: 6px;
}
.form > .form-input-group-checkbox > label {
display: flex;
margin-bottom: 0;
}
.bold {
font-weight: bold;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.7.8",
"version": "2.7.14",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -67,7 +67,7 @@
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"allowToChangeInstallationDirectory": false,
"differentialPackage": false
},
"portable": {
@@ -116,6 +116,7 @@
"app-builder-bin": "^1.9.11",
"babel-cli": "^6.26.0",
"babel-preset-react": "^6.24.1",
"canvas": "^2.9.0",
"electron": "14.1.0",
"electron-builder": "^22.11.7",
"electron-notarize": "^1.0.0",

View File

@@ -146,8 +146,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097665
versionName "2.7.0"
versionCode 2097667
versionName "2.7.2"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -210,7 +210,7 @@ class CameraView extends Component {
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'flex-end' }}>
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginBottom: 20 }}>
{ reverseCameraButton }
<TouchableOpacity onPress={this.photo_onPress}>
<TouchableOpacity onPress={this.photo_onPress} disabled={this.state.snapping}>
<View style={{ flexDirection: 'row', borderRadius: 90, width: 90, height: 90, backgroundColor: '#ffffffaa', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Icon
name={photoIcon}

View File

@@ -38,10 +38,9 @@ class Dropdown extends React.Component {
const listTop = Math.min(maxListTop, this.state.headerSize.y + this.state.headerSize.height);
const wrapperStyle = {
width: this.state.headerSize.width,
height: listHeight + 2, // +2 for the border (otherwise it makes the scrollbar appear)
marginTop: listTop,
marginLeft: this.state.headerSize.x,
alignSelf: 'center',
};
const itemListStyle = Object.assign({}, this.props.itemListStyle ? this.props.itemListStyle : {}, {
@@ -87,6 +86,7 @@ class Dropdown extends React.Component {
if (this.props.labelTransform && this.props.labelTransform === 'trim') headerLabel = headerLabel.trim();
const closeList = () => {
if (this.props.onClose()) this.props.onClose();
this.setState({ listVisible: false });
};
@@ -116,6 +116,7 @@ class Dropdown extends React.Component {
onPress={() => {
this.updateHeaderCoordinates();
this.setState({ listVisible: true });
if (this.props.onOpen()) this.props.onOpen();
}}
>
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>

View File

@@ -19,6 +19,8 @@ interface CodeMirrorResult {
editor: EditorView;
undo: Function;
redo: Function;
select: (anchor: number, head: number)=> void;
insertText: (text: string)=> void;
}
function postMessage(name: string, data: any) {
@@ -36,25 +38,53 @@ function logMessage(...msg: any[]) {
//
// https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
//
// For a tutorial, see:
//
// https://codemirror.net/6/examples/styling/#themes
//
// Use Safari developer tools to view the content of the CodeMirror iframe while
// the app is running. It seems that what appears as ".ͼ1" in the CSS is the
// equivalent of "&" in the theme object. So to target ".ͼ1.cm-focused", you'd
// use '&.cm-focused' in the theme.
const createTheme = (theme: any): Extension => {
const isDarkTheme = theme.appearance === 'dark';
const baseGlobalStyle: Record<string, string> = {
color: theme.color,
backgroundColor: theme.backgroundColor,
fontFamily: theme.fontFamily,
fontSize: `${theme.fontSize}px`,
};
const baseCursorStyle: Record<string, string> = { };
const baseContentStyle: Record<string, string> = { };
const baseSelectionStyle: Record<string, string> = { };
// If we're in dark mode, the caret and selection are difficult to see.
// Adjust them appropriately
if (isDarkTheme) {
// Styling the caret requires styling both the caret itself
// and the CodeMirror caret.
// See https://codemirror.net/6/examples/styling/#themes
baseContentStyle.caretColor = 'white';
baseCursorStyle.borderLeftColor = 'white';
baseSelectionStyle.backgroundColor = '#6b6b6b';
}
const baseTheme = EditorView.baseTheme({
'&': {
color: theme.color,
backgroundColor: theme.backgroundColor,
fontFamily: theme.fontFamily,
fontSize: `${theme.fontSize}px`,
},
'&': baseGlobalStyle,
// These must be !important or more specific than CodeMirror's built-ins
'.cm-content': baseContentStyle,
'&.cm-focused .cm-cursor': baseCursorStyle,
'&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle,
'&.cm-focused': {
outline: 'none',
},
});
const appearanceTheme = EditorView.theme({}, { dark: theme.appearance === 'dark' });
const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme });
const baseHeadingStyle = {
fontWeight: 'bold',
@@ -152,6 +182,13 @@ export function initCodeMirror(parentElement: any, initialText: string, theme: a
postMessage('onChange', { value: editor.state.doc.toString() });
schedulePostUndoRedoDepthChange(editor);
}
if (!viewUpdate.state.selection.eq(viewUpdate.startState.selection)) {
const mainRange = viewUpdate.state.selection.main;
const selStart = mainRange.from;
const selEnd = mainRange.to;
postMessage('onSelectionChange', { selection: { start: selStart, end: selEnd } });
}
}),
],
doc: initialText,
@@ -169,5 +206,14 @@ export function initCodeMirror(parentElement: any, initialText: string, theme: a
redo(editor);
schedulePostUndoRedoDepthChange(editor, true);
},
select: (anchor: number, head: number) => {
editor.dispatch(editor.state.update({
selection: { anchor, head },
scrollIntoView: true,
}));
},
insertText: (text: string) => {
editor.dispatch(editor.state.replaceSelection(text));
},
};
}

View File

@@ -2,7 +2,7 @@ import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import { themeStyle } from '@joplin/lib/theme';
const React = require('react');
const { forwardRef, useImperativeHandle, useEffect, useState, useCallback, useRef } = require('react');
const { forwardRef, useImperativeHandle, useEffect, useMemo, useState, useCallback, useRef } = require('react');
const { WebView } = require('react-native-webview');
const { editorFont } = require('../global-style');
@@ -15,14 +15,27 @@ export interface UndoRedoDepthChangeEvent {
redoDepth: number;
}
export interface Selection {
start: number;
end: number;
}
export interface SelectionChangeEvent {
selection: Selection;
}
type ChangeEventHandler = (event: ChangeEvent)=> void;
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
type SelectionChangeEventHandler = (event: SelectionChangeEvent)=> void;
interface Props {
themeId: number;
initialText: string;
initialSelection?: Selection;
style: any;
onChange: ChangeEventHandler;
onSelectionChange: SelectionChangeEventHandler;
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
}
@@ -31,6 +44,7 @@ function fontFamilyFromSettings() {
return [f, 'sans-serif'].join(', ');
}
// Obsolete with CodeMirror 6. See ./CodeMirror.ts for styling.
// function useCss(themeId:number):string {
// const [css, setCss] = useState('');
@@ -169,6 +183,17 @@ function fontFamilyFromSettings() {
// return css;
// }
function useCss(themeId: number): string {
return useMemo(() => {
const theme = themeStyle(themeId);
return `
:root {
background-color: ${theme.backgroundColor};
}
`;
}, [themeId]);
}
function useHtml(css: string): string {
const [html, setHtml] = useState('');
@@ -211,11 +236,15 @@ function NoteEditor(props: Props, ref: any) {
const [source, setSource] = useState(undefined);
const webviewRef = useRef(null);
const setInitialSelectionJS = props.initialSelection ? `
cm.select(${props.initialSelection.start}, ${props.initialSelection.end});
` : '';
const injectedJavaScript = `
function postMessage(name, data) {
window.ReactNativeWebView.postMessage(JSON.stringify({
data,
name,
name,
}));
}
@@ -226,7 +255,7 @@ function NoteEditor(props: Props, ref: any) {
// This variable is not used within this script
// but is called using "injectJavaScript" from
// the wrapper component.
let cm = null;
window.cm = null;
try {
${shim.injectedJs('codeMirrorBundle')};
@@ -236,6 +265,7 @@ function NoteEditor(props: Props, ref: any) {
const initialText = ${JSON.stringify(props.initialText)};
cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, theme);
${setInitialSelectionJS}
} catch (e) {
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
} finally {
@@ -243,8 +273,8 @@ function NoteEditor(props: Props, ref: any) {
}
`;
// const css = useCss(props.themeId);
const html = useHtml('');
const css = useCss(props.themeId);
const html = useHtml(css);
useImperativeHandle(ref, () => {
return {
@@ -254,6 +284,14 @@ function NoteEditor(props: Props, ref: any) {
redo: function() {
webviewRef.current.injectJavaScript('cm.redo(); true;');
},
select: (anchor: number, head: number) => {
webviewRef.current.injectJavaScript(
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)}); true;`
);
},
insertText: (text: string) => {
webviewRef.current.injectJavaScript(`cm.insertText(${JSON.stringify(text)}); true;`);
},
};
});
@@ -300,6 +338,10 @@ function NoteEditor(props: Props, ref: any) {
console.info('onUndoRedoDepthChange', event);
props.onUndoRedoDepthChange(event);
},
onSelectionChange: (event: SelectionChangeEvent) => {
props.onSelectionChange(event);
},
};
if (handlers[msg.name]) {

View File

@@ -103,7 +103,7 @@ export default class SelectDateTimeDialog extends React.PureComponent<any, any>
return (
<View style={{ flex: 0, margin: 20, alignItems: 'center' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{ this.state.date && <Text style={{ ...theme.normalText, marginRight: 10 }}>{time.formatDateToLocal(this.state.date)}</Text> }
{ this.state.date && <Text style={{ ...theme.normalText,color: theme.color, marginRight: 10 }}>{time.formatDateToLocal(this.state.date)}</Text> }
<Button title="Set date" onPress={this.onSetDate} />
</View>
<DateTimePickerModal

View File

@@ -29,8 +29,10 @@ class ScreenHeaderComponent extends React.PureComponent {
constructor() {
super();
this.styles_ = {};
this.state = { showUndoRedoButtons: true };
}
styles() {
const themeId = Setting.value('theme');
if (this.styles_[themeId]) return this.styles_[themeId];
@@ -256,7 +258,7 @@ class ScreenHeaderComponent extends React.PureComponent {
}
const renderTopButton = (options) => {
if (!options.visible) return null;
if (!options.visible || !this.state.showUndoRedoButtons) return null;
const icon = <Icon name={options.iconName} style={this.styles().topIcon} />;
const viewStyle = options.disabled ? this.styles().iconButtonDisabled : this.styles().iconButton;
@@ -422,6 +424,16 @@ class ScreenHeaderComponent extends React.PureComponent {
color: theme.color,
fontSize: theme.fontSize,
}}
onOpen={() => {
this.setState({
showUndoRedoButtons: false,
});
}}
onClose={() => {
this.setState({
showUndoRedoButtons: true,
});
}}
onValueChange={async (folderId, itemIndex) => {
// If onValueChange is specified, use this as a callback, otherwise do the default
// which is to take the selectedNoteIds from the state and move them to the

View File

@@ -530,9 +530,9 @@ class ConfigScreenComponent extends BaseScreenComponent {
if (this.state.profileExportStatus === 'prompt') {
const profileExportPrompt = (
<View style={this.styles().settingContainer} key="profileExport">
<Text style={this.styles().settingText}>Path:</Text>
<TextInput style={{ ...this.styles().textInput, paddingRight: 20 }} onChange={(event: any) => this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard" keyboardAppearance={theme.keyboardAppearance}></TextInput>
<Button title="OK" onPress={this.exportProfileButtonPress2_}></Button>
<Text style={{ ...this.styles().settingText, flex: 0 }}>Path:</Text>
<TextInput style={{ ...this.styles().textInput, paddingRight: 20, width: '75%', marginRight: 'auto' }} onChange={(event: any) => this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard" keyboardAppearance={theme.keyboardAppearance} />
<Button title="OK" onPress={this.exportProfileButtonPress2_} />
</View>
);

View File

@@ -93,9 +93,6 @@ class NoteScreenComponent extends BaseScreenComponent {
this.doFocusUpdate_ = false;
// iOS doesn't support multiline text fields properly so disable it
this.enableMultilineTitle_ = Platform.OS !== 'ios';
this.saveButtonHasBeenShown_ = false;
this.styles_ = {};
@@ -231,7 +228,6 @@ class NoteScreenComponent extends BaseScreenComponent {
this.onAlarmDialogAccept = this.onAlarmDialogAccept.bind(this);
this.onAlarmDialogReject = this.onAlarmDialogReject.bind(this);
this.todoCheckbox_change = this.todoCheckbox_change.bind(this);
this.titleTextInput_contentSizeChange = this.titleTextInput_contentSizeChange.bind(this);
this.title_changeText = this.title_changeText.bind(this);
this.undoRedoService_stackChange = this.undoRedoService_stackChange.bind(this);
this.screenHeader_undoButtonPress = this.screenHeader_undoButtonPress.bind(this);
@@ -389,7 +385,6 @@ class NoteScreenComponent extends BaseScreenComponent {
paddingBottom: 10, // Added for iOS (Not needed for Android??)
};
if (this.enableMultilineTitle_) styles.titleTextInput.height = this.state.titleTextInputHeight;
if (this.state.HACK_webviewLoadingState === 1) styles.titleTextInput.marginTop = 1;
this.styles_[cacheKey] = StyleSheet.create(styles);
@@ -493,7 +488,11 @@ class NoteScreenComponent extends BaseScreenComponent {
}
body_selectionChange(event: any) {
this.selection = event.nativeEvent.selection;
if (this.useEditorBeta()) {
this.selection = event.selection;
} else {
this.selection = event.nativeEvent.selection;
}
}
makeSaveAction() {
@@ -713,9 +712,17 @@ class NoteScreenComponent extends BaseScreenComponent {
const newNote = Object.assign({}, this.state.note);
if (this.state.mode == 'edit' && !!this.selection) {
const newText = `\n${resourceTag}\n`;
const prefix = newNote.body.substring(0, this.selection.start);
const suffix = newNote.body.substring(this.selection.end);
newNote.body = `${prefix}\n${resourceTag}\n${suffix}`;
newNote.body = `${prefix}${newText}${suffix}`;
if (this.useEditorBeta()) {
// The beta editor needs to be explicitly informed of changes
// to the note's body
this.editorRef.current.insertText(newText);
}
} else {
newNote.body += `\n${resourceTag}`;
}
@@ -884,11 +891,6 @@ class NoteScreenComponent extends BaseScreenComponent {
output.push({
title: _('Attach...'),
onPress: async () => {
if (this.state.mode === 'edit' && this.useEditorBeta()) {
alert('Attaching files from the beta editor is not yet supported. You may do so from the viewer mode instead.');
return;
}
const buttons = [];
// On iOS, it will show "local files", which means certain files saved from the browser
@@ -971,13 +973,6 @@ class NoteScreenComponent extends BaseScreenComponent {
await this.saveOneProperty('todo_completed', checked ? time.unixMs() : 0);
}
titleTextInput_contentSizeChange(event: any) {
if (!this.enableMultilineTitle_) return;
const height = event.nativeEvent.contentSize.height;
this.setState({ titleTextInputHeight: height });
}
scheduleFocusUpdate() {
if (this.focusUpdateIID_) shim.clearTimeout(this.focusUpdateIID_);
@@ -1137,7 +1132,9 @@ class NoteScreenComponent extends BaseScreenComponent {
ref={this.editorRef}
themeId={this.props.themeId}
initialText={note.body}
initialSelection={this.selection}
onChange={this.onBodyChange}
onSelectionChange={this.body_selectionChange}
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
style={this.styles().bodyTextInput}
/>;
@@ -1178,8 +1175,6 @@ class NoteScreenComponent extends BaseScreenComponent {
<View style={titleContainerStyle}>
{isTodo && <Checkbox style={this.styles().checkbox} checked={!!Number(note.todo_completed)} onChange={this.todoCheckbox_change} />}
<TextInput
onContentSizeChange={this.titleTextInput_contentSizeChange}
multiline={this.enableMultilineTitle_}
ref="titleTextField"
underlineColorAndroid="#ffffff00"
autoCapitalize="sentences"

View File

@@ -21,6 +21,7 @@ class NoteTagsDialogComponent extends React.Component {
tagListData: [],
newTags: '',
savingTags: false,
tagFilter: '',
};
const noteHasTag = tagId => {
@@ -88,6 +89,10 @@ class NoteTagsDialogComponent extends React.Component {
this.cancelButton_press = () => {
if (this.props.onCloseRequested) this.props.onCloseRequested();
};
this.filterTags = (allTags) => {
return allTags.filter((tag) => tag.title.includes(this.state.tagFilter.toLowerCase()), allTags);
};
}
UNSAFE_componentWillMount() {
@@ -140,16 +145,16 @@ class NoteTagsDialogComponent extends React.Component {
fontSize: 20,
color: theme.color,
},
newTagBox: {
tagBox: {
flexDirection: 'row',
alignItems: 'center',
paddingLeft: theme.marginLeft,
paddingRight: theme.marginRight,
paddingLeft: 10,
paddingRight: 10,
borderBottomWidth: 1,
borderBottomColor: theme.dividerColor,
},
newTagBoxLabel: Object.assign({}, theme.normalText, { marginRight: 8 }),
newTagBoxInput: Object.assign({}, theme.lineInput, { flex: 1 }),
tagBoxInput: Object.assign({}, theme.lineInput, { flex: 1 }),
};
this.styles_[themeId] = StyleSheet.create(styles);
@@ -161,7 +166,7 @@ class NoteTagsDialogComponent extends React.Component {
const dialogContent = (
<View style={{ flex: 1 }}>
<View style={this.styles().newTagBox}>
<View style={this.styles().tagBox}>
<Text style={this.styles().newTagBoxLabel}>{_('New tags:')}</Text>
<TextInput
selectionColor={theme.textSelectionColor}
@@ -170,10 +175,23 @@ class NoteTagsDialogComponent extends React.Component {
onChangeText={value => {
this.setState({ newTags: value });
}}
style={this.styles().newTagBoxInput}
style={this.styles().tagBoxInput}
placeholder={_('tag1,tag2,...')}
/>
</View>
<FlatList data={this.state.tagListData} renderItem={this.renderTag} keyExtractor={this.tagKeyExtractor} />
<View style={this.styles().tagBox}>
<TextInput
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
value={this.state.tagFilter}
onChangeText={value => {
this.setState({ tagFilter: value });
}}
placeholder={_('Filter tags')}
style={this.styles().tagBoxInput}
/>
</View>
<FlatList data={this.filterTags(this.state.tagListData)} renderItem={this.renderTag} keyExtractor={this.tagKeyExtractor} />
</View>
);

View File

@@ -1,6 +1,6 @@
const React = require('react');
const Component = React.Component;
const { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Alert } = require('react-native');
const { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Alert, Image } = require('react-native');
const { connect } = require('react-redux');
const Icon = require('react-native-vector-icons/Ionicons').default;
const Folder = require('@joplin/lib/models/Folder').default;
@@ -74,7 +74,7 @@ class SideMenuContentComponent extends Component {
styles.folderButton = Object.assign({}, styles.button);
styles.folderButton.paddingLeft = 0;
styles.folderButtonText = Object.assign({}, styles.buttonText);
styles.folderButtonText = Object.assign({}, styles.buttonText, { paddingLeft: 0 });
styles.folderButtonSelected = Object.assign({}, styles.folderButton);
styles.folderButtonSelected.backgroundColor = theme.selectedColor;
styles.folderIcon = Object.assign({}, theme.icon);
@@ -219,6 +219,18 @@ class SideMenuContentComponent extends Component {
if (actionDone === 'auth') this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
}
renderFolderIcon(theme, folderIcon) {
if (!folderIcon) return null;
if (folderIcon.type === 1) { // FolderIconType.Emoji
return <Text style={{ fontSize: theme.fontSize, marginRight: 4 }}>{folderIcon.emoji}</Text>;
} else if (folderIcon.type === 2) { // FolderIconType.DataUrl
return <Image style={{ width: 20, height: 20, marginRight: 4, resizeMode: 'contain' }} source={{ uri: folderIcon.dataUrl }}/>;
} else {
throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
}
}
renderFolderItem(folder, selected, hasChildren, depth) {
const theme = themeStyle(this.props.themeId);
@@ -228,6 +240,7 @@ class SideMenuContentComponent extends Component {
height: 36,
alignItems: 'center',
paddingRight: theme.marginRight,
paddingLeft: 10,
};
if (selected) folderButtonStyle.backgroundColor = theme.selectedColor;
folderButtonStyle.paddingLeft = depth * 10 + theme.marginLeft;
@@ -253,7 +266,6 @@ class SideMenuContentComponent extends Component {
);
const folderIcon = Folder.unserializeIcon(folder.icon);
const icon = folderIcon ? `${folderIcon.emoji} ` : '';
return (
<View key={folder.id} style={{ flex: 1, flexDirection: 'row' }}>
@@ -267,8 +279,9 @@ class SideMenuContentComponent extends Component {
}}
>
<View style={folderButtonStyle}>
{this.renderFolderIcon(theme, folderIcon)}
<Text numberOfLines={1} style={this.styles().folderButtonText}>
{icon + Folder.displayTitle(folder)}
{Folder.displayTitle(folder)}
</Text>
</View>
</TouchableOpacity>

View File

@@ -492,13 +492,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.7.0;
MARKETING_VERSION = 12.7.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -521,12 +521,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.7.0;
MARKETING_VERSION = 12.7.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -667,14 +667,14 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.7.0;
MARKETING_VERSION = 12.7.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
@@ -698,14 +698,14 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 79;
CURRENT_PROJECT_VERSION = 80;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.7.0;
MARKETING_VERSION = 12.7.1;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -43,6 +43,7 @@ module.exports = {
'@joplin/renderer': path.resolve(__dirname, '../renderer/'),
'@joplin/tools': path.resolve(__dirname, '../tools/'),
'@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
'@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
},
{
get: (target, name) => {
@@ -60,5 +61,6 @@ module.exports = {
path.resolve(__dirname, '../renderer'),
path.resolve(__dirname, '../tools'),
path.resolve(__dirname, '../fork-htmlparser2'),
path.resolve(__dirname, '../fork-uslug'),
],
};

View File

@@ -550,6 +550,7 @@ async function initialize(dispatch: Function) {
// eslint-disable-next-line require-atomic-updates
BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance();
Resource.shareService_ = ShareService.instance();
DecryptionWorker.instance().dispatch = dispatch;
DecryptionWorker.instance().setLogger(mainLogger);
DecryptionWorker.instance().setKvStore(KvStore.instance());

View File

@@ -8,5 +8,6 @@
"author": "<%= pluginAuthor %>",
"homepage_url": "<%= pluginHomepageUrl %>",
"repository_url": "<%= pluginRepositoryUrl %>",
"keywords": []
"keywords": [],
"categories": []
}

View File

@@ -29,6 +29,7 @@ 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 manifest = readManifest(manifestPath);
const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`);
@@ -67,10 +68,19 @@ function currentGitInfo() {
}
}
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`);
});
}
function readManifest(manifestPath) {
const content = fs.readFileSync(manifestPath, 'utf8');
const output = JSON.parse(content);
if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`);
validateCategories(output.categories);
return output;
}

View File

@@ -54,6 +54,9 @@ import { loadMasterKeysFromSettings, migrateMasterPassword } from './services/e2
import SyncTargetNone from './SyncTargetNone';
import { setRSA } from './services/e2ee/ppk';
import RSA from './services/e2ee/RSA.node';
import Resource from './models/Resource';
import { ProfileConfig } from './services/profileConfig/types';
import initProfile from './services/profileConfig/initProfile';
const appLogger: LoggerWrapper = Logger.create('App');
@@ -69,6 +72,7 @@ export default class BaseApplication {
private eventEmitter_: any;
private scheduleAutoAddResourcesIID_: any = null;
private database_: any = null;
private profileConfig_: ProfileConfig = null;
protected showStackTraces_: boolean = false;
protected showPromptString_: boolean = false;
@@ -645,6 +649,12 @@ export default class BaseApplication {
public initRedux() {
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn() as any));
setStore(this.store_);
this.store_.dispatch({
type: 'PROFILE_CONFIG_SET',
value: this.profileConfig_,
});
BaseModel.dispatch = this.store().dispatch;
FoldersScreenUtils.dispatch = this.store().dispatch;
// reg.dispatch = this.store().dispatch;
@@ -713,14 +723,16 @@ export default class BaseApplication {
// https://immerjs.github.io/immer/docs/freezing
setAutoFreeze(initArgs.env === 'dev');
const profileDir = this.determineProfileDir(initArgs);
const rootProfileDir = this.determineProfileDir(initArgs);
const { profileDir, profileConfig, isSubProfile } = await initProfile(rootProfileDir);
this.profileConfig_ = profileConfig;
const resourceDirName = 'resources';
const resourceDir = `${profileDir}/${resourceDirName}`;
const tempDir = `${profileDir}/tmp`;
const cacheDir = `${profileDir}/cache`;
Setting.setConstant('env', initArgs.env);
Setting.setConstant('profileDir', profileDir);
Setting.setConstant('resourceDirName', resourceDirName);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('tempDir', tempDir);
@@ -777,6 +789,7 @@ export default class BaseApplication {
appLogger.info(`Profile directory: ${profileDir}`);
appLogger.info(`Root profile directory: ${rootProfileDir}`);
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
this.database_.setLogExcludedQueryTypes(['SELECT']);
@@ -819,7 +832,7 @@ export default class BaseApplication {
// Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
// Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
Setting.setValue('sync.10.userContentPath', 'http://joplincloud.local:22300');
}
// For now always disable fuzzy search due to performance issues:
@@ -837,6 +850,7 @@ export default class BaseApplication {
}
if ('welcomeDisabled' in initArgs) Setting.setValue('welcome.enabled', !initArgs.welcomeDisabled);
if (isSubProfile) Setting.setValue('welcome.enabled', false);
if (!Setting.value('api.token')) {
void EncryptionService.instance()
@@ -855,6 +869,7 @@ export default class BaseApplication {
BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance();
Resource.shareService_ = ShareService.instance();
DecryptionWorker.instance().setLogger(globalLogger);
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
DecryptionWorker.instance().setKvStore(KvStore.instance());

View File

@@ -33,6 +33,10 @@ export default class BaseSyncTarget {
return true;
}
public static supportsRecursiveLinkedNotes(): boolean {
return false;
}
public option(name: string, defaultValue: any = null) {
return this.options_ && name in this.options_ ? this.options_[name] : defaultValue;
}

View File

@@ -38,6 +38,10 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
return false;
}
public static supportsRecursiveLinkedNotes(): boolean {
return true;
}
public async isAuthenticated() {
return true;
}

View File

@@ -4,33 +4,16 @@ export interface SyncTargetInfo {
label: string;
supportsSelfHosted: boolean;
supportsConfigCheck: boolean;
supportsRecursiveLinkedNotes: boolean;
description: string;
classRef: any;
}
// const syncTargetOrder = [
// 'joplinCloud',
// 'dropbox',
// 'onedrive',
// ];
export default class SyncTargetRegistry {
private static reg_: Record<number, SyncTargetInfo> = {};
private static get reg() {
// if (!this.reg_[0]) {
// this.reg_[0] = {
// id: 0,
// name: SyncTargetNone.targetName(),
// label: SyncTargetNone.label(),
// classRef: SyncTargetNone,
// description: SyncTargetNone.description(),
// supportsSelfHosted: false,
// supportsConfigCheck: false,
// };
// }
return this.reg_;
}
@@ -47,6 +30,10 @@ export default class SyncTargetRegistry {
throw new Error(`Unknown name: ${name}`);
}
public static infoById(id: number): SyncTargetInfo {
return this.infoByName(this.idToName(id));
}
public static addClass(SyncTargetClass: any) {
this.reg[SyncTargetClass.id()] = {
id: SyncTargetClass.id(),
@@ -56,6 +43,7 @@ export default class SyncTargetRegistry {
description: SyncTargetClass.description(),
supportsSelfHosted: SyncTargetClass.supportsSelfHosted(),
supportsConfigCheck: SyncTargetClass.supportsConfigCheck(),
supportsRecursiveLinkedNotes: SyncTargetClass.supportsRecursiveLinkedNotes(),
};
}

View File

@@ -51,6 +51,8 @@
"Add recipient:": "Tilføj modtager:",
"Add title": "Tilføj titel",
"Add to dictionary": "Tilføj til ordbog",
"Admin": "Admin",
"Admin dashboard": "Kontrolpanel til administration",
"Advanced options": "Avancerede indstillinger",
"Advanced tools": "Avancerede værktøjer",
"All notes": "Alle noter",
@@ -78,6 +80,7 @@
"Authorisation token:": "Autoriseringstoken:",
"Auto": "Auto",
"Auto-pair braces, parenthesis, quotations, etc.": "Auto-par klammer, parenteser, citater, etc.",
"Automatically check for updates": "Tjek automatisk efter opdateringer",
"Automatically switch theme to match system theme": "Skifter automatisk tema så det matcher systemets tema",
"Back": "Tilbage",
"Bold": "Fed",
@@ -145,6 +148,7 @@
"Copy": "Kopier",
"Copy dev mode command to clipboard": "Kopier udviklertilstand-kommando til udklipsholderen",
"Copy external link": "Kopiér eksternt link",
"Copy image": "Kopier billede",
"Copy Link Address": "Kopier linkadresse",
"Copy Markdown link": "Kopier markdown link",
"Copy path to clipboard": "Kopier sti til udklipsholder",
@@ -159,6 +163,8 @@
"Could not upgrade master key: %s": "Kunne ikke opgradere hovednøgle: %s",
"Could not verify the share status of this notebook - aborting. Please try again when you are connected to the internet.": "Kunne ikke verificere denne notesbogs status for deling - afbryder. Prøv venligst igen, når du har forbindelse til internettet.",
"Create a notebook": "Opret en notesbog",
"Create notebook": "Opret notesbog",
"Create user": "Opret bruger",
"Created": "Oprettet",
"created date": "oprettelsesdato",
"Created local items: %d.": "Oprettede lokale emner: %d.",
@@ -180,6 +186,7 @@
"Custom TLS certificates": "Brugerdefinerede TLS certifikater",
"Cut": "Klip",
"Dark": "Mørkt",
"Dashboard": "Kontrolpanel",
"Database v%s": "Database v%s",
"Date": "Dato",
"Date format": "Datoformat",
@@ -439,6 +446,7 @@
"Login with Dropbox": "Log på med Dropbox",
"Login with OneDrive": "Log på med OneDrive",
"Logout": "Log ud",
"Logs": "Logfiler",
"Make a donation": "Giv en donation",
"Manage master password": "Administrer hovedadgangskode",
"Manage master password...": "Administrer hovedadgangskode...",
@@ -580,6 +588,7 @@
"Previous versions of this note": "Tidligere udgaver af denne note",
"Print": "Udskriv",
"Privacy Policy": "Privatlivspolitik",
"Profile": "Profil",
"Profile Version: %s": "Profil-version: %s",
"Properties": "Egenskaber",
"Public-private key pair:": "Offentligt-privat nøglepar:",
@@ -832,6 +841,7 @@
"Unsupported link or message: %s": "Ugyldigt link eller besked: %s",
"Untitled": "Uden titel",
"Update": "Opdater",
"Update profile": "Opdater profil",
"Updated": "Opdateret",
"updated date": "opdateringsdato",
"Updated local items: %d.": "Opdaterede lokale emner: %d.",
@@ -851,6 +861,7 @@
"Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.": "Brug denne funktion til at genopbygge søgeindekset, hvis der er problemer med søgning. Det kan tage lang tid afhængig af antallet af noter.",
"Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.": "Bruges til det meste tekst i markdown-editoren. Hvis ikke fundet, bruges en proportional (variabel bredde) font.",
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": "Bruges hvor en font med fast bredde er nødvendig for at vise læsbar tekst (f.eks. tabeller, afkrydsningsfelter, kode). Hvis ikke fundet, bruges en generisk monospatieret (fast bredde) font.",
"User deletions": "Brugersletninger",
"Users": "Brugere",
"Valid": "Gyldig",
"View": "Vis",

View File

@@ -51,6 +51,8 @@
"Add recipient:": "Füge Empfänger hinzu:",
"Add title": "Titel hinzufügen",
"Add to dictionary": "Zum Wörterbuch hinzufügen",
"Admin": "Admin",
"Admin dashboard": "Admin-Übersichtsseite",
"Advanced options": "Erweiterte Optionen",
"Advanced tools": "Erweiterte Optionen",
"All notes": "Alle Notizen",
@@ -78,6 +80,7 @@
"Authorisation token:": "Berechtigungstoken:",
"Auto": "Automatisch",
"Auto-pair braces, parenthesis, quotations, etc.": "Automatisches Hinzufügen von geschweiften Klammern, runden Klammern, Anführungszeichen usw.",
"Automatically check for updates": "Automatisch auf Aktualisierungen prüfen",
"Automatically switch theme to match system theme": "Automatisch das Design ändern, um es dem System-Design anzupassen",
"Back": "Zurück",
"Bold": "Fett",
@@ -132,6 +135,7 @@
"Completed": "Abgeschlossen",
"Completed decryption.": "Entschlüsselung abgeschlossen.",
"Completed: %s (%s)": "Abgeschlossen: %s (%s)",
"Compress old changes": "Komprimiere alte Änderungen",
"Configuration": "Konfiguration",
"Confirm password cannot be empty": "Bestätigungs-Passwort darf nicht leer sein",
"Confirm password:": "Passwort bestätigen:",
@@ -145,6 +149,7 @@
"Copy": "Kopieren",
"Copy dev mode command to clipboard": "Entwicklermodus-Befehl in Zwischenablage kopieren",
"Copy external link": "Externen Link kopieren",
"Copy image": "Bild kopieren",
"Copy Link Address": "Link-Adresse kopieren",
"Copy Markdown link": "Markdown-Link kopieren",
"Copy path to clipboard": "Pfad in Zwischenablage kopieren",
@@ -159,6 +164,8 @@
"Could not upgrade master key: %s": "Konnte Hauptschlüssel nicht aktualisieren: %s",
"Could not verify the share status of this notebook - aborting. Please try again when you are connected to the internet.": "Der Freigabestatus dieses Notizbuchs konnte nicht überprüft werden - Vorgang wird abgebrochen. Bitte versuche es erneut, wenn eine Internetverbindung besteht.",
"Create a notebook": "Notizbuch erstellen",
"Create notebook": "Notizbuch erstellen",
"Create user": "Benutzer erstellen",
"Created": "Erstellt",
"created date": "Erstellungsdatum",
"Created local items: %d.": "Lokale Elemente erstellt: %d.",
@@ -170,7 +177,7 @@
"Creates a new note.": "Erstellt eine neue Notiz.",
"Creates a new notebook.": "Erstellt ein neues Notizbuch.",
"Creates a new to-do.": "Erstellt eine neue Aufgabe.",
"Creating new %s...": "Neue(s) %s wird erstellt ...",
"Creating new %s...": "Neue(s) %s erstellen ...",
"Creating report...": "Bericht wird erstellt ...",
"Current version is up-to-date.": "Die aktuelle Version ist auf dem neuesten Stand.",
"custom order": "Benutzerdefinierte Reihenfolge",
@@ -180,6 +187,7 @@
"Custom TLS certificates": "Benutzerdefinierte TLS-Zertifikate",
"Cut": "Ausschneiden",
"Dark": "Dunkel",
"Dashboard": "Übersichtsseite",
"Database v%s": "Datenbank v%s",
"Date": "Datum",
"Date format": "Datumsformat",
@@ -191,6 +199,8 @@
"Default: %s": "Standard: %s",
"Delete": "Löschen",
"Delete attachment \"%s\"?": "Anhang „%s“ löschen?",
"Delete expired sessions": "Lösche abgelaufene Sitzungen",
"Delete expired tokens": "Lösche abgelaufene Tokens",
"Delete line": "Zeile löschen",
"Delete local data and re-download from sync target": "Lösche lokale Daten und lade Daten erneut vom Synchronisierungsziel",
"Delete note \"%s\"?": "Notiz „%s“ löschen?",
@@ -255,6 +265,8 @@
"Editor monospace font family": "Nichtproportionale Schriftfamilie im Editor",
"Either \"text\" or \"json\"": "Entweder „text“ oder „json“",
"Emacs": "Emacs",
"Email": "E-Mail",
"Emails": "E-Mails",
"emphasised text": "hervorgehobener Text",
"Enable": "Aktivieren",
"Enable ++insert++ syntax": "Syntax ++insert++ aktivieren",
@@ -439,6 +451,7 @@
"Login with Dropbox": "Mit Dropbox anmelden",
"Login with OneDrive": "Mit OneDrive anmelden",
"Logout": "Abmelden",
"Logs": "Protokolle",
"Make a donation": "Spenden",
"Manage master password": "Master-Passwort verwalten",
"Manage master password...": "Master-Passwort verwalten...",
@@ -580,6 +593,10 @@
"Previous versions of this note": "Vorherige Version dieser Notiz",
"Print": "Drucken",
"Privacy Policy": "Datenschutzrichtlinie",
"Process failed payment subscriptions": "Verarbeite fehlgeschlagene Zahlungsabonnements",
"Process oversized accounts": "Verarbeite zu große Konten",
"Process user deletions": "Verarbeite Benutzerlöschungen",
"Profile": "Profil",
"Profile Version: %s": "Profil-Version: %s",
"Properties": "Eigenschaften",
"Public-private key pair:": "Öffentlich-Privates Schlüsselpaar:",
@@ -621,6 +638,11 @@
"Revision: %s (%s)": "Revision: %s (%s)",
"Runs the commands contained in the text file. There should be one command per line.": "Führe Befehle aus der Textdatei aus. Jede Zeile darf nur einen Befehl enthalten.",
"S3": "S3",
"S3 access key": "S3-Zugriffsschlüssel",
"S3 bucket": "S3 Bucket",
"S3 region": "S3-Region",
"S3 secret key": "S3 geheimer Sschlüssel",
"S3 URL": "S3 URL",
"Safe mode is currently active. Note rendering and all plugins are temporarily disabled.": "Sicherer Modus aktiv. Notizdarstellung und alle Plugins werden vorübergehend deaktiviert.",
"Save": "Speichern",
"Save alarm": "Alarm speichern",
@@ -827,6 +849,8 @@
"Unsupported link or message: %s": "Nicht unterstützter Link oder Nachricht: %s",
"Untitled": "Unbenannt",
"Update": "Aktualisieren",
"Update profile": "Profil aktualisieren",
"Update total sizes": "Aktualisiere Gesamtgrößen",
"Updated": "Aktualisiert",
"updated date": "Aktualisierungsdatum",
"Updated local items: %d.": "Lokale Elemente aktualisiert: %d.",
@@ -846,6 +870,7 @@
"Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.": "Verwende dies, um den Suchindex neu aufzubauen, wenn es ein Problem mit der Suche gibt. Dies kann je nach Anzahl der Notizen eine lange Zeit dauern.",
"Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.": "Hauptsächlich für Text im Markdown-Editor verwendet. Falls nicht vorhanden wird eine generische Proportional-Schriftart (mit variabler Breite) verwendet.",
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": "Verwendete Schriftart mit fester Breite, um Text lesbar zu machen (z.B. in Tabellen, Kontrollkästchen, Code). Falls nicht vorhanden wird eine Monotype-Scrhiftart (mit fester Breite) verwendet.",
"User deletions": "Benutzer-Löschungen",
"Users": "Benutzer",
"Valid": "Gültig",
"View": "Ansicht",

View File

@@ -66,7 +66,7 @@
"Aritim Dark": "Aritim Dark",
"Attach file": "Liitä tiedosto",
"Attach photo": "Liitä valokuva",
"Attach...": "Liitä...",
"Attach...": "Liitä...",
"Attaches the given file to the note.": "Liittää annetun tiedoston muistiinpanoon.",
"attachment": "liite",
"Attachment conflict: \"%s\"": "Liitteen ristiriita: \"%s\"",
@@ -78,6 +78,7 @@
"Authorisation token:": "Valtuutuksen tunnus:",
"Auto": "Automaattinen",
"Auto-pair braces, parenthesis, quotations, etc.": "Yhdistä sulut, sulkeet, lainaukset jne.",
"Automatically check for updates": "Tarkista päivitykset automaattisesti",
"Automatically switch theme to match system theme": "Vaihda teema automaattisesti vastaamaan järjestelmän teemaa",
"Back": "Takaisin",
"Bold": "Lihavoitu",
@@ -142,6 +143,7 @@
"Convert to todo": "Muunna tehtäväksi",
"Copy": "Kopioi",
"Copy dev mode command to clipboard": "Kopioi kehitystila komento leikepöydälle",
"Copy image": "Kopioi kuva",
"Copy Link Address": "Kopioi linkin osoite",
"Copy Markdown link": "Kopioi Merkinnän linkki",
"Copy path to clipboard": "Kopioi polku leikepöydälle",
@@ -153,6 +155,8 @@
"Could not install plugin: %s": "Laajennuksen asentaminen epäonnistui: %s",
"Could not upgrade master key: %s": "Pääavainta ei voitu päivittää: %s",
"Create a notebook": "Luo muistikirja",
"Create notebook": "Luo muistikirja",
"Create user": "Luo käyttäjä",
"Created": "Luotu",
"created date": "luotu päivämäärä",
"Created local items: %d.": "Luodut paikalliset kohteet: %d.",
@@ -248,6 +252,8 @@
"Editor monospace font family": "Editorin monospace fonttiperhe",
"Either \"text\" or \"json\"": "Joko \"text\" tai \"json\"",
"Emacs": "Emacs",
"Email": "Sähköposti",
"Emails": "Sähköpostit",
"emphasised text": "korostettu teksti",
"Enable": "Ota käyttöön",
"Enable ++insert++ syntax": "Ota käyttöön ++insert++ syntax",
@@ -279,6 +285,8 @@
"Encryption": "Salaus",
"Encryption Config": "Salauksen määritys",
"Encryption is: %s": "Salaus on: %s",
"Encryption keys": "Salausavaimet",
"Encryption:": "Salaus:",
"Enter code here": "Syötä koodi tähän",
"Enter master password:": "Syötä pääsalasana:",
"Enter notebook title": "Anna muistikirjan otsikko",
@@ -346,6 +354,7 @@
"Idle": "Käyttämättömänä",
"Ignore": "Ohita",
"Ignore TLS certificate errors": "Ohita TLS varmenteen virheet",
"Images": "Kuvat",
"Import": "Tuo",
"Importing from \"%s\" as \"%s\" format. Please wait...": "Tuodaan kohteesta \"%s\" as \"%s\" muodossa. Odota...",
"Importing notes...": "Muistiinpanojen tuominen...",
@@ -374,6 +383,7 @@
"Invalid answer: %s": "Virheellinen vastaus: %s",
"Invalid command: \"%s\"": "Virheellinen komento: \"%s\"",
"Invalid option value: \"%s\". Possible values are: %s.": "Virheellinen asetusarvo: \"%s\". Mahdolliset arvot ovat: %s.",
"Invalid password": "Virheellinen salasana",
"Italic": "Kursiivi",
"Item \"%s\" could not be downloaded: %s": "Kohdetta \"%s\" ei voitu ladata: %s",
"Items that cannot be decrypted": "Kohteet, joita ei voi purkaa",
@@ -419,6 +429,8 @@
"Login with Dropbox": "Kirjaudu sisään Dropbox",
"Login with OneDrive": "Kirjaudu sisään OneDrive",
"Make a donation": "Tee lahjoitus",
"Manage master password": "Pääsalasanan hallinta",
"Manage master password...": "Pääsalasanan hallinta...",
"Manage your plugins": "Hallitse laajennuksia",
"Manual": "Manuaalinen",
"Markdown": "Markdown",
@@ -426,6 +438,8 @@
"Marks a to-do as non-completed.": "Merkitsee tehtävän keskeneräiseksi.",
"Markup": "Merkintä",
"Master Key %s": "Pääavain %s",
"Master password": "Pääsalasana",
"Master password:": "Pääsalasana:",
"Max concurrent connections": "Samanaikaiset yhteydet enintään",
"Missing Master Keys": "Puuttuvat pääavaimet",
"Missing required argument: %s": "Vaadittu argumentti puuttuu: %s",
@@ -475,7 +489,7 @@
"Note body": "Muistiinpanon kappale",
"Note does not exist: \"%s\". Create it?": "Huomautusta ei ole: \"%s\". Luodaanko se?",
"Note has been saved.": "Huomautus on tallennettu.",
"Note History": "Muistiinpano historia",
"Note History": "Muistiinpanohistoria",
"Note is not a to-do: \"%s\"": "Huomautus ei ole tehtävä: \"%s\"",
"Note list": "Muistiinpanot",
"Note list growth factor": "Huomautus luettelon kasvutekijä",
@@ -484,10 +498,12 @@
"Note&book": "&Muistikirjat",
"Note: Does not work in all desktop environments.": "Huomautus: Ei toimi kaikissa työpöytäympäristöissä.",
"Note: When a note is shared, it will no longer be encrypted on the server.": "Huomautus: Kun muistiinpano on jaettu, sitä ei enää salata palvelimella.",
"Notebook": "Muistikirja",
"Notebook list growth factor": "Muistikirjaluettelon kasvutekijä",
"Notebook: %s": "Muistikirja: %s",
"Notebooks": "Muistikirjat",
"Notebooks cannot be named \"%s\", which is a reserved title.": "Muistikirjoja ei voi nimetä \"%s\", joka on varattu otsikko.",
"Notes": "Muistiinpanot",
"Notes and settings are stored in: %s": "Muistiinpanot ja oletusasetukset tallennetaan: %s",
"Notes can only be created within a notebook.": "Muistiinpanoja voi luoda vain muistikirjaan.",
"Numbered List": "Numeroitu luettelo",
@@ -601,6 +617,7 @@
"See the pre-release page for more details: %s": "Lisätietoja on ennakkojulkaisusivulla: %s",
"Select": "Valitse",
"Select all": "Valitse kaikki",
"Select file...": "Valitse tiedosto...",
"Server is already running on port %d": "Palvelin on jo käynnissä portissa %d",
"Server is not running.": "Palvelin ei ole käynnissä.",
"Server is running on port %d": "Palvelin on käynnissä portissa %d",
@@ -797,6 +814,7 @@
"Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.": "Tämän avulla voit muodostaa hakuindeksin uudelleen, jos haussa on ongelma. Se voi kestää kauan muistiinpanojen määrästä riippuen.",
"Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.": "Käytetään useimmissa teksteissä markdown editorissa. Jos sitä ei löydy, käytetään yleistä suhteellista fonttia (vaihtelevaa leveyttä).",
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": "Käytetään, kun tekstiin tarvitaan kiinteäleveyksinen fontti (esim. taulukot, valintaruudut, koodi). Jos sitä ei löydy, käytetään yleistä monospace fonttia (kiinteäleveyksinen).",
"Users": "Käyttäjät",
"Valid": "Kelvollinen",
"View": "Näytä",
"View on map": "Näytä kartalla",

View File

@@ -79,7 +79,9 @@
"Authentication was not completed (did not receive an authentication token).": "Impossible d'autoriser le logiciel (jeton d'identification non‑reçu).",
"Authorisation token:": "Code d'authentification :",
"Auto": "Auto",
"Auto-add disabled accounts for deletion": "Supprimer automatiquement les comptes désactivés",
"Auto-pair braces, parenthesis, quotations, etc.": "Auto‑compléter les paires de parenthèses, guillemets, etc.",
"Automatically check for updates": "Vérifier automatiquement les mises à jour",
"Automatically switch theme to match system theme": "Changer le thème automatiquement pour correspondre au thème système",
"Back": "Retour",
"Bold": "Gras",
@@ -134,6 +136,7 @@
"Completed": "Terminé",
"Completed decryption.": "Déchiffrement complété.",
"Completed: %s (%s)": "Terminé : %s (%s)",
"Compress old changes": "Compresser les vieux changements",
"Configuration": "Configuration",
"Confirm password cannot be empty": "Le mot de passe de confirmation ne peut être vide",
"Confirm password:": "Confirmer le mot de passe :",
@@ -147,6 +150,7 @@
"Copy": "Copier",
"Copy dev mode command to clipboard": "Copier commande de développement dans presse papier",
"Copy external link": "Copier le lien externe",
"Copy image": "Copier image",
"Copy Link Address": "Copier l'adresse du lien",
"Copy Markdown link": "Copier lien Markdown",
"Copy path to clipboard": "Copier le chemin",
@@ -161,6 +165,8 @@
"Could not upgrade master key: %s": "Impossible de mettre la clef à niveau : %s",
"Could not verify the share status of this notebook - aborting. Please try again when you are connected to the internet.": "Impossible de vérifier l'état du partage du carnet - annulation. Veuillez réessayer lorsque vous serez connecté à internet.",
"Create a notebook": "Créer un carnet",
"Create notebook": "Créer un carnet",
"Create user": "Créer utilisateur",
"Created": "Créé",
"created date": "date de création",
"Created local items: %d.": "Objets créés localement : %d.",
@@ -182,6 +188,7 @@
"Custom TLS certificates": "Certificats TLS personnalisés",
"Cut": "Couper",
"Dark": "Sombre",
"Dashboard": "Tableau de bord",
"Database v%s": "Base de données v%s",
"Date": "Date",
"Date format": "Format de la date",
@@ -193,6 +200,8 @@
"Default: %s": "Défaut : %s",
"Delete": "Supprimer",
"Delete attachment \"%s\"?": "Supprimer fichier joint \"%s\" ?",
"Delete expired sessions": "Supprimer les sessions expirées",
"Delete expired tokens": "Supprimer les tokens expirés",
"Delete line": "Supprimer la ligne",
"Delete local data and re-download from sync target": "Supprimer les données locales et re‑télécharger depuis la cible de synchronisation",
"Delete note \"%s\"?": "Supprimer note \"%s\" ?",
@@ -257,6 +266,8 @@
"Editor monospace font family": "Police monospace de l'éditeur",
"Either \"text\" or \"json\"": "Soit \"text\" soit \"json\"",
"Emacs": "Emacs",
"Email": "Email",
"Emails": "Emails",
"emphasised text": "texte en italique",
"Enable": "Activer",
"Enable ++insert++ syntax": "Activer la syntaxe ++insertion++",
@@ -363,6 +374,7 @@
"Idle": "Arrêté",
"Ignore": "Ignorer",
"Ignore TLS certificate errors": "Ignorer les erreurs de certificats TLS",
"Images": "Images",
"Import": "Importer",
"Importing from \"%s\" as \"%s\" format. Please wait...": "Importer depuis \"%s\" au format \"%s\". Veuillez patienter…",
"Importing notes...": "Importation des notes…",
@@ -441,6 +453,7 @@
"Login with Dropbox": "Se connecter à Dropbox",
"Login with OneDrive": "Se connecter à OneDrive",
"Logout": "Se déconnecter",
"Logs": "Journal",
"Make a donation": "Faire un don",
"Manage master password": "Gestion du mot de passe maître",
"Manage master password...": "Gestion du mot de passe maître...",
@@ -541,6 +554,7 @@
"Operation cancelled": "Opération annulée",
"Options": "Options",
"Or create an account.": "Ou créer un compte.",
"Other applications...": "Autres applications...",
"Output format: %s": "Format de la sortie : %s",
"Page orientation for PDF export": "Orientation de page pour l'export PDF",
"Page size for PDF export": "Taille de page pour l'export PDF",
@@ -582,6 +596,10 @@
"Previous versions of this note": "Versions précédentes de cette note",
"Print": "Imprimer",
"Privacy Policy": "Politique de confidentialité",
"Process failed payment subscriptions": "Traiter les inscriptions avec échec de paiement",
"Process oversized accounts": "Traiter les comptes ayant excédé leur limite",
"Process user deletions": "Traiter la suppression d'utilisateurs",
"Profile": "Profil",
"Profile Version: %s": "Version du profil : %s",
"Properties": "Propriétés",
"Public-private key pair:": "Paire de clefs publiques privées :",
@@ -644,6 +662,8 @@
"See the pre-release page for more details: %s": "Voir la page des pré‑release pour plus de détails : %s",
"Select": "Sélectionner",
"Select all": "Sélectionner tout",
"Select emoji...": "Sélectionner émoji...",
"Select file...": "Sélectionner fichier...",
"Server is already running on port %d": "Le serveur tourne déjà sur le port %d",
"Server is not running.": "Le serveur est arrêté.",
"Server is running on port %d": "Le serveur tourne sur le port %d",
@@ -834,6 +854,8 @@
"Unsupported link or message: %s": "Lien ou message non géré : %s",
"Untitled": "Sans titre",
"Update": "Mettre à jour",
"Update profile": "Mettre à jour le profil",
"Update total sizes": "Mettre à jour les tailles totales",
"Updated": "Mis à jour",
"updated date": "date de modification",
"Updated local items: %d.": "Objets màj localement : %d.",
@@ -853,6 +875,7 @@
"Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.": "Utilisez ceci pour corriger l'index du moteur de recherche en cas de problème. Cela peut prendre longtemps selon le nombre de notes.",
"Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.": "Utilisée pour la plupart du texte de l'éditeur Markdown. Par défaut, une police proportionnelle sera utilisée.",
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": "Utilisée lorsque une police à taille fixe est nécessaire pour afficher le texte de façon lisible (par ex. pour les tables, code source, etc.).",
"User deletions": "Suppressions d'utilisateurs",
"Users": "Utilisateurs",
"Valid": "Valide",
"View": "Affichage",

View File

@@ -41,45 +41,45 @@ locales['uk_UA'] = require('./uk_UA.json');
locales['vi'] = require('./vi.json');
locales['zh_CN'] = require('./zh_CN.json');
locales['zh_TW'] = require('./zh_TW.json');
stats['ar'] = {"percentDone":92};
stats['ar'] = {"percentDone":90};
stats['eu'] = {"percentDone":26};
stats['bs_BA'] = {"percentDone":66};
stats['bg_BG'] = {"percentDone":52};
stats['ca'] = {"percentDone":98};
stats['hr_HR'] = {"percentDone":96};
stats['cs_CZ'] = {"percentDone":88};
stats['bs_BA'] = {"percentDone":65};
stats['bg_BG'] = {"percentDone":51};
stats['ca'] = {"percentDone":97};
stats['hr_HR'] = {"percentDone":94};
stats['cs_CZ'] = {"percentDone":87};
stats['da_DK'] = {"percentDone":98};
stats['de_DE'] = {"percentDone":98};
stats['et_EE'] = {"percentDone":51};
stats['de_DE'] = {"percentDone":99};
stats['et_EE'] = {"percentDone":50};
stats['en_GB'] = {"percentDone":100};
stats['en_US'] = {"percentDone":100};
stats['es_ES'] = {"percentDone":98};
stats['es_ES'] = {"percentDone":96};
stats['eo'] = {"percentDone":29};
stats['fi_FI'] = {"percentDone":92};
stats['fr_FR'] = {"percentDone":98};
stats['fr_FR'] = {"percentDone":100};
stats['gl_ES'] = {"percentDone":33};
stats['id_ID'] = {"percentDone":91};
stats['it_IT'] = {"percentDone":89};
stats['hu_HU'] = {"percentDone":77};
stats['nl_BE'] = {"percentDone":90};
stats['nl_NL'] = {"percentDone":84};
stats['nb_NO'] = {"percentDone":89};
stats['fa'] = {"percentDone":63};
stats['pl_PL'] = {"percentDone":83};
stats['pt_BR'] = {"percentDone":92};
stats['pt_PT'] = {"percentDone":83};
stats['ro'] = {"percentDone":58};
stats['sl_SI'] = {"percentDone":92};
stats['sv'] = {"percentDone":98};
stats['th_TH'] = {"percentDone":42};
stats['vi'] = {"percentDone":89};
stats['tr_TR'] = {"percentDone":92};
stats['uk_UA'] = {"percentDone":82};
stats['el_GR'] = {"percentDone":86};
stats['ru_RU'] = {"percentDone":92};
stats['sr_RS'] = {"percentDone":75};
stats['zh_CN'] = {"percentDone":96};
stats['zh_TW'] = {"percentDone":89};
stats['ja_JP'] = {"percentDone":97};
stats['ko'] = {"percentDone":88};
stats['id_ID'] = {"percentDone":90};
stats['it_IT'] = {"percentDone":87};
stats['hu_HU'] = {"percentDone":76};
stats['nl_BE'] = {"percentDone":89};
stats['nl_NL'] = {"percentDone":83};
stats['nb_NO'] = {"percentDone":88};
stats['fa'] = {"percentDone":62};
stats['pl_PL'] = {"percentDone":82};
stats['pt_BR'] = {"percentDone":91};
stats['pt_PT'] = {"percentDone":82};
stats['ro'] = {"percentDone":57};
stats['sl_SI'] = {"percentDone":91};
stats['sv'] = {"percentDone":96};
stats['th_TH'] = {"percentDone":41};
stats['vi'] = {"percentDone":88};
stats['tr_TR'] = {"percentDone":98};
stats['uk_UA'] = {"percentDone":81};
stats['el_GR'] = {"percentDone":84};
stats['ru_RU'] = {"percentDone":91};
stats['sr_RS'] = {"percentDone":73};
stats['zh_CN'] = {"percentDone":99};
stats['zh_TW'] = {"percentDone":88};
stats['ja_JP'] = {"percentDone":96};
stats['ko'] = {"percentDone":87};
module.exports = { locales: locales, stats: stats };

View File

@@ -337,7 +337,7 @@
"Insert Hyperlink": "Wstaw Hyperlink",
"Install": "Zainstaluj",
"Install from file": "Zainstaluj z pliku",
"Installed": "Zainstwalony",
"Installed": "Zainstalowany",
"Installing...": "Instalowanie.",
"Invalid": "Nieprawidłowy",
"Invalid %s: %s.": "Nieprawidłowy %s: %s.",

View File

@@ -51,6 +51,8 @@
"Add recipient:": "Alıcı ekle:",
"Add title": "Başlık ekle",
"Add to dictionary": "Sözlüğe ekle",
"Admin": "Yönetici",
"Admin dashboard": "Yönetici paneli",
"Advanced options": "Gelişmiş seçenekler",
"Advanced tools": "Gelişmiş araçlar",
"All notes": "Tüm notlar",
@@ -78,6 +80,7 @@
"Authorisation token:": "Yetkilendirme anahtarı:",
"Auto": "Otomatik",
"Auto-pair braces, parenthesis, quotations, etc.": "Parantez, çift tırnak gibi değerlerin çiftlerini otomatik ekle.",
"Automatically check for updates": "Otomatik olarak güncellemeleri denetle",
"Automatically switch theme to match system theme": "Temayı system temasına göre otomatik olarak değiştir",
"Back": "Geri",
"Bold": "Kalın",
@@ -98,7 +101,9 @@
"Cannot move note to \"%s\" notebook": "Not, \"%s\" not defterine taşınamadı",
"Cannot move notebook to this location": "Not defteri bu konuma taşınamıyor",
"Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.": "Anahtar yenilenemiyor: kimlik doğrulama verileri eksik. Senkronizasyonu tekrar başlatmak sorunu çözebilir.",
"Cannot save %s \"%s\" because it is larger than the allowed limit (%s)": "%s \"%s\" kaydedilemedi, çünkü izin verilen limitten (%s) daha büyük",
"Cannot save %s \"%s\" because it would go over the total allowed size (%s) for this account": "%s \"%s\" kaydedilemedi, çünkü bu hesap için izin verilen limitten (%s) daha büyük",
"Cannot share encrypted notebook with recipient %s because they have not enabled end-to-end encryption. They may do so from the screen Configuration > Encryption.": "Şifrelenmiş not defteri %s ile paylaşılamadı, çünkü alıcı uçtan uca şifreleme aktifleştirmemiş. Ayarlar > Şifreleme kısmından bu ayarı açabilir.",
"Change application layout": "Ugulama görünümünü değiştir",
"Change language": "Dil değiştir",
"Characters": "Karakter",
@@ -137,10 +142,13 @@
"Conflicted: %d": "Çakışan: %d",
"Conflicts": "Çakışmalar",
"Conflicts (attachments)": "Çakışmalar (ek dosyalar)",
"Content provided by %s": "İçerik %s tarafından sağlandı",
"Convert to note": "Nota çevir",
"Convert to todo": "Yapılacak listesi olarak çevir",
"Copy": "Kopyala",
"Copy dev mode command to clipboard": "Geliştirici modu komutunu panoya kopyala",
"Copy external link": "Harici bağlantıyı kopyala",
"Copy image": "Resmi kopyala",
"Copy Link Address": "Bağlantı Adresini Kopyala",
"Copy Markdown link": "Markdown bağlantısını kopyala",
"Copy path to clipboard": "Yolu panoya kopyala",
@@ -148,10 +156,15 @@
"Copy token": "Anahtarı kopyala",
"Could not authorise application:\n\n%s\n\nPlease try again.": "Uygulama yetkilendirilemedi:\n\n%s\n\nLütfen tekrar deneyin.",
"Could not connect to Joplin Server. Please check the Synchronisation options in the config screen. Full error was:\n\n%s": "Joplin Sunucusuna bağlanılamadı. Lütfen senkronizasyon yapılandırması ekranını kontrol edin. Hatanın tamamı:\n\n%s",
"Could not connect to plugin repository.": "Eklenti sunucusuna bağlanılamadı.",
"Could not export notes: %s": "Notlar dışarı aktarılamadı: %s",
"Could not install plugin: %s": "Eklenti yüklenemedi: %s",
"Could not respond to the invitation. Please try again, or check with the notebook owner if they are still sharing it.\n\nThe error was: \"%s\"": "Davetiyeye cevap verilemedi. Lütfen yeniden deneyin, ya da eğer hala paylaşımdaysa not defteri sahibiyle bu durumu paylaşın.\n\nHata mesajı: \"%s\"",
"Could not upgrade master key: %s": "Ana şifreleme anahtarı güncellenemedi: %s",
"Could not verify the share status of this notebook - aborting. Please try again when you are connected to the internet.": "Bu not defterinin paylaşım durumu doğrulanamadı, bu sebeple işlem iptal ediliyor. Lütfen internet bağlantınız varken bu işlemi yeniden deneyin.",
"Create a notebook": "Yeni bir not defteri oluşturur",
"Create notebook": "Not defteri oluştur",
"Create user": "Kullanıcı oluştur",
"Created": "Oluşturuldu",
"created date": "oluşturma zamanı",
"Created local items: %d.": "Oluşturulan yerel öğeler: %d.",
@@ -173,6 +186,7 @@
"Custom TLS certificates": "Özel TLS sertifikaları",
"Cut": "Kes",
"Dark": "Karanlık",
"Dashboard": "Kontrol Paneli",
"Database v%s": "Veritabanı v%s",
"Date": "Tarih",
"Date format": "Tarih biçimi",
@@ -221,6 +235,7 @@
"Displays version information": "Sürüm bilgisini görüntüler",
"Do it now": "Hemen yap",
"Do not ask for confirmation.": "Onay için sorma.",
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": "Şifrenizi kaybetmeyin, verilerinizin şifresini çözmenin \"*tek yolu* bu şifreyi girmek olacaktır. Şifrelemeyi etkinleştirmek için lütfen parolanızı aşağıya girin.",
"Download": "İndirme",
"Download and install the relevant extension for your browser:": "Tarayıcınız için uygun uzantıyı indirin ve yükleyin:",
"Downloaded": "İndirildi",
@@ -272,12 +287,16 @@
"Enable Web Clipper Service": "Web Alıntılama Servisini Aç",
"Enable ~sub~ syntax": "~sub~ söz dizimini etkinleştir",
"Enabled": "Etkin",
"Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target.": "Şifrelemeyi devre devreye alırsanız *tüm* notlarınızın ve eklerinizin yeniden senkronize edileceği ve senkronizasyon hedefine şifrelenmemiş olarak gönderileceği anlamına gelir.",
"Encrypted": "Şifrelenmiş",
"Encrypted items cannot be modified": "Şifrelenmiş öğeler değiştirilemez",
"Encrypted notebooks cannot be renamed": "Şifrelenmiş not defterleri yeniden adlandırılamaz",
"Encryption": "Şifreleme",
"Encryption Config": "Yapılandırmayı şifrele",
"Encryption is: %s": "Şifreleme: %s",
"Encryption keys": "Şifreleme anahtarları",
"Encryption:": "Şifreleme:",
"End-to-end encryption": "Uçtan uca şifreleme",
"Enter code here": "Kodu buraya girin",
"Enter master password:": "Ana parolayı girin:",
"Enter notebook title": "Not defteri başlığını girin",
@@ -319,11 +338,13 @@
"For information on how to customise the shortcuts please visit %s": "Kısayolları nasıl özelleştirebileceğiniz hakkında bilgi için lütfen %s adresini ziyaret edin",
"For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:": "Uçtan Uca Şifreleme (E2EE) hakkında bilgi ve nasıl aktif edilebileceğine dair ipuçları için lütfen belgeleri inceleyin:",
"For the list of keyboard shortcuts and config options, type `help keymap`": "Klavye kısayolları ve yapılandırma seçenekleri listesi için `help keymap` yazın",
"Force path style": "Zorlanan yol biçimi",
"Forward": "İleri",
"Found: %d.": "Bulundu: %d.",
"FTS enabled: %d": "FTS aktif edildi: %d",
"Full changelog": "Tüm sürüm geçmişi",
"General": "Genel",
"Generated": "Oluşturma",
"Generating link...": "Bağlantı oluşturuluyor...",
"Get it now:": "Hemen al:",
"Get pre-releases when checking for updates": "Güncellemeleri kontrol ederken ön sürümleri alın",
@@ -334,8 +355,10 @@
"Heading": "Başlık",
"Help": "Yardım",
"Hide %s": "Şunu Gizle: %s",
"Hide disabled keys": "İnaktif anahtarları gizle",
"Hide Joplin": "Joplin'i Gizle",
"Highlight": "Vurgula",
"Home": "Ana sayfa",
"Horizontal Rule": "Yatay kural",
"HTML Directory": "HTML Dizini",
"HTML File": "HTML Dosyası",
@@ -373,8 +396,10 @@
"Invalid answer: %s": "Yanlış cevap: %s",
"Invalid command: \"%s\"": "Geçersiz komut: \"%s\"",
"Invalid option value: \"%s\". Possible values are: %s.": "Geçersiz seçenek değeri: \"%s\". Mümkün değerler: %s.",
"Invalid password": "Hatalı parola",
"Italic": "İtalik",
"Item \"%s\" could not be downloaded: %s": "\"%s\" öğesi indirilemedi: %s",
"Items": "Öğeler",
"Items that cannot be decrypted": "Şifresi çözülemeyen öğeler",
"Items that cannot be synchronised": "Senkronize edilemeyen öğeler",
"Joplin can synchronise your notes using various providers. Select one from the list below.": "Joplin notlarınızı pek çok sağlayıcıyı kullanarak senkron edebilir. Lütfen aşağıdan bunlardan birini seçin.",
@@ -397,12 +422,14 @@
"Keyboard Shortcut": "Klavye Kısayolu",
"Keyboard Shortcuts": "Klavye Kısayolları",
"Keychain Supported: %s": "Keychain Desteği: %s",
"Keys that need upgrading": "Güncellenmesi gereken anahtarlar",
"Landscape": "Yatay",
"Language": "Dil",
"Last error: %s": "Son hata: %s",
"Later": "Daha sonra",
"Layout": "Düzen",
"Layout button sequence": "Düzen butonu sıralaması",
"Leave notebook...": "Not defterinden ayrıl",
"Legal": "Legal",
"Letter": "Kağıt",
"Light": "Aydınlık",
@@ -410,6 +437,7 @@
"Link has been copied to clipboard!": "Bağlantı panoya kopyalandı!",
"Links with protocol \"%s\" are not supported": "“%s” formatlı linkler desteklenmiyor",
"List item": "Öğeyi listele",
"Loaded": "Yüklenme",
"Location": "Konum",
"Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.": "Kilit dosyası zaten tutuluyor. Senkronizasyon yapılmadığını biliyorsanız, kilit dosyasını \"%s\" konumunda silebilir ve işleme devam edebilirsiniz.",
"Log": "Log",
@@ -417,15 +445,24 @@
"Login below.": "Aşağıdan giriş yapabilirsiniz.",
"Login with Dropbox": "Dropbox ile giriş yap",
"Login with OneDrive": "OneDrive ile giriş yapın",
"Logout": "Çıkış Yap",
"Logs": "Kayıtlar",
"Make a donation": "Bağış yapın",
"Manage master password": "Ana parolayı yönet",
"Manage master password...": "Ana parolayı yönet...",
"Manage your plugins": "Eklentileri yönet",
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.": "Uçtan Uca Şifreleme ayarını yönetir. Komutlar: `enable`, `disable`, `decrypt`, `status`, `decrypt-file` ve `target-status`.",
"Manual": "Manuel",
"Markdown": "Markdown",
"Markdown + Front Matter": "Markdown + Front Matter",
"Marks a to-do as done.": "Yapılacakları yapıldı olarak işaretler.",
"Marks a to-do as non-completed.": "Yapılacaklar listesindeki öğeyi tamamlanmamış olarak işaretler.",
"Markup": "Düzenleme",
"Master Key %s": "Ana Anahtar %s",
"Master password": "Ana parola",
"Master password:": "Ana parola:",
"Max concurrent connections": "Maksimum aynı anda bağlantı",
"Missing keys": "Eksik anahtarar",
"Missing Master Keys": "Eksik Ana Anahtar",
"Missing required argument: %s": "Gerekli parametre eksik gözüküyor: %s",
"Mobile data - auto-sync disabled": "Mobil veri - otomatik senkronizasyon devre dışı",
@@ -466,6 +503,7 @@
"Nord": "Nord",
"Not authentified with %s. Please provide any missing credentials.": "%s ile doğrulama sağlanamadı. Lütfen eksik olan tüm bilgileri girin.",
"Not downloaded": "İndirilmeyen",
"Not generated": "Oluşturulmamış",
"note": "not",
"Note": "Not",
"Note area growth factor": "Not alanı büyüme faktörü",
@@ -483,10 +521,12 @@
"Note&book": "Not&defteri",
"Note: Does not work in all desktop environments.": "Not: Tüm masaüstü ortamlarında çalışmaz.",
"Note: When a note is shared, it will no longer be encrypted on the server.": "Not: Bir not paylaşıldığında artık sunucuda şifreli olmayacak.",
"Notebook": "Not defteri",
"Notebook list growth factor": "Not defteri listesi büyüme faktörü",
"Notebook: %s": "Not defteri: %s",
"Notebooks": "Not defterleri",
"Notebooks cannot be named \"%s\", which is a reserved title.": "Not defterine ayrılmış bir başlık adı olan \"%s\" adı verilemez.",
"Notes": "Notlar",
"Notes and settings are stored in: %s": "Notlar ve ayarlar şu konumda saklanır: %s",
"Notes can only be created within a notebook.": "Notlar yalnızca bir not defterinde oluşturulabilir.",
"Numbered List": "Numaralı liste",
@@ -519,8 +559,10 @@
"PDF File": "PDF Dosyası",
"Permission needed": "İzin gerekmekte",
"Permission to use camera": "Kamera kullanımı için izin",
"Please click on \"%s\" to proceed": "Lütfen devam etmek için \"%s\" tuşuna basın",
"Please confirm that you would like to re-encrypt your complete database.": "Lütfen tüm veritabanınızı yeniden şifrelemek istediğinizi doğrulayın.",
"Please enter your password in the master key list below before upgrading the key.": "Anahtarı güncellemek için lütfen ana şifreleme anahtarının parolasını girin.",
"Please note that if it is a large notebook, it may take a few minutes for all the notes to show up on the recipient's device.": "Not defterleri büyük olabilir, ve de bu sebeple alıcı tarafın cihazında tüm notların gözükmesi birkaç dakikayı bulabilir. Bunu lütfen dikkate alın.",
"Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.": "Lütfen uygulamayı doğrulamak için tarayıcınızda aşağıdaki URL'yi açın. Uygulama \"Uygulamalar/Joplin\" de bir dizin oluşturacak ve sadece bu dizindeki dosyaları okuyacak ve yazacaktır. Bu dizin dışındaki hiçbir dosyaya veya diğer kişisel verilere erişemeyecektir. Hiçbir veri üçüncü şahıslarla paylaşılmayacaktır.",
"Please select a notebook first.": "Lütfen önce bir not defteri seçin.",
"Please select the note or notebook to be deleted first.": "Lütfen önce silinecek notu veya not defterini seçin.",
@@ -546,8 +588,10 @@
"Previous versions of this note": "Bu notun önceki sürümleri",
"Print": "Yazdır",
"Privacy Policy": "Gizlilik Politikası",
"Profile": "Profil",
"Profile Version: %s": "Profil sürümü: %s",
"Properties": "Özellikler",
"Public-private key pair:": "Açık - gizli anahtar çifti:",
"Publish note...": "Notu yayımla…",
"Publish Notes": "Notları Yayımla",
"Publish notes to the internet": "Notları İnternet’e yayımla",
@@ -572,6 +616,7 @@
"Rename tag:": "Etiketi yeniden adlandır:",
"Renames the given <item> (note or notebook) to <name>.": "<item> öğesini <name> ile yeniden adlandırır (not veya not defteri).",
"Renew token": "Jetonu yenile",
"Reset master password": "Ana parolayı sıfırla",
"Resources: %d.": "Kaynaklar: %d.",
"Restart and upgrade": "Yeniden başlat ve güncelle",
"Restart now": "Şimdi yeniden başlat",
@@ -584,6 +629,12 @@
"Reverses the sorting order.": "Sıralama düzenini tersine çevirir.",
"Revision: %s (%s)": "Sürüm: %s (%s)",
"Runs the commands contained in the text file. There should be one command per line.": "Metin dosyasındaki komutları çalıştırır. Her bir komut ayrı satırda olmalıdır.",
"S3": "S3",
"S3 access key": "S3 erişim anahtarı",
"S3 bucket": "S3 deposu",
"S3 region": "S3 bölgesi",
"S3 secret key": "S3 gizli anahtarı",
"S3 URL": "S3 URL",
"Safe mode is currently active. Note rendering and all plugins are temporarily disabled.": "Güvenli mod şu an aktif. Not görselleştirme ve tüm ekelntiler geçici olarak devre dışı.",
"Save": "Kaydet",
"Save alarm": "Alarm kurun",
@@ -605,6 +656,7 @@
"Server is running on port %d": "Sunucu %d portunda çalışıyor",
"Set alarm": "Alarm kur",
"Set alarm:": "Alarm kur:",
"Set it to 0 to make it take the complete available space. Recommended width is 600.": "0 olarak belirtmeniz durumunda tüm uygun boş alanı kullanacaktır. Önerilen genişlik değeri 600.",
"Set the password": "Parola belirle",
"Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s": "<name> özelliğini verilen <note> içinde verilen [value] ile değiştirir. Olası özellikler şunlardır:\n\n%s",
"Share": "Paylaş",
@@ -615,7 +667,9 @@
"Show Advanced Settings": "Gelişmiş Seçenekleri Göster",
"Show all": "Tümünü göster",
"Show completed to-dos": "Tamamlanan yapılacaklar listesini göster",
"Show disabled keys": "İnaktif anahtarları görüntüle",
"Show note counts": "Not sayacını göster",
"Show sort order buttons": "Not sıralama butonlarını göster",
"Show tray icon": "Tepsi simgesini göster",
"Sidebar": "Kenar çubuğu",
"Size": "Boyut",
@@ -686,6 +740,7 @@
"Tagged: %d.": "Etiket: %d.",
"Tags": "Etiketler",
"Take photo": "Fotoğraf çek",
"Tasks": "Görevler",
"Text editor command": "Metin editörü komutu",
"Thank you! Your Joplin Cloud account is now setup and ready to use.": "Teşekkürler! Joplin Cloud hesabınız başarıyla oluşuruldu ve artık kullanıma hazır.",
"The app is now going to close. Please relaunch it to complete the process.": "Uygulama şimdi kapanacak. İşlemi tamamlamak için lütfen uygulamayı kapandıktan sonar yeniden çalıştırın.",
@@ -701,14 +756,19 @@
"The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.": "Editör komutu (argüman içerebilir) not açmak için kullanılacaktır. Eğer sağlanmadıysa, varsayılan düzenleyiciyi otomatik olarak algılamaya çalışır.",
"The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes.": "Faktör özelliği, dosyanın diğer dosyaların da ebatına sadık kalarak yeterli alan olduğu sürece hangi değerle büyüyüp ufalacağını tanımlar. Faktör değeri 2 olan bir bileşen, faktör değeri 1 olana nazaran 2 kat fazla alan kaplar. Değişiklikleri görmek için uygulamayı yeniden başlatın.",
"The following attachments are being watched for changes:": "Şu ek dosyaları değişiklikler için izlenmekte:",
"The following keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded key will still be able to decrypt and encrypt your data as usual.": "Mevcut anahtarlarınız eski bir şifreleme algoritmasını kullanıyor, bu sebeple güncellenmesi gerekir. Güncellenen şifreleme ahatarları eskisi gibi verinizi şifreleme ve şifre çözmede kullanılacaktır.",
"The Joplin mobile app does not currently support this type of link: %s": "Joplin mobil uygulaması şu anda bu tür bir bağlantıyı desteklemiyor: %s",
"The Joplin team has vetted this plugin and it meets our standards for security and performance.": "Joplin ekibi bu eklentinin güvenlik ve performans konusunda standartlarını sağladığını beyan eder.",
"The keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.": "Bu kimlikleri olan anahtarlar öğelerinizden bazılarını şifrelemek için kullanılır, ancak uygulama şu anda bunlara erişemez. Büyük olasılıkla senkronizasyon yoluyla indirilmeleri sağlanacaktır.",
"The master key has been upgraded successfully!": "Ana anahtar başarıyla güncellendi!",
"The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.": "Bu kimlikleri olan ana anahtarlar öğelerinizden bazılarını şifrelemek için kullanılır, ancak uygulama şu anda bunlara erişemez. Büyük olasılıkla senkronizasyon yoluyla indirilmeleri sağlanacaktır.",
"The note \"%s\" has been successfully restored to the notebook \"%s\".": "“%s” notu başarılı bir şekilde “%s” not defterine geri yüklendi.",
"The notebook could not be saved: %s": "Not defteri kaydedilemedi: %s",
"The notes have been imported: %s": "Notlar içe aktarıldı: %s",
"The possible commands are:": "Kullanılabilir komutlar:",
"The recipient could not be removed from the list. Please try again.\n\nThe error was: \"%s\"": "Alıcı listeden çıkarılamadı. Lütfen yeniden deneyin.\n\nHata mesajı: \"%s\"",
"The sync target needs to be upgraded before Joplin can sync. The operation may take a few minutes to complete and the app needs to be restarted. To proceed please click on the link.": "Senkronizasyon hedefi'nin Joplin senkronizasyona yeniden başlamadan once güncellenmesi gerekir. Bu işlem notlarınızıın yoğunluğuna göre birkaç dakika sürebilir, ve de bu işlem ardından uygulama yeniden başlatılacaktır. Bu işlemi başlatmak için lütfen linke tıklayın.",
"The sync target needs to be upgraded. Press this banner to proceed.": "Senkronizasyon hedefinin güncellenmesi gerekmekte. Devam etmek için bu butona basın",
"The tag \"%s\" already exists. Please choose a different name.": "\"%s\" etiketi mevcut. Lütfen başka isim seçin.",
"The target to synchronise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).": "Senkronize edilecek hedef. Her senkronizasyon hedefi, `sync.NUM.NAME` olarak adlandırılan ek parametrelere sahip olabilir (tümü aşağıda belgelenmiştir).",
"The Web Clipper needs your authorisation to access your data.": "Web Alıntılayıcısı’nın verinize erişebilmesi için izninize ihtiyacı var.",
@@ -735,10 +795,12 @@
"This service allows the browser extension to communicate with Joplin. When enabling it your firewall may ask you to give permission to Joplin to listen to a particular port.": "Bu hizmet, tarayıcı uzantısının Joplin ile iletişim kurmasını sağlar. Etkinleştirme anında güvenlik duvarınız Joplin’e belirli bir bağlantı portunu dinlemek için izin vermenizi isteyebilir.",
"This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.": "Bu Joplin'in arka planda çalışmasına izin verecektir. Bu ayarı etkinleştirmeniz önerilir; böylece notlarınız sürekli olarak eşitlenir, çakışma sayısı azalır.",
"This will open a new screen. Save your current changes?": "Yeni bir pencere açılacak. Değişiklikler kaydedilsin mi?",
"This will remove the notebook from your collection and you will no longer have access to its content. Do you wish to continue?": "Bu işlem not defterini koleksiyonunuzdan kaldıracak, ve de içeriğine artık erişemeyeceksiniz. Devam etmek istiyor musunuz?",
"Time format": "Zaman biçimi",
"title": "başlık",
"Title": "Başlık",
"To allow Joplin to synchronise with Dropbox, please follow the steps below:": "Joplin'in Dropbox ile senkronize edilmesine izin vermek için lütfen aşağıdaki adımları izleyin:",
"To continue, please enter your master password below.": "Devam etmek için lütfen aşağıya ana parolanızı girin",
"To delete a tag, untag the associated notes.": "Bir etiketi silmek için ilişkilendirilmiş notların etiketini kaldırın.",
"To delete: %d": "Sil: %d",
"To enter command line mode, press \":\"": "Komut satırı moduna girmek için \":\" e basın",
@@ -755,8 +817,10 @@
"Toggle editors": "Editörleri aç / kapat",
"Toggle external editing": "Dış düzenlemeyi aç/kapat",
"Toggle note list": "Not listesini aç/kapat",
"Toggle own sort order": "Kendi Özel sıralamanı aç / kapat",
"Toggle safe mode": "Güvenli modu aç / kapat",
"Toggle sidebar": "Kenar çubuğunu aç / kapat",
"Toggle sort order field": "Sıralama alanine aç / kapat",
"Token has been copied to the clipboard!": "Anahtar panoya kopyalandı!",
"Tools": "Araçlar",
"Total: %d/%d": "Toplam: %d/%d",
@@ -777,6 +841,7 @@
"Unsupported link or message: %s": "Desteklenmeyen bağlantı veya mesaj: %s",
"Untitled": "Başlıksız",
"Update": "Güncelle",
"Update profile": "Profili güncelle",
"Updated": "Güncellendi",
"updated date": "güncelleme zamanı",
"Updated local items: %d.": "Güncellenen yerel öğeler: %d.",
@@ -796,6 +861,8 @@
"Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.": "Arama ile ilgili bir sorun olduğunda bunu kullanın. Not sayısına göre bu işlem uzun sürebilir.",
"Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.": "Markdown metin düzenleyicilerde pek çok metinde kullanılır. Bu değer bulunamazsa, genişliği karakterlerce değişebilen jenerik bir font kullanılacak.",
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": "Sabit bir metin uzunluğu gerektiren yerlerde (örn: tablolar, doğrulama kutucukları, kod vs.) kullanılır. Eğer bulunamazsa jenerik ve monospace olan bir font kullanılacak.",
"User deletions": "Kullancı silmeleri",
"Users": "Kullanıcılar",
"Valid": "Geçerli",
"View": "Görüntüle",
"View on map": "Haritada gör",
@@ -825,6 +892,8 @@
"You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.": "Bu aracı kullanarak verinizi yeniden şifreleyebilirsiniz. Bu metodu örneğin bazı notlarınızın eski bir şifreleme metoduyla şifrelendiği zamanlarda kullanabilirsiniz.",
"Your choice: ": "Seçiminiz: ",
"Your data is going to be re-encrypted and synced again.": "Veriniz yeniden şifrelenecek ve senkron edilecek.",
"Your master password is needed to decrypt some of your data.": "Verilerinizin bir kısmının şifrelemesinin çözülmesi için ana parolanız gerekmektedir.",
"Your password is needed to decrypt some of your data. Type `:e2ee decrypt` to set it.": "Verilerinizin bir kısmının şifrelelemesinin çözülmesi için ana parolanız gerekmektedir. `:e2ee decrypt` yazarak ayarlayabilirsiniz.",
"Your permission to use your camera is required.": "Kamera kullanımı için izniniz gerekmektedir.",
"Your version: %s": "Sürümünüz: %s",
"Zoom In": "Yakınlaştır",

View File

@@ -1,5 +1,5 @@
{
"\"%s\" is missing the required \"%s\" property.": "“%s”缺少必须的属性%s”。",
"\"%s\" is missing the required \"%s\" property.": "“%s”缺少必须的属性\"%s”。",
"%d days": "%d 天",
"%d hour": "%d 小时",
"%d hours": "%d 小时",
@@ -7,7 +7,7 @@
"%d notes match this pattern. Delete them?": "有 %d 条笔记匹配。是否删除?",
"%s %s (%s, %s)": "%s %s (%s, %s)",
"%s (%s) could not be uploaded: %s": "%s (%s) 无法上传:%s",
"%s (%s) would like to share a notebook with you.": "%s (%s) 想要分享笔记本给。",
"%s (%s) would like to share a notebook with you.": "%s (%s) 想要分享笔记本给。",
"%s (%s): %s": "%s (%s): %s",
"%s (pre-release)": "%s(预发行版)",
"%s - Copy": "%s - 副本",
@@ -31,7 +31,7 @@
"(wysiwyg: %s)": "(兼容所见即所得编辑器: %s)",
"- Camera: to allow taking a picture and attaching it to a note.": "- 相机:允许拍照并将照片添加到一条笔记中。",
"- Location: to allow attaching geo-location information to a note.": "- 定位:允许将地理位置信息附加到一条笔记中。",
"- Storage: to allow attaching files to notes and to enable filesystem synchronisation.": "- 存储:允许将文件附加到笔记中启用文件系统同步。",
"- Storage: to allow attaching files to notes and to enable filesystem synchronisation.": "- 存储:允许将文件附加到笔记中启用文件系统同步。",
"<tag-command> can be \"add\", \"remove\", \"list\", or \"notetags\" to assign or remove [tag] from [note], to list notes associated with [tag], or to list tags associated with [note]. The command `tag list` can be used to list all the tags (use -l for long option).": "<tag-command> 可以是 \"add\" 、 \"remove\" 、 \"list\" 或者 \"notetags\" ,用于从 [note] 中赋值或删除 [tag],或者列出与 [tag] 相关的笔记。`tag list` 命令可以用于列出所有的标签 (对于过长选项请使用 -l 参数) 。",
"<todo-command> can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.": "<todo-command> 可以是 \"toggle\" 或者 \"clear\" 。使用 \"toggle\" 命令来切换待办事项的完成状态 (若目标为普通笔记则将会转换成待办事项) 。使用 \"clear\" 命令来把待办事项转换到普通笔记。",
"A3": "A3",
@@ -40,7 +40,7 @@
"About Joplin": "关于 Joplin",
"accelerator": "加速键",
"Accelerator \"%s\" is not valid.": "加速键“%s”无效。",
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to unexpected behaviour.": "加速键“%s”被用于“%s”和“%s”命令。这可能导致未期待的行为。",
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to unexpected behaviour.": "加速键“%s”被用于“%s”和“%s”命令。这可能导致意外的表现。",
"Accept": "接受",
"Action": "动作",
"Actions": "动作",
@@ -48,9 +48,11 @@
"Actual Size": "实际大小",
"Add body": "添加内容",
"Add or remove tags:": "添加或删除标签:",
"Add recipient:": "添加接受者:",
"Add recipient:": "添加收件人:",
"Add title": "添加标题",
"Add to dictionary": "添加到字典",
"Admin": "管理员",
"Admin dashboard": "管理员面板",
"Advanced options": "高级选项",
"Advanced tools": "高级工具",
"All notes": "全部笔记",
@@ -77,13 +79,15 @@
"Authentication was not completed (did not receive an authentication token).": "认证未完成 (未收到认证令牌) 。",
"Authorisation token:": "授权令牌:",
"Auto": "自动",
"Auto-add disabled accounts for deletion": "自动添加已禁用的账户以便删除",
"Auto-pair braces, parenthesis, quotations, etc.": "自动配对花括号、圆括号、引号等。",
"Automatically check for updates": "自动检查更新",
"Automatically switch theme to match system theme": "根据系统主题自动切换",
"Back": "返回",
"Bold": "加粗",
"Browse all plugins": "浏览所有插件",
"Browse...": "浏览...",
"Bulleted List": "项目符号列表",
"Bulleted List": "无序列表",
"Cancel": "取消",
"Cancelling background synchronisation... Please wait.": "正在取消后台同步... 请稍候。",
"Cancelling...": "正在取消...",
@@ -92,7 +96,7 @@
"Cannot change encrypted item": "无法更改已加密条目",
"Cannot copy note to \"%s\" notebook": "无法复制笔记到笔记本 \"%s\"",
"Cannot find \"%s\".": "无法找到“%s”。",
"Cannot initialise synchroniser.": "无法初始化同步。",
"Cannot initialise synchroniser.": "无法启动同步。",
"Cannot load \"%s\" module for format \"%s\" and output \"%s\"": "无法为格式“%2$s”和输出“%3$s”加载“%1$s”模块",
"Cannot load \"%s\" module for format \"%s\" and target \"%s\"": "无法加载为格式“%2$s”和目标“%3$s”加载“%1$s”模块",
"Cannot move note to \"%s\" notebook": "无法移动笔记到笔记本 \"%s\"",
@@ -132,8 +136,9 @@
"Completed": "已完成",
"Completed decryption.": "已完成解密。",
"Completed: %s (%s)": "已完成: %s(%s)",
"Compress old changes": "压缩旧的更改",
"Configuration": "配置",
"Confirm password cannot be empty": "确密码不可为空",
"Confirm password cannot be empty": "确密码不可为空",
"Confirm password:": "确认密码:",
"Confirmation": "确认",
"Conflicted: %d": "有冲突: %d 条",
@@ -144,7 +149,8 @@
"Convert to todo": "转换为待办事项",
"Copy": "复制",
"Copy dev mode command to clipboard": "复制开发者模式命令到剪贴板",
"Copy external link": "拷贝外部访问地址",
"Copy external link": "复制外部链接地址",
"Copy image": "复制图片",
"Copy Link Address": "复制链接地址",
"Copy Markdown link": "复制 Markdown 链接",
"Copy path to clipboard": "复制路径到剪贴板",
@@ -157,7 +163,10 @@
"Could not install plugin: %s": "无法安装插件:%s",
"Could not respond to the invitation. Please try again, or check with the notebook owner if they are still sharing it.\n\nThe error was: \"%s\"": "无法对邀请作出回应。请再试一次,或向笔记本所有者核实他们是否仍在共享它。\n\n错误是:“%s”",
"Could not upgrade master key: %s": "无法升级主密钥:%s",
"Create a notebook": "新建笔记本",
"Could not verify the share status of this notebook - aborting. Please try again when you are connected to the internet.": "无法验证此笔记的分享状态 - 正在终止。 请在连接到互联网后再次尝试。",
"Create a notebook": "新建一个笔记本",
"Create notebook": "新建笔记本",
"Create user": "创建用户",
"Created": "创建日期",
"created date": "创建日期",
"Created local items: %d.": "已新建本地项目: %d。",
@@ -179,26 +188,29 @@
"Custom TLS certificates": "自定义 TLS 证书",
"Cut": "剪切",
"Dark": "暗黑",
"Dashboard": "面板",
"Database v%s": "数据库 v%s",
"Date": "日期",
"Date format": "日期格式",
"days": "天",
"Decrypted items: %d": "解密条目: %d",
"Decrypted items: %d": "解密条目: %d",
"Decrypted items: %s / %s": "已解密条目: %s / %s",
"Decrypting items: %d/%d": "正在解密条目:%d/%d",
"Default": "默认",
"Default: %s": "默认值: %s",
"Delete": "删除",
"Delete attachment \"%s\"?": "是否删除附件“%s”?",
"Delete expired sessions": "删除过期的会话",
"Delete expired tokens": "删除过期的令牌",
"Delete line": "删除行",
"Delete local data and re-download from sync target": "删除本地数据并从同步目标导入数据",
"Delete note \"%s\"?": "是否删除笔记“%s”?",
"Delete note?": "是否删除笔记?",
"Delete notebook \"%s\"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.": "是否删除笔记本“%s”?\n\n所有在该笔记本内的笔记和子笔记本也将同时被删除。",
"Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.": "是否删除笔记本?所有在该笔记本内的笔记也将同时被删除。",
"Delete plugin \"%s\"?": "删除插件“%s”?",
"Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.": "是否删除笔记本?所有在该笔记本内的笔记和子笔记本也将同时被删除。",
"Delete plugin \"%s\"?": "是否删除插件“%s”?",
"Delete these %d notes?": "是否删除这 %d 条笔记?",
"Delete this invitation? The recipient will no longer have access to this shared notebook.": "删除这个邀请?接受者将无法再访问到这个共享笔记本。",
"Delete this invitation? The recipient will no longer have access to this shared notebook.": "是否删除该邀请?收件人将无法再访问共享笔记本。",
"Deleted local items: %d.": "已删除本地项目: %d。",
"Deleted remote items: %d.": "已删除远程项目: %d。",
"Deletes the given notebook.": "删除选定的笔记本。",
@@ -211,7 +223,7 @@
"Disable": "禁用",
"Disable encryption": "禁用加密",
"Disable safe mode and restart": "禁用安全模式并重启",
"Disable Web Clipper Service": "禁用网页剪辑服务",
"Disable Web Clipper Service": "禁用网页剪辑",
"Disabled": "已禁用",
"Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?": "禁用加密会导致 *所有笔记与附件* 重新同步,并以非加密的数据形式发送到同步目标。确定继续吗?",
"Discard changes": "放弃更改",
@@ -220,16 +232,16 @@
"Displays only the first top <num> notes.": "只显示最上方的 <num> 条笔记。",
"Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-tnt` would display notes and to-dos.": "仅显示指定类型的项目。可以把 `n` 用于笔记,`t` 用于待办事项,或者 `nt` 用于笔记和待办事项 (示例: `-tt` 只会显示待办事项,当使用 `-tnt` 时将会显示笔记和待办事项。",
"Displays summary about the notes and notebooks.": "显示关于笔记与笔记本的概况。",
"Displays the complete information about note.": "显示关笔记的完整信息。",
"Displays the complete information about note.": "显示关笔记的完整信息。",
"Displays the given note.": "显示选定笔记。",
"Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.": "在当前笔记本中显示笔记。使用 `ls /` 显示笔记本列表。",
"Displays usage information.": "显示用法提示。",
"Displays version information": "显示版本信息",
"Do it now": "现在完成",
"Do it now": "立即执行",
"Do not ask for confirmation.": "不再要求确认。",
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": "不要丢失密码,因为出于安全考虑,这将是解密数据的*唯一*方式!要启用加密功能,请在下面输入密码。",
"Download": "下载",
"Download and install the relevant extension for your browser:": "为您的浏览器下载并安装相关的扩展:",
"Download and install the relevant extension for your browser:": "为您的浏览器下载并安装相关的扩展插件:",
"Downloaded": "已下载",
"Downloaded and decrypted": "已下载并解密",
"Downloaded and encrypted": "已下载并加密",
@@ -254,6 +266,8 @@
"Editor monospace font family": "编辑器等宽字体族",
"Either \"text\" or \"json\"": "\"text\" 或 \"json\"",
"Emacs": "Emacs",
"Email": "邮箱",
"Emails": "邮箱",
"emphasised text": "强调文本",
"Enable": "启用",
"Enable ++insert++ syntax": "启用 ++insert++ 语法",
@@ -261,7 +275,7 @@
"Enable ^sup^ syntax": "启用 ^sup^ 语法",
"Enable abbreviation syntax": "启用缩写语法",
"Enable audio player": "启用音频播放器",
"Enable deflist syntax": "启用清单语法",
"Enable deflist syntax": "启用 deflist 语法",
"Enable encryption": "启用加密",
"Enable footnotes": "启用脚注",
"Enable Fountain syntax support": "启用 Fountain 语法支持",
@@ -272,11 +286,11 @@
"Enable multimarkdown table extension": "启用 MultiMarkdown 表格扩展",
"Enable note history": "启用笔记历史",
"Enable PDF viewer": "启用 PDF 查看器",
"Enable soft breaks": "启软换行",
"Enable table of contents extension": "启用目录扩展",
"Enable soft breaks": "启软换行",
"Enable table of contents extension": "启用目录扩展 ([TOC])",
"Enable typographer support": "启用 Typographer 支持",
"Enable video player": "启用视频播放器",
"Enable Web Clipper Service": "启用网页剪辑服务",
"Enable Web Clipper Service": "启用网页剪辑",
"Enable ~sub~ syntax": "启用 ~sub~ 语法",
"Enabled": "已启用",
"Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target.": "启用加密意味着您的*所有*笔记与附件都将被重新同步,并被加密发送到同步目标。",
@@ -293,25 +307,25 @@
"Enter master password:": "输入主密码:",
"Enter notebook title": "输入笔记本标题",
"Enum": "枚举",
"Error": "错误",
"Error opening note in editor: %s": "在编辑器中打开笔记出错:%s",
"Error": "发生错误",
"Error opening note in editor: %s": "在编辑器中打开笔记时出现错误:%s",
"Error. Please check that URL, username, password, etc. are correct and that the sync target is accessible. The reported error was:": "发生错误。请检查 URL 、用户名、密码等是否正确以及同步目标是否可访问。报告的错误为:",
"Error: %s": "错误:%s",
"Error: %s": "发生错误:%s",
"Errors only": "仅显示错误",
"Evernote Export File (as HTML)": "Evernote 导出文件 (HTML)",
"Evernote Export File (as Markdown)": "Evernote 导出文件 (Markdown)",
"Exits the application.": "退出应用。",
"Export": "导出",
"Export all": "导出全部",
"Export all": "全部导出",
"Export debug report": "导出调试报告",
"Export Debug Report": "导出调试报告",
"Export profile": "导出配置文件",
"Exporting profile...": "正在导出配置文件...",
"Exporting to \"%s\" as \"%s\" format. Please wait...": "导出到“%s”,格式为“%s”。请稍等...",
"Exports Joplin data to the given path. By default, it will export the complete database including notebooks, notes, tags and resources.": "导出 Joplin 数据到选定路径。默认将导出包含笔记本、笔记、标签与资源等完整数据库。",
"Exports Joplin data to the given path. By default, it will export the complete database including notebooks, notes, tags and resources.": "导出 Joplin 数据到选定路径。默认将导出包含笔记本、笔记、标签与资源等完整数据库。",
"Exports only the given note.": "仅导出选定笔记。",
"Exports only the given notebook.": "仅导出选定笔记本。",
"Fail-safe": "故障保护",
"Fail-safe": "故障保护 (Fail-safe)",
"Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)": "故障保护 (Fail-safe) :当同步目标为空时 (通常是配置错误或 Bug ) ,不要删除本地数据",
"Fatal error:": "严重错误:",
"Feature flags": "特性标志",
@@ -319,7 +333,7 @@
"Fetching resources: %d/%d": "正在获取资源:%d/%d",
"File": "文件",
"File system": "文件系统",
"Firefox Extension": "Firefox 扩展",
"Firefox Extension": "Firefox 扩展插件",
"Fix search index": "修复搜索索引",
"Fixing search index...": "正在修复搜索索引...",
"Focus": "聚焦于",
@@ -327,28 +341,30 @@
"Focus title": "聚焦标题",
"Folders": "文件夹",
"For debugging purpose only: export your profile to an external SD card.": "仅用于调试目的:将您的配置文件导出到外部 SD 卡。",
"For information on how to customise the shortcuts please visit %s": "有关如何自定义快捷键,请访问 %s",
"For information on how to customise the shortcuts please visit %s": "若想了解有关如何自定义快捷键的信息,请访问 %s",
"For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:": "若想了解有关端到端加密 (E2EE) 的更多信息,以及如何启用它的建议,请查阅文档:",
"For the list of keyboard shortcuts and config options, type `help keymap`": "输入 `help keymap` 来获取完整的键盘快捷键列表",
"Force path style": "强制路径风格",
"Forward": "前进",
"Found: %d.": "已找到: %d条。",
"FTS enabled: %d": "FTS 已开启: %d",
"Full changelog": "完整更新记录",
"General": "通用选项",
"Generated": "已生成",
"Generating link...": "生成链接...",
"Generating link...": "生成链接...",
"Get it now:": "立即获取:",
"Get pre-releases when checking for updates": "检查更新时获取预发行版本",
"Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.": "读取或设置配置数值。如果 [value] 值没有提供,它将显示 [name] 值。如果没有提供 [name] 或 [value] 值,它将列出当前配置。",
"Go to source URL": "转到源 URL",
"Goto Anything...": "跳转到任意内容...",
"Grant authorisation": "授权令牌",
"Grant authorisation": "批准授权",
"Heading": "标题",
"Help": "帮助",
"Hide %s": "隐藏 %s",
"Hide disabled keys": "隐藏禁用的密钥",
"Hide Joplin": "隐藏 Joplin",
"Highlight": "突出显示",
"Home": "主页",
"Horizontal Rule": "水平线",
"HTML Directory": "HTML 目录",
"HTML File": "HTML 文件",
@@ -362,12 +378,12 @@
"Importing from \"%s\" as \"%s\" format. Please wait...": "从“%s” 导入为“%s”格式 。请稍等...",
"Importing notes...": "正在导入笔记...",
"Imports data into Joplin.": "导入数据到 Jolin。",
"In \"Manual\" mode, attachments are downloaded only when you click on them. In \"Auto\", they are downloaded when you open the note. In \"Always\", all the attachments are downloaded whether you open the note or not.": "“手动”模式下,只有单击附件时才会下载它们。在“自动”中,当你打开笔记时,它们就会被下载下来。在 “总是”,无论是否打开笔记,所有的附件会被下载。",
"In \"Manual\" mode, attachments are downloaded only when you click on them. In \"Auto\", they are downloaded when you open the note. In \"Always\", all the attachments are downloaded whether you open the note or not.": "“手动”模式下,仅在单击附件时才会下载。“自动”模式下,打开笔记,其中的附件就会被全部下载。 “总是”模式下,无论是否打开笔记,所有的附件会被下载。",
"In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.": "在任何命令中,某个笔记或笔记本可通过它的名称或 ID 引用,也可使用代表当前所选笔记和笔记本的快捷变量 ‘$n' 和 '$b'。`$c` 可用于引用当前所选的项目。",
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": "为了将地理位置与笔记关联,本应用需要您的授权以访问您的位置。\n\n您可以在“配置”页面中随时关闭此选项。",
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": "为此,必须对整个数据集进行加密和同步,因此最好在夜间休息时分彻夜运行。\n\n首先,请按照以下说明进行操作: \n\n1.同步您的所有设备。\n2.单击“%s”。\n3.让它运行完成。 在运行时,请避免在其他设备上更改任何笔记,以免发生冲突。\n4.在此设备上完成同步后,同步其他所有设备,并使其同步完成。\n\n重要提醒: 在一台设备上只需要运行一次。",
"In order to use file system synchronisation your permission to write to external storage is required.": "使用文件系统同步,需要写入外部存储的权限。",
"In order to use the web clipper, you need to do the following:": "要使用网页剪辑器,需要执行以下步骤:",
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": "为了将地理位置与笔记关联,本应用需要获取您的位置的权限。\n\n您可以在“配置”页面中随时关闭此选项。",
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": "为此,必须对整个数据集进行加密和同步,因此最好在夜间休息时分运行。\n\n首先,请按照以下说明进行操作: \n\n1.同步您的所有设备。\n2.单击“%s”。\n3.让其完成运行。 在运行时,请不要在其他设备上更改任何笔记,以免发生冲突。\n4.在此设备上完成同步后,同步其他所有设备,并运行至同步完成。\n\n重要提醒: 在一台设备上只需要运行一次。",
"In order to use file system synchronisation your permission to write to external storage is required.": "使用文件系统同步,需要获取写入外部存储的权限。",
"In order to use the web clipper, you need to do the following:": "要使用网页剪辑器,需要执行以下步骤:",
"In progress": "正在进行",
"In: %s": "位于:%s",
"Indent less": "减少缩进",
@@ -383,28 +399,30 @@
"Installing...": "正在安装…",
"Invalid": "无效",
"Invalid %s: %s.": "无效的%s:%s。",
"Invalid answer: %s": "答案无效:%s",
"Invalid answer: %s": "无效答案:%s",
"Invalid command: \"%s\"": "无效命令:“%s”",
"Invalid option value: \"%s\". Possible values are: %s.": "无效的选项值: \"%s\" 。可用值有: %s。",
"Invalid password": "无效密码",
"Italic": "斜体",
"Item \"%s\" could not be downloaded: %s": "条目 \"%s\" 无法下载:%s",
"Items": "条目",
"Items that cannot be decrypted": "无法解密的条目",
"Items that cannot be synchronised": "无法同步的条目",
"Joplin can synchronise your notes using various providers. Select one from the list below.": "Joplin 可以使用多种途径来同步的笔记。可以从下列选项中选择一种方式。",
"Joplin Cloud": "Joplin 论坛",
"Joplin Cloud email": "Joplin 论坛邮箱",
"Joplin Cloud password": "Joplin 论坛密码",
"Joplin can synchronise your notes using various providers. Select one from the list below.": "Joplin 可以使用多种途径来同步的笔记。可以从下列选项中选择一种方式。",
"Joplin Cloud": "Joplin Cloud",
"Joplin Cloud email": "Joplin Cloud 邮箱",
"Joplin Cloud password": "Joplin Cloud 密码",
"Joplin Export Directory": "Joplin 导出目录",
"Joplin Export File": "Joplin 导出文件",
"Joplin failed to decrypt these items multiple times, possibly because they are corrupted or too large. These items will remain on the device but Joplin will no longer attempt to decrypt them.": "Joplin 多次解密这些条目失败,可能由于它们太大或已经损坏。这些条目会保留在设备上,但 Joplin 不会再尝试解密它们。",
"Joplin Forum": "Joplin 论坛",
"Joplin Server": "Joplin 服务器",
"Joplin Server email": "Joplin Server 邮箱",
"Joplin Server email": "Joplin 服务器邮箱",
"Joplin Server password": "Joplin 服务器密码",
"Joplin Server URL": "Joplin 服务器 URL",
"Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.": "Joplin 网页剪辑器可以让将浏览器中的网页和屏幕截图保存到 Joplin。",
"Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.": "Joplin 网页剪辑器可以让将浏览器中的网页和屏幕截图保存到 Joplin。",
"Joplin website": "Joplin 官网",
"Joplin's own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.": "Joplin 自己的同步服务。同时也提供 Joplin 的一些特性,比如发布笔记或者与他人协作笔记本。",
"Joplin's own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.": "Joplin 自己的同步服务。还可以访问 Joplin 的特定功能,比如发布笔记或者与他人协作笔记本。",
"Keep note history for": "保留笔记历史",
"Keyboard Mode": "键盘模式",
"Keyboard Shortcut": "键盘快捷键",
@@ -413,7 +431,7 @@
"Keys that need upgrading": "需要升级的密钥",
"Landscape": "横板",
"Language": "语言",
"Last error: %s": "最后错误: %s",
"Last error: %s": "最后错误: %s",
"Later": "稍后",
"Layout": "布局",
"Layout button sequence": "布局按钮序列",
@@ -427,12 +445,14 @@
"List item": "列表项",
"Loaded": "已加载",
"Location": "位置",
"Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.": "锁定文件已被保存。如果您确认当前未在进行任何同步,可删除锁定文件 \"%s\" 后继续上一操作。",
"Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.": "锁定文件已被占用。如果您确认当前未在进行任何同步,可删除锁定文件 \"%s\" 后继续上一操作。",
"Log": "日志",
"Login": "登录",
"Login below.": "在下方登录。",
"Login with Dropbox": "通过 Dropbox 登录",
"Login with OneDrive": "通过 OneDrive 登录",
"Logout": "登出",
"Logs": "日志",
"Make a donation": "捐助",
"Manage master password": "管理主密码",
"Manage master password...": "管理主密码……",
@@ -451,7 +471,7 @@
"Missing keys": "缺少密钥",
"Missing Master Keys": "缺少主密钥",
"Missing required argument: %s": "缺失必选参数:%s",
"Mobile data - auto-sync disabled": "手机数据自动同步被禁用",
"Mobile data - auto-sync disabled": "移动数据自动同步被禁用",
"More info": "更多信息",
"More information": "更多信息",
"More than one item match \"%s\". Please narrow down your query.": "有多条项目符合“%s”。请缩小您的检索范围。",
@@ -469,14 +489,14 @@
"New sub-notebook": "新建子笔记本",
"New tags:": "新建标签:",
"New to-do": "新建待办事项",
"New version: %s": "新版本:%s",
"New version: %s": "新版本:%s",
"Nextcloud": "Nextcloud",
"Nextcloud password": "Nextcloud 密码",
"Nextcloud username": "Nextcloud 用户名",
"Nextcloud WebDAV URL": "Nextcloud WebDAV URL",
"no": "否",
"No": "否",
"No active notebook.": "无活动笔记本。",
"No active notebook.": "无有效的笔记本。",
"No item with ID %s": "没有 ID 为 %s 的项",
"No notebook has been specified.": "未指定笔记本。",
"No notebook selected.": "未选择笔记本。",
@@ -504,16 +524,18 @@
"Note list growth factor": "笔记列表增长因子",
"Note properties": "笔记属性",
"Note title": "笔记标题",
"Note&book": "笔记本 (&B)",
"Note&book": "笔记&笔记本",
"Note: Does not work in all desktop environments.": "注意: 在部分桌面环境下无法工作。",
"Note: When a note is shared, it will no longer be encrypted on the server.": "注意: 笔记分享后,便不再在服务器上加密。",
"Notebook": "笔记本",
"Notebook list growth factor": "笔记本列表增长因子",
"Notebook: %s": "笔记本:%s",
"Notebooks": "笔记本",
"Notebooks cannot be named \"%s\", which is a reserved title.": "笔记本无法被命名为 \"%s\" ,该标题已被留作他用。",
"Notes": "笔记",
"Notes and settings are stored in: %s": "笔记与设置储存于:%s",
"Notes can only be created within a notebook.": "笔记只能在笔记本内创建。",
"Numbered List": "编号列表",
"Numbered List": "有序列表",
"OK": "确认",
"OLED Dark": "纯黑 (OLED)",
"On %s: %s": "位于 %s: %s",
@@ -541,7 +563,7 @@
"Paste": "粘贴",
"Path:": "路径:",
"PDF File": "PDF 文件",
"Permission needed": "需要权限",
"Permission needed": "需要权限",
"Permission to use camera": "使用相机的权限",
"Please click on \"%s\" to proceed": "请点击“%s”继续",
"Please confirm that you would like to re-encrypt your complete database.": "请确认您要重新加密整个数据库。",
@@ -572,6 +594,10 @@
"Previous versions of this note": "此笔记的早期版本",
"Print": "打印",
"Privacy Policy": "隐私政策",
"Process failed payment subscriptions": "处理失败的付费订阅",
"Process oversized accounts": "处理超容量的账户",
"Process user deletions": "处理用户删除",
"Profile": "配置文件",
"Profile Version: %s": "配置文件版本: %s",
"Properties": "笔记属性",
"Public-private key pair:": "公、私密钥对:",
@@ -584,14 +610,14 @@
"Re-upload local data to sync target": "重新上传本地数据到同步目标",
"Read more about it": "进一步了解",
"Read time: %s min": "阅读时间:%s 分钟",
"Recipient has accepted the invitation": "接受者接受了邀请",
"Recipient has not yet accepted the invitation": "接受者还没有接受邀请",
"Recipient has rejected the invitation": "接受者拒绝了邀请",
"Recipients:": "接受者:",
"Recipient has accepted the invitation": "收件人接受了邀请",
"Recipient has not yet accepted the invitation": "收件人还没有接受邀请",
"Recipient has rejected the invitation": "收件人拒绝了邀请",
"Recipients:": "收件人:",
"Redo": "恢复",
"Refresh": "刷新",
"Reject": "拒绝",
"Remove": "除",
"Remove": "除",
"Remove tag \"%s\" from all notes?": "从所有笔记中删除标签“%s”?",
"Remove this search from the sidebar?": "从边栏中删除该项搜索?",
"Rename": "重命名",
@@ -609,9 +635,15 @@
"Retry All": "全部重试",
"Reveal file in folder": "在文件夹中展示文件",
"Reverse sort order": "倒序",
"Reverses the sorting order.": "反转排序顺序。",
"Reverses the sorting order.": "序。",
"Revision: %s (%s)": "修订: %s (%s)",
"Runs the commands contained in the text file. There should be one command per line.": "执行文本文件中包含的命令。每个命令占一行。",
"S3": "S3",
"S3 access key": "S3 访问密钥",
"S3 bucket": "S3 存储桶",
"S3 region": "S3 地区",
"S3 secret key": "S3 密钥",
"S3 URL": "S3 URL",
"Safe mode is currently active. Note rendering and all plugins are temporarily disabled.": "安全模式当前已被激活。笔记渲染和所有的插件被临时禁用。",
"Save": "保存",
"Save alarm": "保存提醒",
@@ -639,24 +671,25 @@
"Share": "分享",
"Share Notebook": "分享笔记本",
"Share notebook...": "分享笔记本...",
"Sharing notebook...": "分享笔记本...",
"Sharing notebook...": "分享笔记本...",
"Shortcuts are not available in CLI mode.": "快捷键在 CLI 模式下不可用。",
"Show Advanced Settings": "显示高级选项",
"Show all": "显示全部",
"Show completed to-dos": "显示已完成待办事项",
"Show disabled keys": "显示禁用的密钥",
"Show note counts": "显示笔记数",
"Show sort order buttons": "显示排序方法按钮",
"Show tray icon": "显示托盘图标",
"Sidebar": "边栏",
"Size": "大小",
"Skip this version": "跳过该版本",
"Skipped items: %d (use --retry-failed-items to retry decrypting them)": "跳过条目: %d (使用 retry-failed-items 来尝试重新解密)",
"Skipped items: %d (use --retry-failed-items to retry decrypting them)": "跳过条目: %d (使用 --retry-failed-items 来尝试重新解密)",
"Skipped: %d.": "已跳过: %d条。",
"Solarised Dark": "日光暗 (Solarised)",
"Solarised Light": "日光亮 (Solarised)",
"Some items cannot be decrypted.": "些项目无法被解密。",
"Some items cannot be synchronised.": "些条目无法被同步。",
"Some items cannot be synchronised. Press for more info.": "某些条目无法同步。点按以获取更多信息。",
"Some items cannot be decrypted.": "些项目无法被解密。",
"Some items cannot be synchronised.": "些条目无法被同步。",
"Some items cannot be synchronised. Press for more info.": "某些条目无法同步。点按以获取更多信息。",
"Sort notebooks by": "笔记本排序方式",
"Sort notes by": "笔记排序方式",
"Sort selected lines": "排序所选行",
@@ -668,7 +701,7 @@
"Split View": "分栏视图",
"Start application minimised in the tray icon": "应用程序启动时最小化到托盘",
"Start, stop or check the API server. To specify on which port it should run, set the api.port config variable. Commands are (%s).": "启动,停止或检查 API 服务。可以通过设置 ‘api.port’ 变量指定 API 服务运行在哪个端口上。执行命令 (%s) 。",
"Starting decryption... Please wait as it may take several minutes depending on how much there is to decrypt.": "开始解密,请稍候... 取决于需解密的文件数量,解密环节可能需要等待几分钟。",
"Starting decryption... Please wait as it may take several minutes depending on how much there is to decrypt.": "开始解密,请稍候... 取决于需解密的文件数量,环节可能需要几分钟。",
"Starting synchronisation...": "开始同步...",
"Starting to edit note. Close the editor to get back to the prompt.": "开始编辑笔记。关闭编辑器将回到提示符。",
"Statistics": "统计数据",
@@ -676,7 +709,7 @@
"Status": "状态",
"Status: %s": "状态:%s",
"Status: Started on port %d": "状态:在 %d 端口运行",
"Step 1: Enable the clipper service": "步骤一: 启用网页剪辑服务",
"Step 1: Enable the clipper service": "步骤一: 启用网页剪辑",
"Step 1: Open this URL in your browser to authorise the application:": "步骤一:在浏览器中打开此 URL 来授权应用程序:",
"Step 2: Enter the code provided by Dropbox:": "步骤二:输入 Dropbox 提供的代码:",
"Step 2: Install the extension": "步骤二: 安装扩展",
@@ -716,8 +749,9 @@
"Tagged: %d.": "已加标签: %d条。",
"Tags": "标签",
"Take photo": "拍照",
"Tasks": "任务",
"Text editor command": "文本编辑器命令",
"Thank you! Your Joplin Cloud account is now setup and ready to use.": "感谢!的 Joplin Cloud 帐号已经设置完毕。",
"Thank you! Your Joplin Cloud account is now setup and ready to use.": "感谢!的 Joplin Cloud 帐号已经设置完毕。",
"The app is now going to close. Please relaunch it to complete the process.": "应用将要关闭。请重新启动它以完成此过程。",
"The application has been authorised - you may now close this browser tab.": "授权成功 - 您可以关闭此页面了。",
"The application has been authorised!": "应用已成功授权!",
@@ -728,15 +762,15 @@
"The default admin password is insecure and has not been changed! [Change it now](%s)": "默认管理员密码不安全且尚未更改![现在更改](%s)",
"The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.": "默认的加密方法已更改为一种更安全的方法,建议您将其应用于您的数据。",
"The default encryption method has been changed, you should re-encrypt your data.": "默认的加密方法已更改,您应当重新加密数据。",
"The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.": "该文本编辑器命令 (可包参数) 将会被用于打开笔记。若未提供将尝试自动检测默认编辑器。",
"The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes.": "因子属性决定了列表上的一项如何增长或缩小,以适应其容器中相对于其他项目的可用空间。因此因子为 2 的项是因为 1 的项占用空间的两倍。重启软件以查看变化。",
"The following attachments are being watched for changes:": "下列附件被监控变化:",
"The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.": "该文本编辑器命令 (可包参数) 将会被用于打开笔记。若未提供将尝试自动检测默认编辑器。",
"The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes.": "拉伸系数属性用于指定项目之间的容量比例。如,因子为 2 的项目所占容量是因为 1 的项目的两倍。该更改在软件重启后生效。",
"The following attachments are being watched for changes:": "下列附件发生的改动正在被监控:",
"The following keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded key will still be able to decrypt and encrypt your data as usual.": "以下密钥使用了过时的加密算法,建议对其进行升级。 升级后的密钥仍将能够照常解密和加密您的数据。",
"The Joplin mobile app does not currently support this type of link: %s": "Joplin 移动应用目前不支持这种类型的链接:%s",
"The Joplin team has vetted this plugin and it meets our standards for security and performance.": "Joplin 团队已经通过了该插件的审查,它符合我们对于安全和性能的要求。",
"The keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.": "具有这些 ID 的密钥用于加密你的部分项目,但应用程序目前无法访问它们。它们很可能最终会通过同步下载。",
"The Joplin team has vetted this plugin and it meets our standards for security and performance.": "Joplin 团队已经核准了该插件,它符合我们对于安全和性能的要求。",
"The keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.": "具有这些 ID 的密钥正被用于加密您的某些项,但应用程序目前无法访问它们。项目最终会通过同步获取,但目前可能仍未被同步。",
"The master key has been upgraded successfully!": "主密钥已成功升级!",
"The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.": "具有这些 ID 的主密钥用于加密某些项,但应用程序目前无法访问它们。最终它们很可能通过同步下载。",
"The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.": "具有这些 ID 的主密钥正被用于加密某些项,但应用程序目前无法访问它们。项目最终会通过同步获取,但目前可能仍未被同步。",
"The note \"%s\" has been successfully restored to the notebook \"%s\".": "笔记“%s”已成功恢复到笔记本“%s”中。",
"The notebook could not be saved: %s": "无法保存笔记本:%s",
"The notes have been imported: %s": "以下笔记已被导入: %s",
@@ -746,8 +780,8 @@
"The sync target needs to be upgraded. Press this banner to proceed.": "同步目标需要升级。按这个横幅继续。",
"The tag \"%s\" already exists. Please choose a different name.": "标签 \"%s\" 已存在。请选择一个不同的名称。",
"The target to synchronise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).": "所要同步的目标。每个同步目标都可能有名为 `sync.NUM.NAME` 的附加参数 (见下文) 。",
"The Web Clipper needs your authorisation to access your data.": "Web Clipper 需要的授权才能访问的数据。",
"The web clipper service is enabled and set to auto-start.": "网页剪辑服务已启用且将自动启动。",
"The Web Clipper needs your authorisation to access your data.": "网页剪辑器需要的授权才能访问的数据。",
"The web clipper service is enabled and set to auto-start.": "网页剪辑已启用并已设置为自动启动。",
"The web clipper service is not enabled.": "网页剪辑未启用。",
"Theme": "主题",
"There are currently no notes. Create one by clicking on the (+) button.": "当前没有任何笔记。点击 (+) 按钮创建。",
@@ -755,9 +789,9 @@
"There is no data to export.": "没有可导出的数据。",
"There was a [conflict](%s) on the attachment below.\n\n%s": "以下附件存在一个[冲突](%s) \n\n%s",
"There was an error downloading this attachment:": "下载此附件时出错:",
"There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s": "设置的 Joplin Cloud 帐号时发生了一个错误。请确认的邮箱和密码是否正确,然后再试一次。错误是:\n\n%s",
"There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s": "设置的 Joplin Cloud 帐号时发生了一个错误。请确认的邮箱和密码是否正确,然后再试一次。错误是:\n\n%s",
"These items will remain on the device but will not be uploaded to the sync target. In order to find these items, either search for the title or the ID (which is displayed in brackets above).": "这些条目将只保留在本设备上,不会上传到同步目标。若需查找这些项,请搜索标题或 ID (显示在上方括号中) 。",
"These plugins enhance the Markdown renderer with additional features. Please note that, while these features might be useful, they are not standard Markdown and thus most of them will only work in Joplin. Additionally, some of them are *incompatible* with the WYSIWYG editor. If you open a note that uses one of these plugins in that editor, you will lose the plugin formatting. It is indicated below which plugins are compatible or not with the WYSIWYG editor.": "这些插件为 Markdown 渲染器提供额外特性。请注意,尽管这些额外特性可能是可用的,但它们并非标准 Markdown 语法,因此许多功能只能在 Joplin 内运行。此外,其中的一些功能与所见即所得 (WYSIWYG) 编辑器 *不兼容*。如果您在上述编辑器中打开使用了这些插件的笔记,就会丢失插件的格式。下面已表明插件与所见即所得 (WYSIWYG) 编辑器兼容与否。",
"These plugins enhance the Markdown renderer with additional features. Please note that, while these features might be useful, they are not standard Markdown and thus most of them will only work in Joplin. Additionally, some of them are *incompatible* with the WYSIWYG editor. If you open a note that uses one of these plugins in that editor, you will lose the plugin formatting. It is indicated below which plugins are compatible or not with the WYSIWYG editor.": "这些插件为 Markdown 渲染器提供额外特性。请注意,尽管这些额外特性可能有用,但它们并非标准 Markdown 语法,因此许多功能只能在 Joplin 内运行。此外,其中的一些功能与所见即所得 (WYSIWYG) 编辑器 *不兼容*。如果您在上述 (WYSIWYG) 编辑器中打开使用了这些插件的笔记,就会丢失插件的格式。下已注明插件与所见即所得 (WYSIWYG) 编辑器兼容与否。",
"This attachment is not downloaded or not decrypted yet": "该附件没有下载或者尚未解密",
"This attachment is not downloaded or not decrypted yet.": "该附件没有下载或者尚未解密。",
"This authorisation token is only needed to allow third-party applications to access Joplin.": "该授权令牌仅用于允许第三方应用程序访问 Joplin。",
@@ -766,9 +800,9 @@
"This note has been modified:": "该笔记已被修改:",
"This note has no content. Click on \"%s\" to toggle the editor and edit the note.": "该笔记没有任何内容。点击 \"%s\" 切换到编辑器并编辑笔记。",
"This note has no history": "此笔记没有历史记录",
"This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.": "此富文本编辑器有许多限制,建议在使用之前注意它们。",
"This service allows the browser extension to communicate with Joplin. When enabling it your firewall may ask you to give permission to Joplin to listen to a particular port.": "该服务允许浏览器扩展与 Joplin 通信。当启用它时,的防火墙可能会要求你允许 Joplin 监听一个特定的端口。",
"This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.": "这将允许 Joplin 在后台运行,如果经常修改和同步笔记,推荐启用该设置来减少可能的冲突。",
"This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.": "此富文本编辑器有许多限制,建议在使用之前留意。",
"This service allows the browser extension to communicate with Joplin. When enabling it your firewall may ask you to give permission to Joplin to listen to a particular port.": "该服务允许浏览器扩展插件与 Joplin 通信。当启用它时,的防火墙可能会请求允许 Joplin 监听一个特定的端口。",
"This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.": "这将允许 Joplin 在后台运行,如果经常修改和同步笔记,推荐启用该设置来减少可能的冲突。",
"This will open a new screen. Save your current changes?": "这将打开一个新页面。是否保存当前更改?",
"This will remove the notebook from your collection and you will no longer have access to its content. Do you wish to continue?": "这将从您的收藏中删除该笔记本,您将不再有机会访问其内容。您希望继续吗?",
"Time format": "时间格式",
@@ -792,8 +826,10 @@
"Toggle editors": "切换编辑器",
"Toggle external editing": "切换外部编辑",
"Toggle note list": "切换笔记列表",
"Toggle own sort order": "切换自有排序顺序",
"Toggle safe mode": "切换安全模式",
"Toggle sidebar": "切换边栏",
"Toggle sort order field": "切换排序字段",
"Token has been copied to the clipboard!": "令牌已复制到剪贴板!",
"Tools": "工具",
"Total: %d/%d": "总数: %d/%d 条",
@@ -809,12 +845,14 @@
"Unknown item type downloaded - please upgrade Joplin to the latest version": "已下载项目为未知类型,请将 Joplin 升级至最新版本",
"Unpublish note": "取消分享笔记",
"Unshare": "取消分享",
"Unshare this notebook? The recipients will no longer have access to its content.": "取消分享这个笔记本?接受者将无法再访问到它的内容。",
"Unshare this notebook? The recipients will no longer have access to its content.": "取消分享这个笔记本?收件人将无法再访问到它的内容。",
"Unsupported image type: %s": "不支持的图片类型:%s",
"Unsupported link or message: %s": "不被支持的链接或信息:%s",
"Untitled": "无标题",
"Update": "更新",
"Updated": "更新日期",
"Update profile": "更新配置文件",
"Update total sizes": "更新总大小",
"Updated": "已更新",
"updated date": "更新日期",
"Updated local items: %d.": "已更新本地项目: %d。",
"Updated remote items: %d.": "已更新远程项目: %d。",
@@ -833,6 +871,8 @@
"Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.": "如果搜索功能遇到问题,可以使用这个重建索引。花费的时间取决于笔记的数量。",
"Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.": "用于 markdown 编辑器中的大多数文本。如果没找到,会使用默认(非等宽)字体。",
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": "用于那些需要使用固定宽度的字体来清晰地布局文本的场景(例如:表格、多选框和代码块)。如果没有找到,会使用默认(等宽)字体。",
"User deletions": "用户删除",
"Users": "用户",
"Valid": "有效",
"View": "视图",
"View on map": "在地图上查看",
@@ -859,10 +899,11 @@
"You currently have no notebooks.": "您当前没有笔记本。",
"You do not have any installed plugin.": "您尚未安装任何插件。",
"You may also type `status` for more information.": "输入 `status` 可获取更多信息。",
"You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.": "您可以使用下面的工具重新加密您的数据。例如,在当您知道某些笔记使用了过时的加密方法时。",
"You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.": "您可以使用下面的工具重新加密您的数据。例如当您知道某些笔记使用了过时的加密方法时,可使用该项目。",
"Your choice: ": "您的选择: ",
"Your data is going to be re-encrypted and synced again.": "您的数据将被重新加密并再次同步。",
"Your master password is needed to decrypt some of your data.": "要解密您的某些数据,必须使用您的主密码。",
"Your password is needed to decrypt some of your data. Type `:e2ee decrypt` to set it.": "要解密您的某些数据,需要使用您的密码。输入`:e2ee decrypt`来设置。",
"Your permission to use your camera is required.": "您需要授予相机权限。",
"Your version: %s": "您的版本:%s",
"Zoom In": "放大",

View File

@@ -15,8 +15,17 @@ export class MarkupLanguageUtils {
throw new Error(`Unsupported markup language: ${language}`);
}
public extractImageUrls(language: MarkupLanguage, text: string) {
return this.lib_(language).extractImageUrls(text);
public extractImageUrls(language: MarkupLanguage, text: string): string[] {
let urls: string[] = [];
if (language === MarkupLanguage.Any) {
urls = urls.concat(this.lib_(MarkupLanguage.Markdown).extractImageUrls(text));
urls = urls.concat(this.lib_(MarkupLanguage.Html).extractImageUrls(text));
} else {
urls = this.lib_(language).extractImageUrls(text);
}
return urls;
}
// Create a new MarkupToHtml instance while injecting options specific to Joplin

View File

@@ -1,4 +1,4 @@
import { FolderEntity, FolderIcon, NoteEntity } from '../services/database/types';
import { defaultFolderIcon, FolderEntity, FolderIcon, NoteEntity } from '../services/database/types';
import BaseModel, { DeleteOptions } from '../BaseModel';
import time from '../time';
import { _ } from '../locale';
@@ -767,7 +767,11 @@ export default class Folder extends BaseItem {
}
public static unserializeIcon(icon: string): FolderIcon {
return icon ? JSON.parse(icon) : null;
if (!icon) return null;
return {
...defaultFolderIcon(),
...JSON.parse(icon),
};
}
}

View File

@@ -1,8 +1,8 @@
const { supportDir, setupDatabaseAndSynchronizer, switchClient } = require('../testing/test-utils.js');
const Folder = require('../models/Folder').default;
const Note = require('../models/Note').default;
const Resource = require('../models/Resource').default;
const shim = require('../shim').default;
import { supportDir, setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils';
import Folder from '../models/Folder';
import Note from '../models/Note';
import Resource from '../models/Resource';
import shim from '../shim';
const testImagePath = `${supportDir}/photo.jpg`;
@@ -77,4 +77,24 @@ describe('models/Resource', function() {
expect(originalStat.size).toBe(newStat.size);
}));
// it('should encrypt a shared resource using the correct encryption key', (async () => {
// const folder1 = await Folder.save({ title: 'folder1' });
// const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
// await shim.attachFileToNote(note1, testImagePath);
// Resource.shareService_ = {
// shareById: () => {
// return {
// master_key_id: '',
// };
// },
// } as any;
// try {
// } finally {
// Resource.shareService_ = null;
// }
// }));
});

View File

@@ -14,6 +14,7 @@ const { FsDriverDummy } = require('../fs-driver-dummy.js');
import JoplinError from '../JoplinError';
import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
import { getEncryptionEnabled } from '../services/synchronizer/syncInfoUtils';
import ShareService from '../services/share/ShareService';
export default class Resource extends BaseItem {
@@ -24,6 +25,8 @@ export default class Resource extends BaseItem {
public static FETCH_STATUS_DONE = 2;
public static FETCH_STATUS_ERROR = 3;
public static shareService_: ShareService = null;
public static fsDriver_: any;
static tableName() {
@@ -39,6 +42,11 @@ export default class Resource extends BaseItem {
return this.encryptionService_;
}
protected static shareService() {
if (!this.shareService_) throw new Error('Resource.shareService_ is not set!!');
return this.shareService_;
}
static isSupportedImageMimeType(type: string) {
const imageMimeTypes = ['image/jpg', 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp'];
return imageMimeTypes.indexOf(type.toLowerCase()) >= 0;
@@ -197,6 +205,8 @@ export default class Resource extends BaseItem {
public static async fullPathForSyncUpload(resource: ResourceEntity) {
const plainTextPath = this.fullPath(resource);
const share = resource.share_id ? await this.shareService().shareById(resource.share_id) : null;
if (!getEncryptionEnabled() || !itemCanBeEncrypted(resource as any)) {
// Normally not possible since itemsThatNeedSync should only return decrypted items
if (resource.encryption_blob_encrypted) throw new Error('Trying to access encrypted resource but encryption is currently disabled');
@@ -207,7 +217,9 @@ export default class Resource extends BaseItem {
if (resource.encryption_blob_encrypted) return { path: encryptedPath, resource: resource };
try {
await this.encryptionService().encryptFile(plainTextPath, encryptedPath);
await this.encryptionService().encryptFile(plainTextPath, encryptedPath, {
masterKeyId: share && share.master_key_id ? share.master_key_id : '',
});
} catch (error) {
if (error.code === 'ENOENT') throw new JoplinError(`File not found:${error.toString()}`, 'fileNotFound');
throw error;

View File

@@ -1,6 +1,6 @@
import { expectNotThrow, naughtyStrings, setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils';
import Note from '../models/Note';
import Revision from '../models/Revision';
import Revision, { ObjectPatch } from '../models/Revision';
describe('models/Revision', function() {
@@ -139,6 +139,53 @@ describe('models/Revision', function() {
expect(JSON.stringify(merged)).toBe(JSON.stringify(newObject));
}));
it('should handle invalid object patch', (async () => {
const oldObject = {
one: '123',
two: '456',
three: '789',
};
const brokenPatch = `{"new":{"four":"444
"},"deleted":["one"]}`;
const expected = {
two: '456',
three: '789',
four: '444',
};
const merged = Revision.applyObjectPatch(oldObject, brokenPatch);
expect(JSON.stringify(merged)).toBe(JSON.stringify(expected));
}));
it('should not strip off newlines from object values', (async () => {
const oldObject = {
one: '123',
two: '456',
three: '789',
};
const patch: ObjectPatch = {
'new': {
'four': 'one line\ntwo line',
},
'deleted': [],
};
const expected = {
one: '123',
two: '456',
three: '789',
four: 'one line\ntwo line',
};
const merged = Revision.applyObjectPatch(oldObject, JSON.stringify(patch));
expect(JSON.stringify(merged)).toBe(JSON.stringify(expected));
}));
it('should move target revision to the top', (async () => {
const revs = [
{ id: '123' },
@@ -182,6 +229,10 @@ describe('models/Revision', function() {
%0A%0A# `,
expected: [-(19 + 27 + 2), 17 + 67 + 4],
},
{
patch: '',
expected: [-0, +0],
},
];
for (const test of tests) {

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