1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

49 Commits

Author SHA1 Message Date
Laurent Cozic
996b6623f1 Android release v1.0.253 2019-05-24 14:50:18 +01:00
Laurent Cozic
613041b806 Electron release v1.0.155 2019-05-24 14:47:35 +01:00
Laurent Cozic
ff1d01a864 Merge branch 'master' of github.com:laurent22/joplin 2019-05-24 14:47:30 +01:00
Laurent Cozic
bcbbe10bf8 Electron release v1.0.154 2019-05-24 14:47:12 +01:00
Laurent Cozic
3de0abfc84 Update FUNDING.yml 2019-05-24 09:57:53 +01:00
Laurent Cozic
133fd03469 Update FUNDING.yml 2019-05-24 09:55:29 +01:00
Laurent Cozic
4f97c5c017 Update FUNDING.yml 2019-05-24 09:55:07 +01:00
Laurent Cozic
2d8fbac58c Create FUNDING.yml 2019-05-24 09:53:31 +01:00
Laurent Cozic
95f7ac4a4a Merge branch 'master' of github.com:laurent22/joplin 2019-05-24 09:07:11 +01:00
Laurent Cozic
6a56a6ccf0 Doc: Added more technical info for revision history 2019-05-24 09:05:16 +01:00
Luis Orozco
1eb8df9fa6 Desktop: Fixes #1186, #1354: Clears search when clicking on a notebook. (#1504)
* Fixes #1186, #1354. Clears search when clicking on a notebook.

* use new resetSearch method where possible, replaced tabs with spaces

* replaced a couple more spaces with tabs
2019-05-24 08:13:01 +01:00
Helmut K. C. Tessarek
485b4baebb Update de_DE.po 2019-05-23 23:40:42 -04:00
Laurent Cozic
5590d887c9 Doc: Added note history info 2019-05-24 00:21:15 +01:00
Laurent Cozic
d00bfa997e Doc: Fixed more links 2019-05-22 16:57:45 +01:00
Laurent Cozic
1a8590e9b9 Update website 2019-05-22 16:49:57 +01:00
Laurent Cozic
5a978977df Fix Iran flag 2019-05-22 16:49:32 +01:00
Laurent Cozic
5d763c7e6c All: Remove tags from Welcome item due to issue with cleaning them up afterwards 2019-05-22 16:38:53 +01:00
Laurent Cozic
050b089e72 Update translations 2019-05-22 16:34:59 +01:00
Helmut K. C. Tessarek
733ea4027c Doc: use new forum link (#1545) 2019-05-22 16:20:10 +01:00
Helmut K. C. Tessarek
10500c78b1 All: Fix: Default sort order for notebooks should be title and ascending (#1541) 2019-05-22 16:18:16 +01:00
Caleb John
0040cc02a2 Only delete the .desktop file if it will be replaced by the script (#1537) 2019-05-22 16:16:41 +01:00
Luis Orozco
74afd20f0c Desktop: Fixes #1426: added backticks to auto-wrapping quotes. (#1534) 2019-05-22 16:16:03 +01:00
Luis Orozco
dc9bde2184 Github: updated Contributing.md (#1533)
* updated Contributing.md

- Added several guidelines
- Moved some rules to bulleted lists (for quicker reading).

* Replace links to old forum domain to new domain. Removed a word.
2019-05-22 16:14:59 +01:00
Mehrad Mahmoudian
5243ea7eb2 All: New: Added Persian translation (#1539)
* [init] the first version of the fa.po file

Providing translation for Persian language

* [fix] moved the fa.po file into correct path
2019-05-22 16:05:25 +01:00
水货
7d93492658 Update zh_CN.po (#1524) 2019-05-22 15:57:20 +01:00
Helmut K. C. Tessarek
c6b56345f5 add /%d to Fetching resources: %d (#1532) 2019-05-22 15:56:25 +01:00
Laurent Cozic
8a6fe20a69 All: Resolves #1481: New: Allow downloading attachments on demand or automatically (#1527)
* Allow downloading resources automatically, on demand, or when loading note

* Make needToBeFetched calls to return the right number of resources

* All: Improved handling of resource downloading and decryption

* Desktop: Click on resource to download it (and, optionally, to decrypt it)

* Desktop: Better handling of resource state (not downloaded, downloading, encrypted) in front end

* Renamed setting to sync.resourceDownloadMode

* Download resources when changing setting

* tweaks

* removed duplicate cs

* Better report resource download progress

* Make sure resource cache is properly cleared when needed

* Also handle manual download for non-image resources

* More improvements to logic when downloading and decrypting resources
2019-05-22 15:56:07 +01:00
Laurent Cozic
6bcbedd6a4 CLI v1.0.137 2019-05-19 12:05:02 +01:00
Laurent Cozic
4c935b78f9 Removed log statement 2019-05-19 12:04:09 +01:00
Laurent Cozic
94cddda6d0 Removed temp files 2019-05-19 11:22:00 +01:00
Laurent Cozic
1924ea062c CLI v1.0.136 2019-05-19 11:20:17 +01:00
Laurent Cozic
07e88b2eeb All: Handle missing resource blob when setting resource size 2019-05-19 11:18:44 +01:00
Laurent Cozic
e4a08c29d7 Desktop, Mobile: Improved: Gray out checkboxes that have been ticked inside notes 2019-05-17 22:41:30 +01:00
Laurent Cozic
d60afcaabe Fixed merge 2019-05-16 17:36:02 +00:00
Laurent Cozic
1a091460ca All: Fixed: Prevent app from trying to upload resource it has not downloaded yet 2019-05-16 17:34:16 +00:00
Laurent Cozic
8ebaa7f6eb All: Put back "Fetched items" message during sync 2019-05-15 08:14:36 +01:00
Laurent Cozic
e2a64e21a2 Electron release v1.0.153 2019-05-14 22:23:47 +01:00
Laurent Cozic
78ddd22f09 Log more revision information to allow debugging issues 2019-05-14 22:23:34 +01:00
Laurent Cozic
c546b7076a Fixed doc 2019-05-14 22:02:47 +01:00
Laurent Cozic
0e2bb5d784 Desktop: Improved: When opening a note using Goto Anything, open all its parent notebooks too 2019-05-14 00:11:27 +01:00
Laurent Cozic
5c069c38f5 CLI v1.0.135 2019-05-13 23:59:27 +01:00
Laurent Cozic
451b9c0ae9 CLI v1.0.133 2019-05-13 23:55:53 +01:00
Laurent Cozic
047897621a Fix CLI build script 2019-05-13 23:52:12 +01:00
Laurent Cozic
52e5cec585 Update website 2019-05-13 23:41:31 +01:00
Laurent Cozic
bc98b65efa Update website 2019-05-13 23:23:57 +01:00
Laurent Cozic
9250e77862 Added link to CLI changelog 2019-05-13 23:23:42 +01:00
Laurent Cozic
cd69e71945 Forgot to publish in publish script 2019-05-13 23:20:25 +01:00
Laurent Cozic
e705e6e990 CLI v1.0.129 2019-05-13 23:18:57 +01:00
Laurent Cozic
4638f11c5e Created CLI release script with changelog auto-generation 2019-05-13 23:18:44 +01:00
153 changed files with 4031 additions and 772 deletions

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# These are supported funding model platforms
github: laurent22
patreon: joplin
custom: https://joplinapp.org/donate/

View File

@@ -1,26 +1,42 @@
# User support
For general discussion about Joplin, user support, software development questions, and to discuss new features, please go to the [Joplin Forum](https://discourse.joplin.cozic.net/). It is possible to login with your GitHub account.
The [Joplin Forum](https://discourse.joplinapp.org/) is the community driven place for user support, general discussion about Joplin, problems with installation, new features and software development questions. It is possible to login with your GitHub account.
# Reporting a bug
Please check first that it [has not already been reported](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). Also consider [enabling debug mode](https://github.com/laurent22/joplin/blob/master/readme/debugging.md) before reporting the issue so that you can provide as much details as possible to help fix it.
File bugs in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). Please follow these guidelines:
If possible, **please provide a screenshot**. A screenshot showing the problem is often more useful than a paragraph describing it as it can make it immediately clear what the issue is.
- Search existing issues first, make sure yours hasn't already been reported.
- Don't use the issue tracker for support questions.
- Consider [enabling debug mode](https://github.com/laurent22/joplin/blob/master/readme/debugging.md) so that you can provide as much details as possible when reporting the issue.
- Stay on topic, but describe the issue in detail so that others can reproduce it.
- **Provide a screenshot** if possible. A screenshot showing the problem is often more useful than a paragraph describing it.
# Feature requests
Again, please check that it has not already been requested. If it has, simply **up-vote the issue** - the ones with the most up-votes are likely to be implemented. "+1" comments are not tracked.
Please check that your request has not already been posted in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). If it has, **up-voting the issue** increases the chances it'll be noticed and implemented in the future. "+1" comments are not tracked.
# Creating a pull request
As a general rule, suggestions to _improve Joplin_ should be posted first in the [Joplin Forum](https://discourse.joplinapp.org/) for discussion.
- If you want to add a new feature, consider asking about it before implementing it or checking existing discussions to make sure it is within the scope of the project. As a rule of thumb **if your change is likely to involve more than 50 lines of code, you should discuss it in the forum**, just so that you don't spend too much time implementing something that might not be accepted.
Avoid listing multiple requests in one report in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). One issue per request makes it easier to track and discuss it.
- Bug fixes are always welcome.
# Contribute to the project
## Contributing to Joplin's translation
Joplin is available in multiple languages thanks to the help of its users. You can help translate Joplin to your language or keep it up to date. Please read the documentation about [Localisation](https://github.com/laurent22/joplin#localisation).
## Contributing to Joplin's code
If you want to start contributing to the project's code, please follow these guidelines before creating a pull request:
- Bug fixes are always welcome. Start by reviewing the list of [essential issues](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3Aessential)
- Before adding a new feature, ask about it in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue) or the [Joplin Forum](https://discourse.joplinapp.org/), or check if existing discussions exist to make sure the new functionality is desired.
- **Changes that will consist in more than 50 lines of code should be discussed the [Joplin Forum](https://discourse.joplinapp.org/)**, so that you don't spend too much time implementing something that might not be accepted.
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/master/BUILD.md) for more details.
# Coding style
## Coding style
There are only two rules, but not following them means the pull request will not be accepted (it can be accepted once the issues are fixed):

View File

@@ -1154,8 +1154,8 @@ msgid "Decrypting items: %d/%d"
msgstr "فك تشفير العناصر: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "جلب الموارد: %d"
msgid "Fetching resources: %d/%d"
msgstr "جلب الموارد: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "فضلاً اختر أين سيتم تصدير حالة المزامنة"
@@ -1526,6 +1526,24 @@ msgstr ""
"الوجهة المستهدفة المزامنة إليها. كل وجهة مزامنة مستهدفة قد يكون لها معلمات "
"إضافية تكون مسماة بـ `sync.NUM.NAME` (جميعها موثقة أدناه)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "الدليل الذي تتم المزامنة معه (المسار المطلق)"

View File

@@ -1191,11 +1191,11 @@ msgstr "Blocs de notes"
#, fuzzy, javascript-format
msgid "Decrypting items: %d/%d"
msgstr "Elements obtinguts: %d/%d."
msgstr "Elements obtinguts: %d/%d"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "Recursos: %d."
msgid "Fetching resources: %d/%d"
msgstr "Recursos: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Seleccioneu on s'hauria d'exportar l'estat de la sincronització"
@@ -1569,6 +1569,24 @@ msgstr ""
"L'objectiu on se sincronitzarà. Cada objectiu pot tenir paràmetres "
"addicionals que s'anomenen com a «sync.NUM.NAME» (es documenten a sota)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Directori on es farà la sincronització (camí absolut)"

View File

@@ -1167,8 +1167,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Rozšifrované položky: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "Stahování zdrojů: %d"
msgid "Fetching resources: %d/%d"
msgstr "Stahování zdrojů: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Prosím vyberte, kam má být stav synchronizace exportován"
@@ -1544,6 +1544,24 @@ msgstr ""
"Cíl synchronizace. Každý cíl může mít další parametry ve formátu `sync.NUM."
"NAME` (dokumentace níže)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Složka k synchronizaci (absolutní cesta)"

View File

@@ -1167,11 +1167,11 @@ msgstr "Notesbøger"
#, fuzzy, javascript-format
msgid "Decrypting items: %d/%d"
msgstr "Hentede emner: %d/%d."
msgstr "Hentede emner: %d/%d"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "Ressourcer: %d."
msgid "Fetching resources: %d/%d"
msgstr "Ressourcer: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Vælg hvor sync status skal eksporteres til"
@@ -1551,6 +1551,24 @@ msgstr ""
"Synkroniserings mål. Hver synk. mål kan have ekstra parametre som navngives "
"som `sync.NUM.NAME` (se dokumentation herunder)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Mappe der skal synkroniseres med (absolut sti)"

View File

@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.2.1\n"
"X-Generator: Poedit 2.2.3\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "To delete a tag, untag the associated notes."
@@ -675,7 +675,7 @@ msgid "Quit"
msgstr "Verlassen"
msgid "Close Window"
msgstr ""
msgstr "Fenster schließen"
msgid "&Edit"
msgstr "&Bearbeiten"
@@ -1021,12 +1021,11 @@ msgstr "Standort"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Notizen-Liste"
msgstr "Notizen-Verlauf"
msgid "Previous versions of this note"
msgstr ""
msgstr "Vorherige Version von dieser Notiz"
msgid "Note properties"
msgstr "Notiz-Eigenschaften"
@@ -1034,19 +1033,22 @@ msgstr "Notiz-Eigenschaften"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
"Die Notiz \"%s\" wurde erfolgreich wiederhergestellt und ist im Notizbuch "
"\"%s\" verfügbar."
#, fuzzy
msgid "This note has no history"
msgstr "Diese Notiz wurde verändert:"
msgstr "Diese Notiz hat keinen Verlauf"
msgid "Restore"
msgstr ""
msgstr "Wiederherstellen"
#, javascript-format
msgid ""
"Click \"%s\" to restore the note. It will be copied in the notebook named "
"\"%s\". The current version of the note will not be replaced or modified."
msgstr ""
"Klicke \"%s\" um die Notiz wiederherzustellen. Sie wird in das Notizbuch \"%s"
"\" kopiert. Die aktuelle Version der Notiz wird nicht ersetzt oder verändert."
msgid "Open..."
msgstr "Öffne..."
@@ -1200,8 +1202,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Entschlüsselte Objekte: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "Ressourcen abrufen: %d"
msgid "Fetching resources: %d/%d"
msgstr "Ressourcen abrufen: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr ""
@@ -1361,7 +1363,7 @@ msgstr "Synchronisation ist bereits im Gange. Status: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgstr "Unbekannter Eintrags-Typ heruntergeladen - bitte aktualisiere Joplin"
msgid "Encrypted"
msgstr "Verschlüsselt"
@@ -1578,6 +1580,28 @@ msgstr ""
"kann zusätzliche Parameter haben, die als `sync.NUM.NAME` (alle unten "
"dokumentiert) bezeichnet werden."
msgid "Attachment download behaviour"
msgstr "Verhalten für das Herunterladen von Anhängen"
msgid ""
"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."
msgstr ""
"Im \"Manuell\" Modus werden die Anhänge nur heruntergeladen wenn Du auf sie "
"klickst. Bei \"Automatisch\" werden sie heruntergeladen sobald die Notiz "
"geöffnet wird. Bei \"Immer\" werden die Anhänge heruntergeladen egal ob die "
"Notiz geöffnet wird oder nicht."
msgid "Always"
msgstr "Immer"
msgid "Manual"
msgstr "Manuell"
msgid "Auto"
msgstr "Automatisch"
msgid "Directory to synchronise with (absolute path)"
msgstr "Verzeichnis mit dem synchronisiert werden soll (absoluter Pfad)"
@@ -1617,19 +1641,18 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "Ignoriere TLS-Zertifikatfehler"
#, fuzzy
msgid "Enable note history"
msgstr "Aktiviere Fußnoten"
msgstr "Aktiviere Notizen-Verlauf"
msgid "days"
msgstr ""
msgstr "Tage"
#, javascript-format
msgid "%d days"
msgstr ""
msgstr "%d Tage"
msgid "Keep note history for"
msgstr ""
msgstr "Speicher Notizen-Verlauf für"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
@@ -1704,18 +1727,18 @@ msgstr ""
"Bitte wähle aus, wohin der Synchronisations-Status exportiert werden soll."
msgid "Restored Notes"
msgstr ""
msgstr "Wiederhergestellte Notizen"
msgid "Items that cannot be synchronised"
msgstr "Objekte können nicht synchronisiert werden"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Dieses Notizbuch konnte nicht geöffnet werden: %s"
msgstr "%s (%s) konnte nicht hochgeladen werden: %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Dieses Notizbuch konnte nicht geöffnet werden: %s"
msgstr "Eintrag \"%s\" konnte nicht heruntergeladen werden: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "

View File

@@ -1068,7 +1068,7 @@ msgid "Decrypting items: %d/%d"
msgstr ""
#, javascript-format
msgid "Fetching resources: %d"
msgid "Fetching resources: %d/%d"
msgstr ""
msgid "Please select where the sync status should be exported to"
@@ -1420,6 +1420,24 @@ msgid ""
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr ""

View File

@@ -1086,7 +1086,7 @@ msgid "Decrypting items: %d/%d"
msgstr ""
#, javascript-format
msgid "Fetching resources: %d"
msgid "Fetching resources: %d/%d"
msgstr ""
msgid "Please select where the sync status should be exported to"
@@ -1445,6 +1445,24 @@ msgstr ""
"The target to synchonize to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Directory to synchronize with (absolute path)"

View File

@@ -1176,8 +1176,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Descifrando elementos: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "Obteniendo refuersos: %d"
msgid "Fetching resources: %d/%d"
msgstr "Obteniendo refuersos: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Seleccione a dónde se debería exportar el estado de sincronización"
@@ -1554,6 +1554,24 @@ msgstr ""
"tener parámetros adicionales los cuales son llamados como `sync.NUM.NAME` "
"(todos abajo documentados)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Directorio con el que sincronizarse (ruta completa)"

View File

@@ -1179,11 +1179,11 @@ msgstr "Koadernoak"
#, fuzzy, javascript-format
msgid "Decrypting items: %d/%d"
msgstr "Itemak eskuratuta: %d%d."
msgstr "Itemak eskuratuta: %d/%d"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "Baliabideak: %d."
msgid "Fetching resources: %d/%d"
msgstr "Baliabideak: %d/%d"
#, fuzzy
msgid "Please select where the sync status should be exported to"
@@ -1568,6 +1568,24 @@ msgstr ""
"parametro gehigarriak, horrela izendatuta `sync.NUM.NAME` (dena beherago "
"dokumentatuta)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Sinkronizatzeko direktorioa (bide-izena osorik)"

1837
CliClient/locales/fa.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -660,7 +660,7 @@ msgid "Quit"
msgstr "Quitter"
msgid "Close Window"
msgstr ""
msgstr "Fermer la fenêtre"
msgid "&Edit"
msgstr "&Édition"
@@ -1005,32 +1005,33 @@ msgstr "Lieu"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Liste de notes"
msgstr "Historique des notes"
msgid "Previous versions of this note"
msgstr ""
msgstr "Versions précédentes de cette note"
msgid "Note properties"
msgstr "Propriétés de la note"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
msgstr "La note \"%s\" a été restaurée dans le carnet \"%s\"."
#, fuzzy
msgid "This note has no history"
msgstr "Cette note a été modifiée :"
msgstr "Cette note n'a pas d'historique"
msgid "Restore"
msgstr ""
msgstr "Restaurer"
#, javascript-format
msgid ""
"Click \"%s\" to restore the note. It will be copied in the notebook named "
"\"%s\". The current version of the note will not be replaced or modified."
msgstr ""
"Cliquez sur \"%s\" pour restaurer cette note. Elle sera copiée dans le "
"carnet \"%s\". La version actuelle de la note ne sera pas replacée ou "
"modifiée."
msgid "Open..."
msgstr "Ouvrir..."
@@ -1185,8 +1186,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Déchiffrement des objets : %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "Tél. ressources : %d"
msgid "Fetching resources: %d/%d"
msgstr "Tél. ressources : %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr ""
@@ -1346,7 +1347,7 @@ msgstr "La synchronisation est déjà en cours. État : %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgstr "Objet inconnu téléchargé - veuillez mettre Joplin à jour"
msgid "Encrypted"
msgstr "Chiffré"
@@ -1562,6 +1563,28 @@ msgstr ""
"avoir des paramètres supplémentaires sous le nom `sync.NUM.NOM` (documentés "
"ci-dessous)."
msgid "Attachment download behaviour"
msgstr "Téléchargement des ressources"
msgid ""
"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."
msgstr ""
"En mode \"manuel\", les ressources sont téléchargées uniquement lorsque vous "
"cliquez dessus. En mode \"auto\", elle sont téléchargée lorsque vous ouvrez "
"la note. En mode \"toujours\", toutes les ressources sont téléchargées, que "
"vous ayez ouvert la note ou pas."
msgid "Always"
msgstr "Toujours"
msgid "Manual"
msgstr "Manuel"
msgid "Auto"
msgstr "Auto"
msgid "Directory to synchronise with (absolute path)"
msgstr "Répertoire avec lequel synchroniser (chemin absolu)"
@@ -1601,19 +1624,18 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "Ignorer les erreurs de certificats TLS"
#, fuzzy
msgid "Enable note history"
msgstr "Activer les notes de bas de page"
msgstr "Activer l'historique des notes"
msgid "days"
msgstr ""
msgstr "jours"
#, javascript-format
msgid "%d days"
msgstr ""
msgstr "%d jours"
msgid "Keep note history for"
msgstr ""
msgstr "Garder l'historique des notes pour"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
@@ -1685,18 +1707,18 @@ msgid "Please specify the notebook where the notes should be imported to."
msgstr "Veuillez sélectionner le carnet où les notes doivent être importées."
msgid "Restored Notes"
msgstr ""
msgstr "Notes restaurées"
msgid "Items that cannot be synchronised"
msgstr "Objets qui ne peuvent pas être synchronisés"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Ce fichier n'a pas pu être ouvert : %s"
msgstr "%s (%s) n'a pas pu être envoyé : %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Ce fichier n'a pas pu être ouvert : %s"
msgstr "L'objet \"%s\" n'a pas pu être téléchargé : %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "

View File

@@ -1167,11 +1167,11 @@ msgstr "Cadernos"
#, fuzzy, javascript-format
msgid "Decrypting items: %d/%d"
msgstr "Elementos obtidos: %d/%d."
msgstr "Elementos obtidos: %d/%d"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "Recursos: %d."
msgid "Fetching resources: %d/%d"
msgstr "Recursos: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Seleccione onde exportar o estado da sincronización"
@@ -1551,6 +1551,24 @@ msgstr ""
"Destino co que sincronizar. Cada destino da sincronización pode ter "
"parámetros adicionais que se chaman «sync.NUM.NAME» (documentados arriba)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Cartafol a sincronizar con (ruta absoluta)"

View File

@@ -1166,11 +1166,11 @@ msgstr "Bilježnice"
#, fuzzy, javascript-format
msgid "Decrypting items: %d/%d"
msgstr "Stvorene lokalne stavke: %d."
msgstr "Stvorene lokalne stavke: %d/%d"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "Resursi: %d."
msgid "Fetching resources: %d/%d"
msgstr "Resursi: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Odaberi lokaciju za izvoz statusa sinkronizacije"
@@ -1547,6 +1547,24 @@ msgid ""
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Direktorij za sinkroniziranje (apsolutna putanja)"

View File

@@ -1192,8 +1192,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Decrittografia Elementi: %d/%d"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "Risorse: %d."
msgid "Fetching resources: %d/%d"
msgstr "Risorse: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr ""
@@ -1565,6 +1565,24 @@ msgstr ""
"Ogni target di sincronizzazione può avere parametri aggiuntivi denominati "
"come `sync.NUM.NAME` (tutti documentati di seguito)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Cartella da sincronizzare con (percorso assoluto)"

View File

@@ -1169,8 +1169,8 @@ msgid "Decrypting items: %d/%d"
msgstr "復号中のアイテム: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "取得中のリソース: %d"
msgid "Fetching resources: %d/%d"
msgstr "取得中のリソース: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "同期状況の出力先を選択してください"
@@ -1544,6 +1544,24 @@ msgstr ""
"同期する先です。いずれの同期先も `sync.NUM.NAME` のように追加のパラメーターを"
"持つことができるでしょう(すべてのドキュメントは下にあります)。"
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "同期先のディレクトリ(絶対パス)"

View File

@@ -1068,7 +1068,7 @@ msgid "Decrypting items: %d/%d"
msgstr ""
#, javascript-format
msgid "Fetching resources: %d"
msgid "Fetching resources: %d/%d"
msgstr ""
msgid "Please select where the sync status should be exported to"
@@ -1420,6 +1420,24 @@ msgid ""
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr ""

View File

@@ -1163,8 +1163,8 @@ msgid "Decrypting items: %d/%d"
msgstr "복호화 항목: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "리소스 가져오는 중: %d."
msgid "Fetching resources: %d/%d"
msgstr "리소스 가져오는 중: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "동기화 상태를 내보낼 대상을 선택하세요"
@@ -1536,6 +1536,24 @@ msgstr ""
"동기화를 할 대상을 선택합니다. 각각의 동기화 대상은 `sync.NUM.NAME` 형식으로 "
"된 추가적인 매개 변수를 포함할 수 있습니다(아래에 문서화되어 있습니다)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "동기화를 할 폴더 (절대적 경로)"

View File

@@ -1168,7 +1168,7 @@ msgid "Decrypting items: %d/%d"
msgstr "Dekrypterer elementer: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgid "Fetching resources: %d/%d"
msgstr "Henter ressurser: %d"
msgid "Please select where the sync status should be exported to"
@@ -1545,6 +1545,24 @@ msgstr ""
"Målet å synkronisere til. Hvert synkroniseringsmål kan ha tilleggsparametere "
"som er navngitt som `sync.NUM.NAME` (dokumentert nedenfor)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Katalog å synkronisere med (absolutt sti)"

View File

@@ -1184,11 +1184,11 @@ msgstr "Notitieboeken"
#, fuzzy, javascript-format
msgid "Decrypting items: %d/%d"
msgstr "Opgehaalde items: %d/%d."
msgstr "Opgehaalde items: %d/%d"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "Middelen: %d."
msgid "Fetching resources: %d/%d"
msgstr "Middelen: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Selecteer waar de synchronisatie status naar geëxporteerd moet worden"
@@ -1570,6 +1570,24 @@ msgid ""
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Folder om mee te synchroniseren (absolute pad)"

View File

@@ -1192,8 +1192,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Bezig met ontsleutelen van items: %d/%d"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "Bronnen: %d."
msgid "Fetching resources: %d/%d"
msgstr "Bronnen: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Kies waar de synchronisatiestatus naar moet worden geëxporteerd"
@@ -1566,6 +1566,24 @@ msgstr ""
"argumenten bevatten, welke `sync.NUM.NAME` worden genoemd (wordt hieronder "
"uitgelegd)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Map waarnaar gesynchroniseerd moet worden (absoluut pad)"

View File

@@ -1182,8 +1182,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Decriptando itens: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "Buscando recursos: %d"
msgid "Fetching resources: %d/%d"
msgstr "Buscando recursos: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr ""
@@ -1560,6 +1560,24 @@ msgstr ""
"O alvo para onde sincronizar. Cada alvo pode ter parâmetros adicionais que "
"são nomeados como `sync.NUM.NAME` (todos documentados abaixo)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Diretório para sincronizar (caminho absoluto)"

View File

@@ -1094,8 +1094,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Se decriptează itemi: %d/%d"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "Resurse: %d."
msgid "Fetching resources: %d/%d"
msgstr "Resurse: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr ""
@@ -1452,6 +1452,24 @@ msgid ""
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr ""

View File

@@ -1180,8 +1180,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Расшифровано элементов: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "Получение ресурсов: %d"
msgid "Fetching resources: %d/%d"
msgstr "Получение ресурсов: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr ""
@@ -1558,6 +1558,24 @@ msgstr ""
"Цель синхронизации. Каждая цель синхронизации может иметь дополнительные "
"параметры, именованные как `sync.NUM.NAME` (все документировано ниже)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Каталог синхронизации (абсолютный путь)"

View File

@@ -1182,11 +1182,11 @@ msgstr "Beležnice"
#, fuzzy, javascript-format
msgid "Decrypting items: %d/%d"
msgstr "Preneseni predmeti: %d/%d."
msgstr "Preneseni predmeti: %d/%d"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "Viri: %d."
msgid "Fetching resources: %d/%d"
msgstr "Viri: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Prosim izberite, kam želite izvoziti sinhronizacijski status"
@@ -1566,6 +1566,24 @@ msgstr ""
"Ciljno sinhronizacijsko mesto. Vsak sinhronizacijski cilj ima lahko dodatne "
"parametre imenovano kot `sync.NUM.NAME` (vse je dokumentirano spodaj)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Mesto ciljne sinhronizacije (absolutna pot)"

View File

@@ -1188,8 +1188,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Dekrypterar objekt: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "Hämtar resurser: %d"
msgid "Fetching resources: %d/%d"
msgstr "Hämtar resurser: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Välj vart synkroniseringstillståndet ska exporteras till"
@@ -1567,6 +1567,24 @@ msgstr ""
"Målet att synkronisera till. Varje synkroniseringsmål kan ha ytterligare "
"parametrar som heter `sync.NUM.NAME` (alla dokumenterade nedan)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Katalog för att synkronisera med (absolut sökväg)"

View File

@@ -1157,8 +1157,8 @@ msgid "Decrypting items: %d/%d"
msgstr "Şifresi çözülenler: %d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "Kaynaklar alınıyor: %d"
msgid "Fetching resources: %d/%d"
msgstr "Kaynaklar alınıyor: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "Lütfen senkronizasyon durumunun nereye aktarılacağını seçin"
@@ -1534,6 +1534,24 @@ msgstr ""
"Senkronize edilecek hedef. Her senkronizasyon hedefi, `sync.NUM.NAME` olarak "
"adlandırılan ek parametrelere sahip olabilir (tümü aşağıda belgelenmiştir)."
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "Eşitlenecek dizin (kesin yol)"

View File

@@ -47,7 +47,7 @@ msgid "y"
msgstr "是"
msgid "Cancelling background synchronisation... Please wait."
msgstr "正在取消后台同步... 请稍候。"
msgstr "正在取消后台同步……请稍候。"
#, javascript-format
msgid "No such command: %s"
@@ -601,7 +601,7 @@ msgid "About Joplin"
msgstr "关于 Joplin"
msgid "Preferences..."
msgstr ""
msgstr "偏好……"
msgid "Check for updates..."
msgstr "检查更新..."
@@ -623,7 +623,7 @@ msgid "Quit"
msgstr "退出"
msgid "Close Window"
msgstr ""
msgstr "关闭窗口"
msgid "&Edit"
msgstr "编辑 (&E)"
@@ -947,32 +947,32 @@ msgstr "位置"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "笔记列表"
msgstr "笔记历史"
msgid "Previous versions of this note"
msgstr ""
msgstr "此笔记的早期版本"
msgid "Note properties"
msgstr "笔记属性"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
msgstr "笔记\"%s\"已成功恢复到笔记本\"%s\"中。"
#, fuzzy
msgid "This note has no history"
msgstr "笔记已被修改:"
msgstr "笔记没有历史记录"
msgid "Restore"
msgstr ""
msgstr "恢复"
#, javascript-format
msgid ""
"Click \"%s\" to restore the note. It will be copied in the notebook named "
"\"%s\". The current version of the note will not be replaced or modified."
msgstr ""
"单击 \"%s\" 以恢复笔记。它将会被复制到名为 \"%s\" 的笔记本中。笔记的当前版本"
"不会被替换或修改。"
msgid "Open..."
msgstr "打开…"
@@ -1123,8 +1123,8 @@ msgid "Decrypting items: %d/%d"
msgstr "正在解密项目:%d/%d"
#, javascript-format
msgid "Fetching resources: %d"
msgstr "正在获取资源:%d"
msgid "Fetching resources: %d/%d"
msgstr "正在获取资源:%d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "请选择同步状态的导出位置"
@@ -1163,9 +1163,11 @@ msgid ""
"Type a note title to jump to it. Or type # followed by a tag name, or @ "
"followed by a notebook name."
msgstr ""
"输入笔记标题以便转跳到它。或者输入 # 跟着一个标签名字,或者输入 @ 跟着一个笔"
"记本名字。"
msgid "Goto Anything..."
msgstr ""
msgstr "转到某处……"
#, javascript-format
msgid "Usage: %s"
@@ -1278,7 +1280,7 @@ msgstr "已经在同步。状态:%s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgstr "已下载项目为未知类型,请将 Joplin 升级到最新版本"
msgid "Encrypted"
msgstr "已加密"
@@ -1373,45 +1375,41 @@ msgstr "聚焦正文"
msgid "When creating a new note:"
msgstr "当新建笔记时:"
#, fuzzy
msgid "Enable soft breaks"
msgstr "目录"
msgstr ""
#, fuzzy
msgid "Enable math expressions"
msgstr "启用加密"
msgstr "启用数学表达式"
msgid "Enable ==mark== syntax"
msgstr ""
msgstr "启用 ==mark== 句法"
#, fuzzy
msgid "Enable footnotes"
msgstr "目录"
msgstr "启用脚注"
#, fuzzy
msgid "Enable table of contents extension"
msgstr "目录"
msgstr "启用目录扩展"
msgid "Enable ~sub~ syntax"
msgstr ""
msgstr "启用 ~sub~ 句法"
msgid "Enable ^sup^ syntax"
msgstr ""
msgstr "启用 ^sup^ 句法"
msgid "Enable deflist syntax"
msgstr ""
msgstr "启用术语表句法"
msgid "Enable abbreviation syntax"
msgstr ""
msgstr "启用缩写句法"
msgid "Enable markdown emoji"
msgstr ""
msgstr "启用 markdown emoji"
msgid "Enable ++insert++ syntax"
msgstr ""
msgstr "启用 ++insert++ 句法"
msgid "Enable multimarkdown table extension"
msgstr ""
msgstr "启用 multimarkdown 表格扩展"
msgid "Show tray icon"
msgstr "显示托盘图标"
@@ -1493,6 +1491,24 @@ msgid ""
msgstr ""
"所同步的目标。每个同步目标都可能有名为 `sync.NUM.NAME` 的附加参数(见下文)。"
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "待同步的目录(绝对路径)。"
@@ -1530,9 +1546,8 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "忽略 TLS 证书错误"
#, fuzzy
msgid "Enable note history"
msgstr "目录"
msgstr "启用笔记历史"
msgid "days"
msgstr ""
@@ -1561,7 +1576,7 @@ msgid "Note"
msgstr "笔记"
msgid "Plugins"
msgstr ""
msgstr "插件"
msgid "Application"
msgstr "应用程序"
@@ -1612,18 +1627,18 @@ msgid "Please specify the notebook where the notes should be imported to."
msgstr "请指定导入笔记的目标笔记本。"
msgid "Restored Notes"
msgstr ""
msgstr "已恢复的笔记"
msgid "Items that cannot be synchronised"
msgstr "无法同步项目"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "该笔记本无法打开:%s"
msgstr "%s (%s) 无法上传到:%s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "该笔记本无法打开:%s"
msgstr "项目 \"%s\" 无法从 %s 中下载"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "

View File

@@ -1134,8 +1134,8 @@ msgid "Decrypting items: %d/%d"
msgstr "正在解密項目: %d/%d 項"
#, fuzzy, javascript-format
msgid "Fetching resources: %d"
msgstr "資源: %d"
msgid "Fetching resources: %d/%d"
msgstr "資源: %d/%d"
msgid "Please select where the sync status should be exported to"
msgstr "請選擇將同步狀態導出到的位置"
@@ -1502,6 +1502,24 @@ msgstr ""
"要同步的目標。每個同步目標可能有附加的參數,它們被命名為 `sync.NUM.NAME` (全"
"部記錄如下)。"
msgid "Attachment download behaviour"
msgstr ""
msgid ""
"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."
msgstr ""
msgid "Always"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Auto"
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr "要同步的目錄 (絕對路徑)"

View File

@@ -1,6 +1,6 @@
{
"name": "joplin",
"version": "1.0.128",
"version": "1.0.137",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -20,7 +20,7 @@
],
"owner": "Laurent Cozic"
},
"version": "1.0.128",
"version": "1.0.137",
"bin": {
"joplin": "./main.js"
},

View File

@@ -53,6 +53,10 @@ async function remoteNotesFoldersResources() {
return remoteItemsByTypes([BaseModel.TYPE_NOTE, BaseModel.TYPE_FOLDER, BaseModel.TYPE_RESOURCE]);
}
async function remoteResources() {
return remoteItemsByTypes([BaseModel.TYPE_RESOURCE]);
}
async function localNotesFoldersSameAsRemote(locals, expect) {
let error = null;
try {
@@ -861,7 +865,7 @@ describe('Synchronizer', function() {
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_IDLE);
const fetcher = new ResourceFetcher(() => { return synchronizer().api() });
fetcher.queueDownload(resource1_2.id);
fetcher.queueDownload_(resource1_2.id);
await fetcher.waitForAllFinished();
resource1_2 = await Resource.load(resource1.id);
@@ -890,7 +894,7 @@ describe('Synchronizer', function() {
// Simulate a failed download
get: () => { return new Promise((resolve, reject) => { reject(new Error('did not work')) }); }
} });
fetcher.queueDownload(resource1.id);
fetcher.queueDownload_(resource1.id);
await fetcher.waitForAllFinished();
resource1 = await Resource.load(resource1.id);
@@ -916,7 +920,7 @@ describe('Synchronizer', function() {
expect(r1.size).toBe(-1);
const fetcher = new ResourceFetcher(() => { return synchronizer().api() });
fetcher.queueDownload(r1.id);
fetcher.queueDownload_(r1.id);
await fetcher.waitForAllFinished();
r1 = await Resource.load(r1.id);
expect(r1.size).toBe(2720);
@@ -970,7 +974,7 @@ describe('Synchronizer', function() {
await encryptionService().loadMasterKeysFromSettings();
const fetcher = new ResourceFetcher(() => { return synchronizer().api() });
fetcher.queueDownload(resource1.id);
fetcher.queueDownload_(resource1.id);
await fetcher.waitForAllFinished();
let resource1_2 = (await Resource.all())[0];
@@ -1053,7 +1057,7 @@ describe('Synchronizer', function() {
await encryptionService().loadMasterKeysFromSettings();
const fetcher = new ResourceFetcher(() => { return synchronizer().api() });
fetcher.queueDownload(resource1.id);
fetcher.queueDownload_(resource1.id);
await fetcher.waitForAllFinished();
await decryptionWorker().start();
@@ -1320,4 +1324,65 @@ describe('Synchronizer', function() {
expect(syncItems[1].sync_disabled).toBe(1);
}));
it("should not upload a resource if it has not been fetched yet", asyncTest(async () => {
// In some rare cases, the synchronizer might try to upload a resource even though it
// doesn't have the resource file. It can happen in this situation:
// - C1 create resource
// - C1 sync
// - C2 sync
// - C2 resource metadata is received but ResourceFetcher hasn't downloaded the file yet
// - C2 enables E2EE - all the items are marked for forced sync
// - C2 sync
// The synchronizer will try to upload the resource, even though it doesn't have the file,
// so we need to make sure it doesn't. But also that once it gets the file, the resource
// does get uploaded.
const note1 = await Note.save({ title: 'note' });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
const resource = (await Resource.all())[0];
await Resource.setLocalState(resource.id, { fetch_status: Resource.FETCH_STATUS_IDLE });
await synchronizer().start();
expect((await remoteResources()).length).toBe(0);
await Resource.setLocalState(resource.id, { fetch_status: Resource.FETCH_STATUS_DONE });
await synchronizer().start();
expect((await remoteResources()).length).toBe(1);
}));
it('should decrypt the resource metadata, but not try to decrypt the file, if it is not present', asyncTest(async () => {
const note1 = await Note.save({ title: 'note' });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
const masterKey = await loadEncryptionMasterKey();
await encryptionService().enableEncryption(masterKey, '123456');
await encryptionService().loadMasterKeysFromSettings();
await synchronizer().start();
expect(await allSyncTargetItemsEncrypted()).toBe(true);
await switchClient(2);
await synchronizer().start();
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
await decryptionWorker().start();
let resource = (await Resource.all())[0];
expect(!!resource.encryption_applied).toBe(false);
expect(!!resource.encryption_blob_encrypted).toBe(true);
const resourceFetcher = new ResourceFetcher(() => { return synchronizer().api() });
await resourceFetcher.start();
await resourceFetcher.waitForAllFinished();
const ls = await Resource.localState(resource);
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_DONE);
await decryptionWorker().start();
resource = (await Resource.all())[0];
expect(!!resource.encryption_blob_encrypted).toBe(false);
}));
});

View File

@@ -74,6 +74,11 @@ const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1
console.info('Testing with sync target: ' + SyncTargetRegistry.idToName(syncTargetId_));
const dbLogger = new Logger();
dbLogger.addTarget('console');
dbLogger.addTarget('file', { path: logDir + '/log.txt' });
dbLogger.setLevel(Logger.LEVEL_WARN);
const logger = new Logger();
logger.addTarget('console');
logger.addTarget('file', { path: logDir + '/log.txt' });
@@ -181,7 +186,7 @@ async function setupDatabase(id = null) {
};
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
databases_[id].setLogger(logger);
databases_[id].setLogger(dbLogger);
await databases_[id].open({ name: filePath });
BaseModel.db_ = databases_[id];

View File

@@ -35,8 +35,7 @@ class HeaderComponent extends React.Component {
};
this.search_onClear = (event) => {
this.setState({ searchQuery: '' });
triggerOnQuery('');
this.resetSearch();
if (this.searchElement_) this.searchElement_.focus();
}
@@ -56,6 +55,17 @@ class HeaderComponent extends React.Component {
this.setState({ showSearchUsageLink: false });
}, 5000);
}
this.search_keyDown = event => {
if (event.keyCode === 27) { // ESCAPE
this.resetSearch();
}
}
this.resetSearch = () => {
this.setState({ searchQuery: '' });
triggerOnQuery('');
}
this.searchUsageLink_click = event => {
bridge().openExternal('https://joplinapp.org/#searching');
@@ -68,6 +78,12 @@ class HeaderComponent extends React.Component {
}
}
componentDidUpdate(prevProps) {
if(prevProps.notesParentType !== this.props.notesParentType && this.props.notesParentType !== 'Search' && this.state.searchQuery) {
this.resetSearch();
}
}
componentWillUnmount() {
if (this.hideSearchUsageLinkIID_) {
clearTimeout(this.hideSearchUsageLinkIID_);
@@ -190,6 +206,7 @@ class HeaderComponent extends React.Component {
ref={elem => this.searchElement_ = elem}
onFocus={this.search_onFocus}
onBlur={this.search_onBlur}
onKeyDown={this.search_keyDown}
/>
<a
href="#"
@@ -257,6 +274,7 @@ const mapStateToProps = (state) => {
return {
theme: state.settings.theme,
windowCommand: state.windowCommand,
notesParentType: state.notesParentType,
};
};

View File

@@ -36,6 +36,7 @@ const ResourceFetcher = require('lib/services/ResourceFetcher');
const { toSystemSlashes, safeFilename } = require('lib/path-utils');
const { clipboard } = require('electron');
const SearchEngine = require('lib/services/SearchEngine');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const ModelCache = require('lib/services/ModelCache');
const NoteTextViewer = require('./NoteTextViewer.min');
const NoteRevisionViewer = require('./NoteRevisionViewer.min');
@@ -226,14 +227,13 @@ class NoteTextComponent extends React.Component {
}
}
this.resourceFetcher_downloadComplete = async (resource) => {
this.refreshResource = async (event) => {
if (!this.state.note || !this.state.note.body) return;
const resourceIds = await Note.linkedResourceIds(this.state.note.body);
if (resourceIds.indexOf(resource.id) >= 0) {
// this.mdToHtml().clearCache();
if (resourceIds.indexOf(event.id) >= 0) {
shared.clearResourceCache();
this.lastSetHtml_ = '';
this.scheduleHtmlUpdate();
//this.updateHtml(this.state.note.body);
}
}
@@ -363,7 +363,8 @@ class NoteTextComponent extends React.Component {
eventManager.on('noteTypeToggle', this.onNoteTypeToggle_);
eventManager.on('todoToggle', this.onTodoToggle_);
ResourceFetcher.instance().on('downloadComplete', this.resourceFetcher_downloadComplete);
shared.installResourceHandling(this.refreshResource);
ExternalEditWatcher.instance().on('noteChange', this.externalEditWatcher_noteChange);
}
@@ -376,7 +377,8 @@ class NoteTextComponent extends React.Component {
eventManager.removeListener('noteTypeToggle', this.onNoteTypeToggle_);
eventManager.removeListener('todoToggle', this.onTodoToggle_);
ResourceFetcher.instance().off('downloadComplete', this.resourceFetcher_downloadComplete);
shared.uninstallResourceHandling(this.refreshResource);
ExternalEditWatcher.instance().off('noteChange', this.externalEditWatcher_noteChange);
}
@@ -474,6 +476,8 @@ class NoteTextComponent extends React.Component {
// Scroll back to top when loading new note
if (loadingNewNote) {
shared.clearResourceCache();
this.editorMaxScrollTop_ = 0;
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
@@ -521,6 +525,11 @@ class NoteTextComponent extends React.Component {
this.setViewerPercentScroll(scrollPercent ? scrollPercent : 0);
}, 10);
}
if (note && note.body && Setting.value('sync.resourceDownloadMode') === 'auto') {
const resourceIds = await Note.linkedResourceIds(note.body);
await ResourceFetcher.instance().markForDownload(resourceIds);
}
}
if (note) {
@@ -647,7 +656,7 @@ class NoteTextComponent extends React.Component {
async webview_ipcMessage(event) {
const msg = event.channel ? event.channel : '';
const args = event.args;
const args = event.args;
const arg0 = args && args.length >= 1 ? args[0] : null;
const arg1 = args && args.length >= 2 ? args[1] : null;
@@ -666,6 +675,10 @@ class NoteTextComponent extends React.Component {
const ls = Object.assign({}, this.state.localSearch);
ls.resultCount = arg0;
this.setState({ localSearch: ls });
} else if (msg.indexOf('markForDownload:') === 0) {
const s = msg.split(':');
if (s.length < 2) throw new Error('Invalid message: ' + msg);
ResourceFetcher.instance().markForDownload(s[1]);
} else if (msg === 'percentScroll') {
this.ignoreNextEditorScroll_ = true;
this.setEditorPercentScroll(arg0);
@@ -864,6 +877,9 @@ class NoteTextComponent extends React.Component {
}
return output;
}
//fixes #1426 but this is an Ace issue, so it can be removed if ace/brace is updated.
this.editor_.editor.getSession().getMode().$quotes = {'"': '"', "'": "'", "`": "`"};
// Disable Markdown auto-completion (eg. auto-adding a dash after a line with a dash.
// https://github.com/ajaxorg/ace/issues/2754
@@ -1741,6 +1757,7 @@ class NoteTextComponent extends React.Component {
if (htmlHasChanged) {
let options = {
cssFiles: this.state.lastRenderCssFiles,
downloadResources: Setting.value('sync.resourceDownloadMode'),
};
this.webviewRef_.current.wrappedInstance.send('setHtml', html, options);
this.lastSetHtml_ = html;

View File

@@ -714,7 +714,7 @@ class SideBarComponent extends React.Component {
let resourceFetcherText = '';
if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) {
resourceFetcherText = _('Fetching resources: %d', this.props.resourceFetcher.toFetchCount);
resourceFetcherText = _('Fetching resources: %d/%d', this.props.resourceFetcher.fetchingCount, this.props.resourceFetcher.toFetchCount);
}
let lines = Synchronizer.reportToLines(this.props.syncReport);

View File

@@ -123,6 +123,10 @@
}
loadCssFiles(event.options.cssFiles);
if (event.options.downloadResources === 'manual') {
webviewLib.setupResourceManualDownload();
}
}
let ignoreNextScrollEvent = false;
@@ -308,7 +312,7 @@
updateBodyHeight();
});
updateBodyHeight();
updateBodyHeight();
</script>
</body>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,7 @@ locales['de_DE'] = require('./de_DE.json');
locales['en_US'] = require('./en_US.json');
locales['es_ES'] = require('./es_ES.json');
locales['eu'] = require('./eu.json');
locales['fa'] = require('./fa.json');
locales['fr_FR'] = require('./fr_FR.json');
locales['gl_ES'] = require('./gl_ES.json');
locales['hr_HR'] = require('./hr_HR.json');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.0.152",
"version": "1.0.155",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.0.152",
"version": "1.0.155",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {

View File

@@ -193,13 +193,25 @@ class Dialog extends React.PureComponent {
}
}
gotoItem(item) {
async gotoItem(item) {
this.props.dispatch({
pluginName: PLUGIN_NAME,
type: 'PLUGIN_DIALOG_SET',
open: false,
});
if (this.state.listType === BaseModel.TYPE_NOTE || this.state.listType === BaseModel.TYPE_FOLDER) {
const folderPath = await Folder.folderPath(this.props.folders, item.parent_id);
for (const folder of folderPath) {
this.props.dispatch({
type: "FOLDER_SET_COLLAPSED",
id: folder.id,
collapsed: false,
});
}
}
if (this.state.listType === BaseModel.TYPE_NOTE) {
this.props.dispatch({
type: "FOLDER_AND_NOTE_SELECT",

View File

@@ -37,7 +37,7 @@ if [[ ! -e ~/.joplin/VERSION ]] || [[ $(< ~/.joplin/VERSION) != "$version" ]]; t
echo 'Downloading Joplin...'
# Delete previous version (in future versions joplin.desktop shouldn't exist)
rm -f ~/.joplin/*.AppImage ~/.local/share/applications/joplin.desktop ~/.local/share/applications/appimagekit-joplin.desktop ~/.joplin/VERSION
rm -f ~/.joplin/*.AppImage ~/.local/share/applications/joplin.desktop ~/.joplin/VERSION
# Creates the folder where the binary will be stored
mkdir -p ~/.joplin/
@@ -78,7 +78,10 @@ if [[ ! -e ~/.joplin/VERSION ]] || [[ $(< ~/.joplin/VERSION) != "$version" ]]; t
(cd $TMPDIR && ~/.joplin/Joplin.AppImage --appimage-extract joplin.desktop &> /dev/null)
APPIMAGE_VERSION=$(grep "^X-AppImage-BuildId=" $TMPDIR/squashfs-root/joplin.desktop | head -n 1 | cut -d " " -f 1)
rm -rf $TMPDIR/squashfs-root
# Only delete the desktop file if it will be replaced
rm -f ~/.local/share/applications/appimagekit-joplin.desktop
# On some systems this directory doesn't exist by default
mkdir -p ~/.local/share/applications
echo -e "[Desktop Entry]\nEncoding=UTF-8\nName=Joplin\nComment=Joplin for Desktop\nExec=/home/$USER/.joplin/Joplin.AppImage\nIcon=/home/$USER/.joplin/Icon512.png\nStartupWMClass=Joplin\nType=Application\nCategories=Application;\n$APPIMAGE_VERSION" >> ~/.local/share/applications/appimagekit-joplin.desktop
echo "${COLOR_GREEN}OK${COLOR_RESET}"

View File

@@ -20,15 +20,15 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
Operating System | Download | Alternative
-----------------|--------|-------------------
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.151/Joplin-Setup-1.0.151.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> | Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.151/JoplinPortable.exe'>Portable version</a><br><br>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.
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.151/Joplin-1.0.151.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> | You can also use Homebrew: `brew cask install joplin`
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.151/Joplin-1.0.151-x86_64.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | An Arch Linux package [is also available](#terminal-application).<br><br>If it works with your distribution (it has been tested on Ubuntu, Fedora, Gnome and Mint), the recommended way is to use this script as it will handle the desktop icon too:<br><br> `wget -O - https://raw.githubusercontent.com/laurent22/joplin/master/Joplin_install_and_update.sh | bash`
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.152/Joplin-Setup-1.0.152.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> | Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.152/JoplinPortable.exe'>Portable version</a><br><br>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.
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.152/Joplin-1.0.152.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> | You can also use Homebrew: `brew cask install joplin`
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.152/Joplin-1.0.152-x86_64.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | An Arch Linux package [is also available](#terminal-application).<br><br>If it works with your distribution (it has been tested on Ubuntu, Fedora, Gnome and Mint), the recommended way is to use this script as it will handle the desktop icon too:<br><br> `wget -O - https://raw.githubusercontent.com/laurent22/joplin/master/Joplin_install_and_update.sh \| bash`
## Mobile applications
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://joplinapp.org/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.252/joplin-v1.0.252.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://joplinapp.org/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.253/joplin-v1.0.253.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
## Terminal application
@@ -59,7 +59,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
- Support
- [Joplin Forum](https://discourse.joplin.cozic.net)
- [Joplin Forum](https://discourse.joplinapp.org)
- [How to enable end-to-end encryption](https://github.com/laurent22/joplin/blob/master/readme/e2ee.md)
- [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/master/readme/spec.md)
- [How to enable debug mode](https://github.com/laurent22/joplin/blob/master/readme/debugging.md)
@@ -68,7 +68,8 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
- About
- [Changelog](https://github.com/laurent22/joplin/blob/master/readme/changelog.md)
- [Changelog (Desktop App)](https://github.com/laurent22/joplin/blob/master/readme/changelog.md)
- [Changelog (CLI App)](https://github.com/laurent22/joplin/blob/master/readme/changelog_cli.md)
- [Stats](https://github.com/laurent22/joplin/blob/master/readme/stats.md)
- [Donate](https://github.com/laurent22/joplin/blob/master/readme/donate.md)
<!-- TOC -->
@@ -78,6 +79,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
- Desktop, mobile and terminal applications.
- [Web Clipper](https://github.com/laurent22/joplin/blob/master/readme/clipper.md) for Firefox and Chrome.
- End To End Encryption (E2EE)
- Note history (revisions)
- Synchronisation with various services, including NextCloud, Dropbox, WebDAV and OneDrive.
- Import Enex files (Evernote export format) and Markdown files.
- Export JEX files (Joplin Export format) and raw files.
@@ -123,7 +125,7 @@ In general the way to import notes from any application into Joplin is to conver
* Standard Notes: Please see [this tutorial](https://programadorwebvalencia.com/migrate-notes-from-standard-notes-to-joplin/)
* Tomboy Notes: Export the notes to ENEX files [as described here](https://askubuntu.com/questions/243691/how-can-i-export-my-tomboy-notes-into-evernote/608551) for example, and import these ENEX files into Joplin.
* OneNote: First [import the notes from OneNote into Evernote](https://discussion.evernote.com/topic/107736-is-there-a-way-to-import-from-onenote-into-evernote-on-the-mac/). Then export the ENEX file from Evernote and import it into Joplin.
* NixNote: Synchronise with Evernote, then export the ENEX files and import them into Joplin. More info [in this thread](https://discourse.joplin.cozic.net/t/import-from-nixnote/183/3).
* NixNote: Synchronise with Evernote, then export the ENEX files and import them into Joplin. More info [in this thread](https://discourse.joplinapp.org/t/import-from-nixnote/183/3).
# Exporting
@@ -193,6 +195,14 @@ Joplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a
For a more technical description, mostly relevant for development or to review the method being used, please see the [Encryption specification](https://joplinapp.org/spec/).
# Note history
The Joplin applications automatically save previous versions of your notes at regular intervals. These versions are synced across devices and can be viewed from the desktop application. To do so, click on the "Information" button on a note, then click on "Previous version of this note". From this screen you can view the previous versions of the note as well as restore any of them.
This feature can be disabled from the "Note history" section in the settings, and it is also possible to change for how long the history of a note is saved.
More information about this feature in the [announcement post](https://www.patreon.com/posts/note-history-now-27083082).
# External text editor
Joplin notes can be opened and edited using an external editor of your choice. It can be a simple text editor like Notepad++ or Sublime Text or an actual Markdown editor like Typora. In that case, images will also be displayed within the editor. To open the note in an external editor, click on the icon in the toolbar or press Ctrl+E (or Cmd+E). Your default text editor will be used to open the note. If needed, you can also specify the editor directly in the General Options, under "Text editor command".
@@ -323,8 +333,8 @@ Please see the [donation page](https://joplinapp.org/donate/) for information on
# Community
- For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplin.cozic.net/). It is possible to login with your GitHub account.
- Also see here for information about [the latest releases and general news](https://discourse.joplin.cozic.net/c/news).
- For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplinapp.org/). It is possible to login with your GitHub account.
- Also see here for information about [the latest releases and general news](https://discourse.joplinapp.org/c/news).
- For bug reports and feature requests, go to the [GitHub Issue Tracker](https://github.com/laurent22/joplin/issues).
- The latest news are posted [on the Patreon page](https://www.patreon.com/joplin).
@@ -350,32 +360,33 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
![](https://joplinapp.org/images/flags/country-4x3/arableague.png) | Arabic | [ar](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ar.po) | عبد الناصر سعيد (as@althobaity.com) | 93%
![](https://joplinapp.org/images/flags/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 53%
![](https://joplinapp.org/images/flags/country-4x3/arableague.png) | Arabic | [ar](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ar.po) | عبد الناصر سعيد (as@althobaity.com) | 92%
![](https://joplinapp.org/images/flags/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 52%
![](https://joplinapp.org/images/flags/es/catalonia.png) | Catalan | [ca](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ca.po) | jmontane, 2018 | 75%
![](https://joplinapp.org/images/flags/country-4x3/hr.png) | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić (trbuhom@net.hr) | 43%
![](https://joplinapp.org/images/flags/country-4x3/cz.png) | Czech | [cs_CZ](https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po) | Lukas Helebrandt (lukas@aiya.cz) | 93%
![](https://joplinapp.org/images/flags/country-4x3/dk.png) | Dansk | [da_DK](https://github.com/laurent22/joplin/blob/master/CliClient/locales/da_DK.po) | Morten Juhl-Johansen Zölde-Fejér (mjjzf@syntaktisk. | 68%
![](https://joplinapp.org/images/flags/country-4x3/de.png) | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Michael Sonntag (ms@editorei.de) | 97%
![](https://joplinapp.org/images/flags/country-4x3/hr.png) | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić (trbuhom@net.hr) | 42%
![](https://joplinapp.org/images/flags/country-4x3/cz.png) | Czech | [cs_CZ](https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po) | Lukas Helebrandt (lukas@aiya.cz) | 92%
![](https://joplinapp.org/images/flags/country-4x3/dk.png) | Dansk | [da_DK](https://github.com/laurent22/joplin/blob/master/CliClient/locales/da_DK.po) | Morten Juhl-Johansen Zölde-Fejér (mjjzf@syntaktisk. | 67%
![](https://joplinapp.org/images/flags/country-4x3/de.png) | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Michael Sonntag (ms@editorei.de) | 96%
![](https://joplinapp.org/images/flags/country-4x3/gb.png) | English (UK) | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100%
![](https://joplinapp.org/images/flags/country-4x3/us.png) | English (US) | [en_US](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_US.po) | | 100%
![](https://joplinapp.org/images/flags/country-4x3/es.png) | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Andros Fenollosa (andros@fenollosa.email) | 93%
![](https://joplinapp.org/images/flags/country-4x3/fr.png) | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 97%
![](https://joplinapp.org/images/flags/es/galicia.png) | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans (marcoslansgarza@gmail.com) | 67%
![](https://joplinapp.org/images/flags/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 83%
![](https://joplinapp.org/images/flags/country-4x3/be.png) | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 53%
![](https://joplinapp.org/images/flags/country-4x3/nl.png) | Nederlands | [nl_NL](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_NL.po) | Heimen Stoffels (vistausss@outlook.com) | 81%
![](https://joplinapp.org/images/flags/country-4x3/es.png) | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Andros Fenollosa (andros@fenollosa.email) | 92%
![](https://joplinapp.org/images/flags/country-4x3/fr.png) | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 100%
![](https://joplinapp.org/images/flags/es/galicia.png) | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans (marcoslansgarza@gmail.com) | 66%
![](https://joplinapp.org/images/flags/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 82%
![](https://joplinapp.org/images/flags/country-4x3/be.png) | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 52%
![](https://joplinapp.org/images/flags/country-4x3/nl.png) | Nederlands | [nl_NL](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_NL.po) | Heimen Stoffels (vistausss@outlook.com) | 80%
![](https://joplinapp.org/images/flags/country-4x3/no.png) | Norwegian | [nb_NO](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nb_NO.po) | Mats Estensen (code@mxe.no) | 93%
![](https://joplinapp.org/images/flags/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | Renato Nunes Bastos (rnbastos@gmail.com) | 87%
![](https://joplinapp.org/images/flags/country-4x3/ro.png) | Română | [ro](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ro.po) | | 53%
![](https://joplinapp.org/images/flags/country-4x3/si.png) | Slovenian | [sl_SI](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sl_SI.po) | | 66%
![](https://joplinapp.org/images/flags/country-4x3/se.png) | Svenska | [sv](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sv.po) | Jonatan Nyberg (jonatan@autistici.org) | 90%
![](https://joplinapp.org/images/flags/country-4x3/.png) | Persian | [fa](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fa.po) | Mehrad Mahmoudian (mehrad@mahmoudian.me) | 36%
![](https://joplinapp.org/images/flags/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | Renato Nunes Bastos (rnbastos@gmail.com) | 86%
![](https://joplinapp.org/images/flags/country-4x3/ro.png) | Română | [ro](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ro.po) | | 52%
![](https://joplinapp.org/images/flags/country-4x3/si.png) | Slovenian | [sl_SI](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sl_SI.po) | | 65%
![](https://joplinapp.org/images/flags/country-4x3/se.png) | Svenska | [sv](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sv.po) | Jonatan Nyberg (jonatan@autistici.org) | 89%
![](https://joplinapp.org/images/flags/country-4x3/tr.png) | Türkçe | [tr_TR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/tr_TR.po) | Zorbey Doğangüneş (zorbeyd@gmail.com) | 87%
![](https://joplinapp.org/images/flags/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov (artyom.karlov@gmail.com) | 93%
![](https://joplinapp.org/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 93%
![](https://joplinapp.org/images/flags/country-4x3/tw.png) | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_TW.po) | penguinsam (samliu@gmail.com) | 81%
![](https://joplinapp.org/images/flags/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov (artyom.karlov@gmail.com) | 92%
![](https://joplinapp.org/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 98%
![](https://joplinapp.org/images/flags/country-4x3/tw.png) | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_TW.po) | penguinsam (samliu@gmail.com) | 80%
![](https://joplinapp.org/images/flags/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | AWASHIRO Ikuya (ikunya@gmail.com) | 87%
![](https://joplinapp.org/images/flags/country-4x3/kr.png) | 한국말 | [ko](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ko.po) | | 89%
![](https://joplinapp.org/images/flags/country-4x3/kr.png) | 한국말 | [ko](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ko.po) | | 88%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Known bugs

View File

@@ -90,8 +90,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097488
versionName "1.0.252"
versionCode 2097489
versionName "1.0.253"
ndk {
abiFilters "armeabi-v7a", "x86"
}

View File

@@ -279,6 +279,12 @@ class BaseApplication {
}
}
resourceFetcher_downloadComplete(event) {
if (event.encrypted) {
DecryptionWorker.instance().scheduleStart();
}
}
reducerActionToString(action) {
let o = [action.type];
if ('id' in action) o.push(action.id);
@@ -426,7 +432,7 @@ class BaseApplication {
}
if (this.hasGui() && action.type === 'SYNC_CREATED_RESOURCE') {
ResourceFetcher.instance().queueDownload(action.id);
ResourceFetcher.instance().autoAddResources();
}
if (refreshFolders) {
@@ -603,6 +609,7 @@ class BaseApplication {
ResourceFetcher.instance().setFileApi(() => { return reg.syncTarget().fileApi() });
ResourceFetcher.instance().setLogger(this.logger_);
ResourceFetcher.instance().on('downloadComplete', this.resourceFetcher_downloadComplete);
ResourceFetcher.instance().start();
SearchEngine.instance().setDb(reg.db());

View File

@@ -161,6 +161,25 @@ module.exports = function(style, options) {
color: black;
}
.not-loaded-resource img {
width: 1.15em;
height: 1.15em;
}
a.not-loaded-resource img {
margin-right: .2em;
}
a.not-loaded-resource {
display: flex;
flex-direction: row;
align-items: center;
}
.checkbox-label-checked {
opacity: 0.5;
}
@media print {
body {
height: auto !important;

View File

@@ -32,7 +32,16 @@ function createPrefixTokens(Token, id, checked, label, postMessageSyntax, source
// in calling code.
const lineIndex = sourceToken.map && sourceToken.map.length ? sourceToken.map[0] : 99999999;
const checkedString = checked ? 'checked' : 'unchecked';
const js = postMessageSyntax + "('checkboxclick:" + checkedString + ':' + lineIndex + "'); return true;";
const labelId = 'cb-label-' + id;
const js = `
${postMessageSyntax}('checkboxclick:${checkedString}:${lineIndex}');
const label = document.getElementById("${labelId}");
label.classList.remove(this.checked ? 'checkbox-label-unchecked' : 'checkbox-label-checked');
label.classList.add(this.checked ? 'checkbox-label-checked' : 'checkbox-label-unchecked');
return true;
`;
token = new Token('checkbox_input', 'input', 0);
token.attrs = [
@@ -44,7 +53,11 @@ function createPrefixTokens(Token, id, checked, label, postMessageSyntax, source
tokens.push(token);
token = new Token('label_open', 'label', 1);
token.attrs = [['for', id]];
token.attrs = [
['id', labelId],
['for', id],
['class', 'checkbox-label-' + checkedString],
];
tokens.push(token);
if (label) {

View File

@@ -5,8 +5,14 @@ const utils = require('../utils');
function renderImageHtml(before, src, after, ruleOptions) {
const resourceId = Resource.urlToId(src);
const resource = ruleOptions.resources[resourceId];
if (!resource) return '<div>' + utils.loaderImage() + '</div>';
const result = ruleOptions.resources[resourceId];
const resource = result ? result.item : null;
const resourceStatus = utils.resourceStatus(result);
if (resourceStatus !== 'ready') {
const icon = utils.resourceStatusImage(resourceStatus);
return '<div class="not-loaded-resource resource-status-' + resourceStatus + '" data-resource-id="' + resourceId + '">' + '<img src="data:image/svg+xml;utf8,' + htmlentities(icon) + '"/>' + '</div>';
}
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {

View File

@@ -14,8 +14,14 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
if (!Resource.isResourceUrl(src)) return defaultRender(tokens, idx, options, env, self);
const resourceId = Resource.urlToId(src);
const resource = ruleOptions.resources[resourceId];
if (!resource) return '<div>' + utils.loaderImage() + '</div>';
const result = ruleOptions.resources[resourceId];
const resource = result ? result.item : null;
const resourceStatus = utils.resourceStatus(result);
if (resourceStatus !== 'ready') {
const icon = utils.resourceStatusImage(resourceStatus);
return '<div class="not-loaded-resource resource-status-' + resourceStatus + '" data-resource-id="' + resourceId + '">' + '<img src="data:image/svg+xml;utf8,' + htmlentities(icon) + '"/>' + '</div>';
}
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {

View File

@@ -18,9 +18,19 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
let hrefAttr = '#';
if (isResourceUrl) {
const resourceId = Resource.pathToId(href);
href = "joplin://" + resourceId;
resourceIdAttr = "data-resource-id='" + resourceId + "'";
icon = '<span class="resource-icon"></span>';
const result = ruleOptions.resources[resourceId];
const resource = result ? result.item : null;
const resourceStatus = utils.resourceStatus(result);
if (resourceStatus !== 'ready') {
const icon = utils.resourceStatusFile(resourceStatus);
return '<a class="not-loaded-resource resource-status-' + resourceStatus + '" data-resource-id="' + resourceId + '">' + '<img src="data:image/svg+xml;utf8,' + htmlentities(icon) + '"/>';
} else {
href = "joplin://" + resourceId;
resourceIdAttr = "data-resource-id='" + resourceId + "'";
icon = '<span class="resource-icon"></span>';
}
} else {
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
// link. This allows the link to be exported too when exporting to PDF.
@@ -29,7 +39,6 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
let js = ruleOptions.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
if (hrefAttr.indexOf('#') === 0 && href.indexOf('#') === 0) js = ''; // If it's an internal anchor, don't add any JS since the webview is going to handle navigating to the right place
// if (js) hrefAttr = '#';
return "<a data-from-md " + resourceIdAttr + " title='" + htmlentities(title) + "' href='" + hrefAttr + "' onclick='" + js + "'>" + icon;
};
}

View File

@@ -1,14 +1,89 @@
module.exports = {
getAttr: function(attrs, name, defaultValue = null) {
for (let i = 0; i < attrs.length; i++) {
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
const Resource = require('lib/models/Resource.js');
const utils = {};
utils.getAttr = function(attrs, name, defaultValue = null) {
for (let i = 0; i < attrs.length; i++) {
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
}
return defaultValue;
}
utils.notDownloadedResource = function() {
return `
<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
<path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
</svg>
`;
}
utils.notDownloadedImage = function() {
// https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/file-image-o.svg
// Height changed to 1795
return `
<svg width="1925" height="1792" xmlns="http://www.w3.org/2000/svg">
<path d="M640 576c0 106-86 192-192 192s-192-86-192-192 86-192 192-192 192 86 192 192zm1024 384v448H256v-192l320-320 160 160 512-512zm96-704H160c-17 0-32 15-32 32v1216c0 17 15 32 32 32h1600c17 0 32-15 32-32V288c0-17-15-32-32-32zm160 32v1216c0 88-72 160-160 160H160c-88 0-160-72-160-160V288c0-88 72-160 160-160h1600c88 0 160 72 160 160z"/>
</svg>
`;
}
utils.notDownloadedFile = function() {
// https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/file-o.svg
return `
<svg width="1925" height="1792" xmlns="http://www.w3.org/2000/svg">
<path d="M1468 380c37 37 68 111 68 164v1152c0 53-43 96-96 96H96c-53 0-96-43-96-96V96C0 43 43 0 96 0h896c53 0 127 31 164 68zm-444-244v376h376c-6-17-15-34-22-41l-313-313c-7-7-24-16-41-22zm384 1528V640H992c-53 0-96-43-96-96V128H128v1536h1280z"/>
</svg>
`;
}
utils.errorImage = function() {
// https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/times-circle.svg
return `
<svg width="1795" height="1795" xmlns="http://www.w3.org/2000/svg">
<path d="M1149 1122c0-17-7-33-19-45L949 896l181-181c12-12 19-28 19-45s-7-34-19-46l-90-90c-12-12-29-19-46-19s-33 7-45 19L768 715 587 534c-12-12-28-19-45-19s-34 7-46 19l-90 90c-12 12-19 29-19 46s7 33 19 45l181 181-181 181c-12 12-19 28-19 45s7 34 19 46l90 90c12 12 29 19 46 19s33-7 45-19l181-181 181 181c12 12 28 19 45 19s34-7 46-19l90-90c12-12 19-29 19-46zm387-226c0 424-344 768-768 768S0 1320 0 896s344-768 768-768 768 344 768 768z"/>
</svg>
`;
}
utils.loaderImage = function() {
return '<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="16px" height="16px" viewBox="0 0 128 128" xml:space="preserve"><g><circle cx="16" cy="64" r="16" fill="#000000" fill-opacity="1"/><circle cx="16" cy="64" r="16" fill="#555555" fill-opacity="0.67" transform="rotate(45,64,64)"/><circle cx="16" cy="64" r="16" fill="#949494" fill-opacity="0.42" transform="rotate(90,64,64)"/><circle cx="16" cy="64" r="16" fill="#cccccc" fill-opacity="0.2" transform="rotate(135,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(180,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(225,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(270,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(315,64,64)"/><animateTransform attributeName="transform" type="rotate" values="0 64 64;315 64 64;270 64 64;225 64 64;180 64 64;135 64 64;90 64 64;45 64 64" calcMode="discrete" dur="720ms" repeatCount="indefinite"></animateTransform></g></svg>';
}
utils.resourceStatusImage = function(state) {
if (state === 'notDownloaded') return utils.notDownloadedResource();
return utils.resourceStatusFile(state);
}
utils.resourceStatusFile = function(state) {
if (state === 'notDownloaded') return utils.notDownloadedResource();
if (state === 'downloading') return utils.loaderImage();
if (state === 'encrypted') return utils.loaderImage();
if (state === 'error') return utils.errorImage();
throw new Error('Unknown state: ' + state);
}
utils.resourceStatus = function(resourceInfo, localState) {
let resourceStatus = 'ready';
if (resourceInfo) {
const resource = resourceInfo.item;
const localState = resourceInfo.localState;
if (localState.fetch_status === Resource.FETCH_STATUS_IDLE) {
resourceStatus = 'notDownloaded';
} else if (localState.fetch_status === Resource.FETCH_STATUS_STARTED) {
resourceStatus = 'downloading';
} else if (localState.fetch_status === Resource.FETCH_STATUS_DONE) {
if (resource.encryption_blob_encrypted || resource.encryption_applied) {
resourceStatus = 'encrypted';
}
}
return defaultValue;
},
} else {
resourceStatus = 'notDownloaded';
}
loaderImage: function() {
return '<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="16px" height="16px" viewBox="0 0 128 128" xml:space="preserve"><g><circle cx="16" cy="64" r="16" fill="#000000" fill-opacity="1"/><circle cx="16" cy="64" r="16" fill="#555555" fill-opacity="0.67" transform="rotate(45,64,64)"/><circle cx="16" cy="64" r="16" fill="#949494" fill-opacity="0.42" transform="rotate(90,64,64)"/><circle cx="16" cy="64" r="16" fill="#cccccc" fill-opacity="0.2" transform="rotate(135,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(180,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(225,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(270,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(315,64,64)"/><animateTransform attributeName="transform" type="rotate" values="0 64 64;315 64 64;270 64 64;225 64 64;180 64 64;135 64 64;90 64 64;45 64 64" calcMode="discrete" dur="720ms" repeatCount="indefinite"></animateTransform></g></svg>';
},
return resourceStatus;
}
};
module.exports = utils;

View File

@@ -1,7 +1,33 @@
const webviewLib = {};
let manualDownloadResourceElements = [];
webviewLib.onUnloadedResourceClick = function(event) {
const resourceId = event.currentTarget.getAttribute('data-resource-id');
webviewLib.options_.postMessage('markForDownload:' + resourceId);
}
webviewLib.setupResourceManualDownload = function() {
for (const element of manualDownloadResourceElements) {
element.style.cursor = 'default';
element.removeEventListener('click', webviewLib.onUnloadedResourceClick);
}
manualDownloadResourceElements = [];
const elements = document.getElementsByClassName('resource-status-notDownloaded');
for (const element of elements) {
element.style.cursor = 'pointer';
element.addEventListener('click', webviewLib.onUnloadedResourceClick);
manualDownloadResourceElements.push(element);
}
}
webviewLib.handleInternalLink = function(event, anchorNode) {
const href = anchorNode.getAttribute('href');
if (!href) return false;
if (href.indexOf('#') === 0) {
event.preventDefault();
let old_hash = location.hash;
@@ -18,6 +44,7 @@ webviewLib.handleInternalLink = function(event, anchorNode) {
setTimeout(function() { location.hash = old_hash; }, 10);
return true;
}
return false;
}
@@ -52,9 +79,8 @@ document.addEventListener('click', function(event) {
// as Katex.
if (!anchor.hasAttribute('data-from-md')) {
if (webviewLib.handleInternalLink(event, anchor)) return;
event.preventDefault();
webviewLib.options_.postMessage(anchor.getAttribute('href'));
if (anchor.getAttribute('href')) webviewLib.options_.postMessage(anchor.getAttribute('href'));
return;
}
@@ -63,3 +89,4 @@ document.addEventListener('click', function(event) {
if (webviewLib.handleInternalLink(event, anchor)) return;
}
});

View File

@@ -104,11 +104,19 @@ class NoteBodyViewer extends Component {
let result = this.mdToHtml_.render(bodyToRender, this.props.webViewStyle, mdOptions);
let html = result.html;
const resourceDownloadMode = Setting.value('sync.resourceDownloadMode');
const injectedJs = [this.mdToHtml_.injectedJavaScript()];
injectedJs.push(shim.injectedJs('webviewLib'));
injectedJs.push('webviewLib.initialize({ postMessage: postMessage });');
console.info(injectedJs);
injectedJs.push(`
const readyStateCheckInterval = setInterval(function() {
if (document.readyState === "complete") {
clearInterval(readyStateCheckInterval);
if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload();
}
}, 10);
`);
html = `
<!DOCTYPE html>
@@ -172,9 +180,10 @@ class NoteBodyViewer extends Component {
if (msg.indexOf('checkboxclick:') === 0) {
const newBody = shared.toggleCheckbox(msg, this.props.note.body);
if (this.props.onCheckboxChange) this.props.onCheckboxChange(newBody);
} else if (msg.indexOf('bodyscroll:') === 0) {
//msg = msg.split(':');
//this.bodyScrollTop_ = Number(msg[1]);
} else if (msg.indexOf('markForDownload:') === 0) {
msg = msg.split(':');
const resourceId = msg[1];
if (this.props.onMarkForDownload) this.props.onMarkForDownload({ resourceId: resourceId });
} else {
this.props.onJoplinLinkClick(msg);
}

View File

@@ -164,10 +164,11 @@ class NoteScreenComponent extends BaseScreenComponent {
}
}
this.resourceFetcher_downloadComplete = async (resource) => {
this.refreshResource = async (resource) => {
if (!this.state.note || !this.state.note.body) return;
const resourceIds = await Note.linkedResourceIds(this.state.note.body);
if (resourceIds.indexOf(resource.id) >= 0 && this.refs.noteBodyViewer) {
shared.clearResourceCache();
const attachedResources = await shared.attachedResources(this.state.note.body);
this.setState({ noteResources: attachedResources }, () => {
this.refs.noteBodyViewer.rebuildMd();
@@ -178,6 +179,7 @@ class NoteScreenComponent extends BaseScreenComponent {
this.takePhoto_onPress = this.takePhoto_onPress.bind(this);
this.cameraView_onPhoto = this.cameraView_onPhoto.bind(this);
this.cameraView_onCancel = this.cameraView_onCancel.bind(this);
this.onMarkForDownload = this.onMarkForDownload.bind(this);
}
styles() {
@@ -235,13 +237,23 @@ class NoteScreenComponent extends BaseScreenComponent {
BackButtonService.addHandler(this.backHandler);
NavService.addHandler(this.navHandler);
ResourceFetcher.instance().on('downloadComplete', this.resourceFetcher_downloadComplete);
shared.clearResourceCache();
shared.installResourceHandling(this.refreshResource);
await shared.initState(this);
if (this.state.note && this.state.note.body && Setting.value('sync.resourceDownloadMode') === 'auto') {
const resourceIds = await Note.linkedResourceIds(this.state.note.body);
await ResourceFetcher.instance().markForDownload(resourceIds);
}
this.refreshNoteMetadata();
}
onMarkForDownload(event) {
ResourceFetcher.instance().markForDownload(event.resourceId);
}
refreshNoteMetadata(force = null) {
return shared.refreshNoteMetadata(this, force);
}
@@ -250,7 +262,7 @@ class NoteScreenComponent extends BaseScreenComponent {
BackButtonService.removeHandler(this.backHandler);
NavService.removeHandler(this.navHandler);
ResourceFetcher.instance().off('downloadComplete', this.resourceFetcher_downloadComplete);
shared.uninstallResourceHandling(this.refreshResource);
if (Platform.OS !== 'ios' && this.state.fromShare) {
ShareExtension.close();
@@ -626,6 +638,7 @@ class NoteScreenComponent extends BaseScreenComponent {
highlightedKeywords={keywords}
theme={this.props.theme}
onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}
onMarkForDownload={this.onMarkForDownload}
onLoadEnd={() => {
setTimeout(() => {
this.setState({ HACK_webviewLoadingState: 1 });

View File

@@ -3,6 +3,8 @@ const Folder = require('lib/models/Folder.js');
const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const Resource = require('lib/models/Resource.js');
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
const Setting = require('lib/models/Setting.js');
const Mutex = require('async-mutex').Mutex;
@@ -154,7 +156,11 @@ shared.noteComponent_change = function(comp, propName, propValue) {
comp.setState(newState);
}
const resourceCache_ = {};
let resourceCache_ = {};
shared.clearResourceCache = function() {
resourceCache_ = {};
}
shared.attachedResources = async function(noteBody) {
if (!noteBody) return {};
@@ -163,14 +169,20 @@ shared.attachedResources = async function(noteBody) {
const output = {};
for (let i = 0; i < resourceIds.length; i++) {
const id = resourceIds[i];
if (resourceCache_[id]) {
output[id] = resourceCache_[id];
} else {
const resource = await Resource.load(id);
const isReady = await Resource.isReady(resource);
if (!isReady) continue;
resourceCache_[id] = resource;
output[id] = resource;
const localState = await Resource.localState(resource);
const o = {
item: resource,
localState: localState,
};
resourceCache_[id] = o;
output[id] = o;
}
}
@@ -269,4 +281,16 @@ shared.toggleCheckbox = function(ipcMessage, noteBody) {
return newBody.join('\n')
}
shared.installResourceHandling = function(refreshResourceHandler) {
ResourceFetcher.instance().on('downloadComplete', refreshResourceHandler);
ResourceFetcher.instance().on('downloadStarted', refreshResourceHandler);
DecryptionWorker.instance().on('resourceDecrypted', refreshResourceHandler);
}
shared.uninstallResourceHandling = function(refreshResourceHandler) {
ResourceFetcher.instance().off('downloadComplete', refreshResourceHandler);
ResourceFetcher.instance().off('downloadStarted', refreshResourceHandler);
DecryptionWorker.instance().off('resourceDecrypted', refreshResourceHandler);
}
module.exports = shared;

View File

@@ -1,6 +1,7 @@
const Setting = require('lib/models/Setting');
const Tag = require('lib/models/Tag');
const { reg } = require('lib/registry.js');
const ResourceFetcher = require('lib/services/ResourceFetcher');
const reduxSharedMiddleware = async function(store, next, action) {
const newState = store.getState();
@@ -15,6 +16,10 @@ const reduxSharedMiddleware = async function(store, next, action) {
reg.resetSyncTarget();
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'sync.resourceDownloadMode') {
ResourceFetcher.instance().autoAddResources();
}
if (action.type == 'NOTE_DELETE') {
refreshTags = true;
}

View File

@@ -207,13 +207,13 @@ class SideMenuContentComponent extends Component {
let resourceFetcherText = '';
if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) {
resourceFetcherText = _('Fetching resources: %d', this.props.resourceFetcher.toFetchCount);
resourceFetcherText = _('Fetching resources: %d/%d', this.props.resourceFetcher.fetchingCount, this.props.resourceFetcher.toFetchCount);
}
let fullReport = [];
if (syncReportText) fullReport.push(syncReportText);
// if (fullReport.length) fullReport.push('');
if (resourceFetcherText) lines.push(resourceFetcherText);
if (resourceFetcherText) fullReport.push(resourceFetcherText);
if (decryptionReportText) fullReport.push(decryptionReportText);
while (fullReport.length < 12) fullReport.push(''); // Add blank lines so that height of report text is fixed and doesn't affect scrolling

View File

@@ -263,7 +263,7 @@ class JoplinDatabase extends Database {
// must be set in the synchronizer too.
// Note: v16 and v17 don't do anything. They were used to debug an issue.
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22];
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
@@ -564,6 +564,21 @@ class JoplinDatabase extends Database {
queries.push('ALTER TABLE sync_items ADD COLUMN item_location INT NOT NULL DEFAULT 1');
}
if (targetVersion == 22) {
const newTableSql = `
CREATE TABLE resources_to_download (
id INTEGER PRIMARY KEY,
resource_id TEXT NOT NULL,
updated_time INT NOT NULL,
created_time INT NOT NULL
);
`;
queries.push(this.sqlStringToLines(newTableSql)[0]);
queries.push('CREATE INDEX resources_to_download_resource_id ON resources_to_download (resource_id)');
queries.push('CREATE INDEX resources_to_download_updated_time ON resources_to_download (updated_time)');
}
queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] });
try {

View File

@@ -458,7 +458,13 @@ class BaseItem extends BaseModel {
const className = classNames[i];
const ItemClass = this.getClass(className);
const whereSql = className === 'Resource' ? ['(encryption_blob_encrypted = 1 OR encryption_applied = 1)'] : ['encryption_applied = 1'];
let whereSql = ['encryption_applied = 1'];
if (className === 'Resource') {
const blobDownloadedButEncryptedSql = 'encryption_blob_encrypted = 1 AND id IN (SELECT resource_id FROM resource_local_states WHERE fetch_status = 2))';
whereSql = ['(encryption_applied = 1 OR (' + blobDownloadedButEncryptedSql + ')'];
}
if (exclusions.length) whereSql.push('id NOT IN ("' + exclusions.join('","') + '")');
const sql = sprintf(`

View File

@@ -32,15 +32,14 @@ class Resource extends BaseItem {
return imageMimeTypes.indexOf(type.toLowerCase()) >= 0;
}
static needToBeFetched(limit = null) {
let sql = 'SELECT * FROM resources WHERE id IN (SELECT resource_id FROM resource_local_states WHERE fetch_status = ?) ORDER BY updated_time DESC';
if (limit !== null) sql += ' LIMIT ' + limit;
return this.modelSelectAll(sql, [Resource.FETCH_STATUS_IDLE]);
}
static async needToBeFetchedCount() {
const r = await this.db().selectOne('SELECT count(*) as total FROM resource_local_states WHERE fetch_status = ?', [Resource.FETCH_STATUS_IDLE]);
return r ? r['total'] : 0;
static needToBeFetched(resourceDownloadMode = null, limit = null) {
let sql = ['SELECT * FROM resources WHERE encryption_applied = 0 AND id IN (SELECT resource_id FROM resource_local_states WHERE fetch_status = ?)'];
if (resourceDownloadMode !== 'always') {
sql.push('AND resources.id IN (SELECT resource_id FROM resources_to_download)');
}
sql.push('ORDER BY updated_time DESC');
if (limit !== null) sql.push('LIMIT ' + limit);
return this.modelSelectAll(sql.join(' '), [Resource.FETCH_STATUS_IDLE]);
}
static async resetStartedFetchStatus() {
@@ -52,13 +51,6 @@ class Resource extends BaseItem {
return Resource.fsDriver_;
}
static filename(resource, encryptedBlob = false) {
let extension = encryptedBlob ? 'crypted' : resource.file_extension;
if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : '';
extension = extension ? ('.' + extension) : '';
return resource.id + extension;
}
static friendlyFilename(resource) {
let output = safeFilename(resource.title); // Make sure not to allow spaces or any special characters as it's not supported in HTTP headers
if (!output) output = resource.id;
@@ -76,6 +68,13 @@ class Resource extends BaseItem {
return Setting.value('resourceDirName');
}
static filename(resource, encryptedBlob = false) {
let extension = encryptedBlob ? 'crypted' : resource.file_extension;
if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : '';
extension = extension ? ('.' + extension) : '';
return resource.id + extension;
}
static relativePath(resource, encryptedBlob = false) {
return Setting.value('resourceDirName') + '/' + this.filename(resource, encryptedBlob);
}
@@ -96,6 +95,13 @@ class Resource extends BaseItem {
const decryptedItem = item.encryption_cipher_text ? await super.decrypt(item) : Object.assign({}, item);
if (!decryptedItem.encryption_blob_encrypted) return decryptedItem;
const localState = await this.localState(item);
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE) {
// Not an error - it means the blob has not been downloaded yet.
// It will be decrypted later on, once downloaded.
return decryptedItem;
}
const plainTextPath = this.fullPath(decryptedItem);
const encryptedPath = this.fullPath(decryptedItem, true);
const noExtPath = pathUtils.dirname(encryptedPath) + '/' + pathUtils.filename(encryptedPath);
@@ -109,12 +115,7 @@ class Resource extends BaseItem {
}
try {
// const stat = await this.fsDriver().stat(encryptedPath);
await this.encryptionService().decryptFile(encryptedPath, plainTextPath, {
// onProgress: (progress) => {
// console.info('Decryption: ', progress.doneSize / stat.size);
// },
});
await this.encryptionService().decryptFile(encryptedPath, plainTextPath);
} catch (error) {
if (error.code === 'invalidIdentifier') {
// As the identifier is invalid it most likely means that this is not encrypted data
@@ -149,12 +150,7 @@ class Resource extends BaseItem {
if (resource.encryption_blob_encrypted) return { path: encryptedPath, resource: resource };
try {
// const stat = await this.fsDriver().stat(plainTextPath);
await this.encryptionService().encryptFile(plainTextPath, encryptedPath, {
// onProgress: (progress) => {
// console.info(progress.doneSize / stat.size);
// },
});
await this.encryptionService().encryptFile(plainTextPath, encryptedPath);
} catch (error) {
if (error.code === 'ENOENT') throw new JoplinError('File not found:' + error.toString(), 'fileNotFound');
throw error;
@@ -244,6 +240,20 @@ class Resource extends BaseItem {
await ResourceLocalState.batchDelete(ids);
}
static async markForDownload(resourceId) {
// Insert the row only if it's not already there
const t = Date.now();
await this.db().exec('INSERT INTO resources_to_download (resource_id, updated_time, created_time) SELECT ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM resources_to_download WHERE resource_id = ?)', [resourceId, t, t, resourceId]);
}
static async downloadedButEncryptedBlobCount() {
const r = await this.db().selectOne('SELECT count(*) as total FROM resource_local_states WHERE fetch_status = ? AND resource_id IN (SELECT id FROM resources WHERE encryption_blob_encrypted = 1)', [
Resource.FETCH_STATUS_DONE,
]);
return r ? r.total : 0;
}
}
Resource.IMAGE_MAX_DIMENSION = 1920;

View File

@@ -82,7 +82,7 @@ class Setting extends BaseModel {
}
return options;
}},
'folders.sortOrder.reverse': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
'folders.sortOrder.reverse': { value: false, type: Setting.TYPE_BOOL, public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
'trackLocation': { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Save geo-location with notes') },
'newTodoFocus': { value: 'title', type: Setting.TYPE_STRING, section: 'note', isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new to-do:'), options: () => {
return {
@@ -155,6 +155,14 @@ class Setting extends BaseModel {
return SyncTargetRegistry.idAndLabelPlainObject();
}},
'sync.resourceDownloadMode': { value: 'always', type: Setting.TYPE_STRING, section: 'sync', public: true, isEnum: true, appTypes: ['mobile', 'desktop'], label: () => _('Attachment download behaviour'), description: () => _('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.'), options: () => {
return {
'always': _('Always'),
'manual': _('Manual'),
'auto': _('Auto'),
};
}},
'sync.2.path': { value: '', type: Setting.TYPE_STRING, section:'sync', show: (settings) => {
try {
return settings['sync.target'] == SyncTargetRegistry.nameToId('filesystem')

View File

@@ -487,7 +487,7 @@ const reducer = (state = defaultState, action) => {
newState = Object.assign({}, state);
newState.collapsedFolderIds = action.ids.slice();
break;
case 'TAG_UPDATE_ALL':
newState = Object.assign({}, state);

View File

@@ -2,6 +2,7 @@ const BaseItem = require('lib/models/BaseItem');
const Resource = require('lib/models/Resource');
const ResourceService = require('lib/services/ResourceService');
const { Logger } = require('lib/logger.js');
const EventEmitter = require('events');
class DecryptionWorker {
@@ -14,6 +15,7 @@ class DecryptionWorker {
};
this.scheduleId_ = null;
this.eventEmitter_ = new EventEmitter();
}
setLogger(l) {
@@ -24,6 +26,14 @@ class DecryptionWorker {
return this.logger_;
}
on(eventName, callback) {
return this.eventEmitter_.on(eventName, callback);
}
off(eventName, callback) {
return this.eventEmitter_.removeListener(eventName, callback);
}
static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new DecryptionWorker();
@@ -85,14 +95,6 @@ class DecryptionWorker {
const ItemClass = BaseItem.itemClass(item);
if (item.type_ === Resource.modelType()) {
const ls = await Resource.localState(item);
if (ls.fetch_status !== Resource.FETCH_STATUS_DONE) {
excludedIds.push(item.id);
continue;
}
}
this.dispatchReport({
itemIndex: i,
itemCount: items.length,
@@ -101,10 +103,21 @@ class DecryptionWorker {
// Don't log in production as it results in many messages when importing many items
// this.logger().info('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
try {
await ItemClass.decrypt(item);
const decryptedItem = await ItemClass.decrypt(item);
if (decryptedItem.type_ === Resource.modelType() && !!decryptedItem.encryption_blob_encrypted) {
// itemsThatNeedDecryption() will return the resource again if the blob has not been decrypted,
// but that will result in an infinite loop if the blob simply has not been downloaded yet.
// So skip the ID for now, and the service will try to decrypt the blob again the next time.
excludedIds.push(decryptedItem.id);
}
if (decryptedItem.type_ === Resource.modelType() && !decryptedItem.encryption_blob_encrypted) {
this.eventEmitter_.emit('resourceDecrypted', { id: decryptedItem.id });
}
} catch (error) {
excludedIds.push(item.id);
if (error.code === 'masterKeyNotLoaded' && options.masterKeyNotLoadedHandler === 'dispatch') {
if (notLoadedMasterKeyDisptaches.indexOf(error.masterKeyId) < 0) {
this.dispatch({
@@ -139,9 +152,16 @@ class DecryptionWorker {
this.logger().info('DecryptionWorker: completed decryption.');
const downloadedButEncryptedBlobCount = await Resource.downloadedButEncryptedBlobCount();
this.dispatchReport({ state: 'idle' });
this.state_ = 'idle';
if (downloadedButEncryptedBlobCount) {
this.logger().info('DecryptionWorker: Some resources have been downloaded but are not decrypted yet. Scheduling another decryption. Resource count: ' + downloadedButEncryptedBlobCount);
this.scheduleStart();
}
}
}

View File

@@ -1,4 +1,5 @@
const Resource = require('lib/models/Resource');
const Setting = require('lib/models/Setting');
const BaseService = require('lib/services/BaseService');
const ResourceService = require('lib/services/ResourceService');
const BaseSyncTarget = require('lib/BaseSyncTarget');
@@ -63,23 +64,32 @@ class ResourceFetcher extends BaseService {
}
updateReport() {
if (this.updateReportIID_) return;
this.updateReportIID_ = setTimeout(async () => {
const toFetchCount = await Resource.needToBeFetchedCount();
this.dispatch({
type: 'RESOURCE_FETCHER_SET',
toFetchCount: toFetchCount,
});
this.updateReportIID_ = null;
}, 2000);
const fetchingCount = Object.keys(this.fetchingItems_).length;
this.dispatch({
type: 'RESOURCE_FETCHER_SET',
fetchingCount: fetchingCount,
toFetchCount: fetchingCount + this.queue_.length,
});
}
queueDownload(resourceId, priority = null) {
async markForDownload(resourceIds) {
if (!Array.isArray(resourceIds)) resourceIds = [resourceIds];
for (const id of resourceIds) {
await Resource.markForDownload(id);
}
for (const id of resourceIds) {
this.queueDownload_(id, 'high');
}
}
queueDownload_(resourceId, priority = null) {
if (priority === null) priority = 'normal';
const index = this.queuedItemIndex_(resourceId);
if (index >= 0) return false;
if (this.fetchingItems_[resourceId]) return false;
const item = { id: resourceId };
@@ -99,12 +109,17 @@ class ResourceFetcher extends BaseService {
if (this.fetchingItems_[resourceId]) return;
this.fetchingItems_[resourceId] = true;
this.updateReport();
const resource = await Resource.load(resourceId);
const localState = await Resource.localState(resource);
const completeDownload = async (emitDownloadComplete = true, localResourceContentPath = '') => {
// 2019-05-12: This is only necessary to set the file size of the resources that come via
// sync. The other ones have been done using migrations/20.js. This code can be removed
// after a few months.
if (resource.size < 0 && localResourceContentPath && !resource.encryption_blob_encrypted) {
if (resource && resource.size < 0 && localResourceContentPath && !resource.encryption_blob_encrypted) {
await ResourceService.autoSetFileSizes();
}
@@ -115,12 +130,15 @@ class ResourceFetcher extends BaseService {
// might still be encrypted and the caller usually can't do much with this. In particular
// the note being displayed will refresh the resource images but since they are still
// encrypted it's not useful. Probably, the views should listen to DecryptionWorker events instead.
if (emitDownloadComplete) this.eventEmitter_.emit('downloadComplete', { id: resource.id });
if (resource && emitDownloadComplete) this.eventEmitter_.emit('downloadComplete', { id: resource.id, encrypted: !!resource.encryption_blob_encrypted });
this.updateReport();
}
const resource = await Resource.load(resourceId);
const localState = await Resource.localState(resource);
if (!resource) {
this.logger().info('ResourceFetcher: Attempting to download a resource that does not exist (has been deleted?): ' + resourceId);
await completeDownload(false);
return;
}
// Shouldn't happen, but just to be safe don't re-download the
// resource if it's already been downloaded.
@@ -131,7 +149,7 @@ class ResourceFetcher extends BaseService {
this.fetchingItems_[resourceId] = resource;
const localResourceContentPath = Resource.fullPath(resource);
const localResourceContentPath = Resource.fullPath(resource, !!resource.encryption_blob_encrypted);
const remoteResourceContentPath = this.resourceDirName_ + "/" + resource.id;
await Resource.setLocalState(resource, { fetch_status: Resource.FETCH_STATUS_STARTED });
@@ -140,6 +158,8 @@ class ResourceFetcher extends BaseService {
this.logger().debug('ResourceFetcher: Downloading resource: ' + resource.id);
this.eventEmitter_.emit('downloadStarted', { id: resource.id })
fileApi.get(remoteResourceContentPath, { path: localResourceContentPath, target: "file" }).then(async () => {
await Resource.setLocalState(resource, { fetch_status: Resource.FETCH_STATUS_DONE });
this.logger().debug('ResourceFetcher: Resource downloaded: ' + resource.id);
@@ -166,7 +186,7 @@ class ResourceFetcher extends BaseService {
async waitForAllFinished() {
return new Promise((resolve, reject) => {
const iid = setInterval(() => {
if (!this.queue_.length && !Object.getOwnPropertyNames(this.fetchingItems_).length) {
if (!this.updateReportIID_ && !this.scheduleQueueProcessIID_ && !this.addingResources_ && !this.queue_.length && !Object.getOwnPropertyNames(this.fetchingItems_).length) {
clearInterval(iid);
resolve();
}
@@ -178,10 +198,12 @@ class ResourceFetcher extends BaseService {
if (this.addingResources_) return;
this.addingResources_ = true;
this.logger().info('ResourceFetcher: Auto-add resources: Mode: ' + Setting.value('sync.resourceDownloadMode'));
let count = 0;
const resources = await Resource.needToBeFetched(limit);
const resources = await Resource.needToBeFetched(Setting.value('sync.resourceDownloadMode'), limit);
for (let i = 0; i < resources.length; i++) {
const added = this.queueDownload(resources[i].id);
const added = this.queueDownload_(resources[i].id);
if (added) count++;
}

View File

@@ -113,6 +113,10 @@ class ResourceService extends BaseService {
static async autoSetFileSize(resourceId, filePath) {
const itDoes = await shim.fsDriver().waitTillExists(filePath);
if (!itDoes) {
// this.logger().warn('Trying to set file size on non-existent resource:', resourceId, filePath);
return;
}
const fileStat = await shim.fsDriver().stat(filePath);
await Resource.setFileSizeOnly(resourceId, fileStat.size);
}

View File

@@ -10,6 +10,7 @@ const { shim } = require('lib/shim');
const BaseService = require('lib/services/BaseService');
const { _ } = require('lib/locale.js');
const ArrayUtils = require('lib/ArrayUtils.js');
const { sprintf } = require('sprintf-js');
class RevisionService extends BaseService {
@@ -138,10 +139,12 @@ class RevisionService extends BaseService {
if (note) {
if (oldNote && oldNote.updated_time < this.oldNoteCutOffDate_()) {
// This is where we save the original version of this old note
await this.createNoteRevision_(oldNote);
const rev = await this.createNoteRevision_(oldNote);
if (rev) this.logger().debug(sprintf('RevisionService::collectRevisions: Saved revision %s (old note)', rev.id));
}
await this.createNoteRevision_(note);
const rev = await this.createNoteRevision_(note);
if (rev) this.logger().debug(sprintf('RevisionService::collectRevisions: Saved revision %s (Last rev was more than %d ms ago)', rev.id, Setting.value('revisionService.intervalBetweenRevisions')));
doneNoteIds.push(noteId);
this.isOldNotesCache_[noteId] = false;
}
@@ -150,7 +153,10 @@ class RevisionService extends BaseService {
if (change.type === ItemChange.TYPE_DELETE && !!change.before_change_item) {
const note = JSON.parse(change.before_change_item);
const revExists = await Revision.revisionExists(BaseModel.TYPE_NOTE, note.id, note.updated_time);
if (!revExists) await this.createNoteRevision_(note);
if (!revExists) {
const rev = await this.createNoteRevision_(note);
if (rev) this.logger().debug(sprintf('RevisionService::collectRevisions: Saved revision %s (for deleted note)', rev.id));
}
doneNoteIds.push(noteId);
}

View File

@@ -231,6 +231,11 @@ class Synchronizer {
if (syncSteps.indexOf("update_remote") >= 0) {
let donePaths = [];
const completeItemProcessing = (path) => {
donePaths.push(path);
}
while (true) {
if (this.cancelling()) break;
@@ -290,6 +295,7 @@ class Synchronizer {
if (error.code === 'rejectedByTarget') {
this.progressReport_.errors.push(error);
this.logger().warn('Rejected by target: ' + path + ': ' + error.message);
completeItemProcessing(path);
continue;
} else {
throw error;
@@ -313,18 +319,23 @@ class Synchronizer {
this.logSyncOperation(action, local, remote, reason);
if (local.type_ == BaseModel.TYPE_RESOURCE && (action == "createRemote" || action === "updateRemote" || (action == "itemConflict" && remote))) {
try {
const remoteContentPath = this.resourceDirName_ + "/" + local.id;
const result = await Resource.fullPathForSyncUpload(local);
local = result.resource;
const localResourceContentPath = result.path;
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: "file" });
} catch (error) {
if (error && ["rejectedByTarget", "fileNotFound"].indexOf(error.code) >= 0) {
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
action = null;
} else {
throw error;
const localState = await Resource.localState(local.id);
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE) {
action = null;
} else {
try {
const remoteContentPath = this.resourceDirName_ + "/" + local.id;
const result = await Resource.fullPathForSyncUpload(local);
local = result.resource;
const localResourceContentPath = result.path;
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: "file" });
} catch (error) {
if (error && ["rejectedByTarget", "fileNotFound"].indexOf(error.code) >= 0) {
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
action = null;
} else {
throw error;
}
}
}
}
@@ -422,7 +433,7 @@ class Synchronizer {
}
}
donePaths.push(path);
completeItemProcessing(path);
}
if (!result.hasMore) break;
@@ -491,7 +502,7 @@ class Synchronizer {
break;
}
// this.logSyncOperation("fetchingProcessed", null, null, "Processing fetched item");
this.logSyncOperation("fetchingProcessed", null, null, "Processing fetched item");
let remote = remotes[i];
if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder

View File

@@ -4,10 +4,7 @@ module.exports = {
"id": "8a1556e382704160808e9a7bef7135d3",
"title": "1. Welcome to Joplin! 🗒️",
"body": "# Welcome to Joplin! 🗒️\n\nJoplin is a free, open source note taking and to-do application, which helps you write and organise your notes, and synchronise them between your devices. 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](https://joplinapp.org/#markdown). Joplin is available as a **💻 desktop**, **📱 mobile** and **🔡 terminal** application.\n\nThe notes in this notebook give an overview of what Joplin can do and how to use it. In general, the three applications share roughly the same functionalities; any differences will be clearly indicated.\n\n![](./AllClients.png)\n\n## Joplin is divided into three parts\n\nJoplin has three main columns:\n\n- **Sidebar** contains the list of your notebooks and tags, as well as the synchronisation status.\n- **Note List** contains the current list of notes - either the notes in the currently selected notebook, the notes in the currently selected tag, or search results.\n- **Note Editor** is the place where you write your notes in Markdown, with a viewer showing what the note will look like. You may also use an [external editor](https://joplinapp.org/#external-text-editor) to edit notes. For example, if you like WYSIWYG editors, you can use something like Typora as an external editor and it will display the note as well as any embedded images.\n\n## Writing notes in Markdown\n\nMarkdown is a lightweight markup language with plain text formatting syntax. Joplin supports a [Github-flavoured Markdown syntax](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions.\n\nIn general, while Markdown is a markup language, it is meant to be human readable, even without being rendered. This is a simple example (you can see how it looks in the viewer panel):\n\n* * *\n\n# Heading\n\n## Sub-heading\n\nParagraphs are separated by a blank line. Text attributes _italic_, **bold** and `monospace` are supported. You can create bullet lists:\n\n* apples\n* oranges\n* pears\n\nOr numbered lists:\n\n1. wash\n2. rinse\n3. repeat\n\nThis is a [link](https://joplinapp.org) and, finally, below is a horizontal rule:\n\n* * *\n\nA lot more is possible including adding code samples, math formulae or checkbox lists - see the [Markdown documentation](https://joplinapp.org/#markdown) for more information.\n\n## Organising your notes\n\n### With notebooks 📔\n\nJoplin notes are organised into a tree of notebooks and sub-notebooks.\n\n- On **desktop**, you can create a notebook by clicking on New Notebook, then you can drag and drop them into other notebooks to organise them as you wish.\n- On **mobile**, press the \"+\" icon and select \"New notebook\".\n- On **terminal**, press `:mn`\n\n![](./SubNotebooks.png)\n\n### With tags 🏷️\n\nThe second way to organise your notes is using tags:\n\n- On **desktop**, right-click on any note in the Note List, and select \"Edit tags\". You can then add the tags, separating them by commas.\n- On **mobile**, open the note and press the \"⋮\" button and select \"Tags\".\n- On **terminal**, type `:help tag` for the available commands.\n",
"tags": [
"markdown",
"organising"
],
"tags": [],
"resources": {
"./AllClients.png": {
"id": "5c05172554194f95b60971f6d577cc1a",
@@ -24,10 +21,7 @@ module.exports = {
"id": "b863cbc514cb4cafbae8dd6a4fcad919",
"title": "2. Importing and exporting notes ↔️",
"body": "# Importing and exporting notes ↔️\n\n## Importing from Evernote\n\nJoplin was designed as a replacement for Evernote and so can import complete Evernote notebooks, as well as notes, tags, images, attached files and note metadata (such as author, geo-location, etc.) via ENEX files.\n\nTo import Evernote data, first export your Evernote notebooks to ENEX files as described [here](https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks). Then, on **desktop**, do the following: Open File > Import > ENEX and select your file. The notes will be imported into a new separate notebook. If needed they can then be moved to a different notebook, or the notebook can be renamed, etc. Read [more about Evernote import](https://joplinapp.org/#importing-from-evernote).\n\n# Importing from other apps\n\nJoplin can also import notes from [many other apps](https://github.com/laurent22/joplin#importing-from-other-applications) as well as [from Markdown or text files](https://github.com/laurent22/joplin#importing-from-markdown-files).\n\n# Exporting notes\n\nJoplin can export to the JEX format (Joplin Export file), which is an archive that can contain multiple notes, notebooks, etc. This is a format mostly designed for backup purposes. You may also export to other formats such as plain Markdown files, to JSON or to PDF. Find out [more about exporting notes](https://github.com/laurent22/joplin#exporting) on the official website.",
"tags": [
"importing",
"exporting"
],
"tags": [],
"resources": {},
"parent_id": "9bb5d498aba74cc6a047cfdc841e82a1"
},
@@ -35,20 +29,15 @@ module.exports = {
"id": "25b656aac0564d1a91ab98295aa3cc58",
"title": "3. Synchronising your notes 🔄",
"body": "# Synchronising your notes 🔄\n\nOne of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. You basically choose the service you prefer among those supported, setup the configuration, and the app will be able to sync between your computers or mobile devices.\n\nThe supported cloud services are the following:\n\n## Setting up Dropbox synchronisation\n\nSelect \"Dropbox\" as the synchronisation target in the config screen (it is selected by default). Then, to initiate the synchronisation process, click on the \"Synchronise\" button in the sidebar and follow the instructions.\n\n## Setting up Nextcloud synchronisation\n\nNextcloud is a self-hosted, private cloud solution. It can store documents, images and videos but also calendars, passwords and countless other things and can sync them to your laptop or phone. As you can host your own Nextcloud server, you own both the data on your device and infrastructure used for synchronisation. As such it is a good fit for Joplin.\n\nTo set it up, go to the config screen and select Nextcloud as the synchronisation target. Then input the WebDAV URL (to get it, go to your Nextcloud page, click on Settings in the bottom left corner of the page and copy the URL). Note that it has to be the **full URL**, so for example if you want the notes to be under `/Joplin`, the URL would be something like `https://example.com/remote.php/webdav/Joplin` (note that \"/Joplin\" part). And **make sure to create the \"/Joplin\" directory in Nextcloud**. Finally set the username and password. If it does not work, please [see this explanation](https://github.com/laurent22/joplin/issues/61#issuecomment-373282608) for more details.\n\n## Setting up OneDrive or WebDAV synchronisation\n\nOneDrive and WebDAV are also supported as synchronisation services. Please see [the export documentation](https://github.com/laurent22/joplin#exporting) for more information.\n\n## Using End-To-End Encryption\n\nJoplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a system where only the owner of the data can read it. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data. Please see the [End-To-End Encryption Tutorial](https://joplinapp.org/e2ee/) for more information about this feature and how to enable it.",
"tags": [
"synchronising"
],
"tags": [],
"resources": {},
"parent_id": "9bb5d498aba74cc6a047cfdc841e82a1"
},
{
"id": "2ee48f80889447429a3cccb04a466072",
"title": "4. Tips 💡",
"body": "# Tips 💡\n\nThe first few notes should have given you an overview of the main functionalities of Joplin, but there's more it can do. See below for some of these features and how to get more help using the app:\n\n## Web clipper\n\n![](./WebClipper.png)\n\nThe Web Clipper is a browser extension that allows you to save web pages and screenshots from your browser. To start using it, open the Joplin desktop application, go to the Web Clipper Options and follow the instructions.\n\nMore info on the official website: https://joplinapp.org/clipper/\n\n## Attachments\n\nAny kind of file can be attached to a note. In Markdown, links to these files are represented as an ID. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.\n\nImages can be attached either by clicking on \"Attach file\" or by pasting (with `Ctrl+V` or `Cmd+V`) an image directly in the editor, or by drag and dropping an image.\n\nMore info about attachments: https://joplinapp.org#attachments--resources\n\n## Search\n\nJoplin supports advanced search queries, which are fully documented on the official website: https://joplinapp.org#searching\n\n## Alarms\n\nAn alarm can be associated with any to-do. It will be triggered at the given time by displaying a notification. To use this feature, see the documentation: https://joplinapp.org#notifications\n\n## Markdown advanced tips\n\nJoplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions.\n\nFor example, tables are supported:\n\n| Tables | Are | Cool |\n| ------------- |:-------------:| -----:|\n| col 3 is | right-aligned | $1600 |\n| col 2 is | centered | $12 |\n| zebra stripes | are neat | $1 |\n\nYou can also create lists of checkboxes. These checkboxes can be ticked directly in the viewer, or by adding an \"x\" inside:\n\n- [ ] Milk\n- [ ] Eggs\n- [x] Beer\n\nMath expressions can be added using the [KaTeX notation](https://khan.github.io/KaTeX/):\n\n$$\nf(x) = \\int_{-\\infty}^\\infty\n \\hat f(\\xi)\\,e^{2 \\pi i \\xi x}\n \\,d\\xi\n$$\n\nVarious other tricks are possible, such as using HTML, or customising the CSS. See the Markdown documentation for more info - https://joplinapp.org#markdown\n\n## Community and further help\n\n- For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplin.cozic.net/). It is possible to login with your GitHub account.\n- The latest news are posted [on the Patreon page](https://www.patreon.com/joplin).\n- For bug reports and feature requests, go to the [GitHub Issue Tracker](https://github.com/laurent22/joplin/issues).\n\n## Donations\n\nDonations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.\n\nPlease see the [donation page](https://joplinapp.org/donate/) for information on how to support the development of Joplin.",
"tags": [
"attachment",
"search"
],
"body": "# Tips 💡\n\nThe first few notes should have given you an overview of the main functionalities of Joplin, but there's more it can do. See below for some of these features and how to get more help using the app:\n\n## Web clipper\n\n![](./WebClipper.png)\n\nThe Web Clipper is a browser extension that allows you to save web pages and screenshots from your browser. To start using it, open the Joplin desktop application, go to the Web Clipper Options and follow the instructions.\n\nMore info on the official website: https://joplinapp.org/clipper/\n\n## Attachments\n\nAny kind of file can be attached to a note. In Markdown, links to these files are represented as an ID. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.\n\nImages can be attached either by clicking on \"Attach file\" or by pasting (with `Ctrl+V` or `Cmd+V`) an image directly in the editor, or by drag and dropping an image.\n\nMore info about attachments: https://joplinapp.org#attachments--resources\n\n## Search\n\nJoplin supports advanced search queries, which are fully documented on the official website: https://joplinapp.org#searching\n\n## Alarms\n\nAn alarm can be associated with any to-do. It will be triggered at the given time by displaying a notification. To use this feature, see the documentation: https://joplinapp.org#notifications\n\n## Markdown advanced tips\n\nJoplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions.\n\nFor example, tables are supported:\n\n| Tables | Are | Cool |\n| ------------- |:-------------:| -----:|\n| col 3 is | right-aligned | $1600 |\n| col 2 is | centered | $12 |\n| zebra stripes | are neat | $1 |\n\nYou can also create lists of checkboxes. These checkboxes can be ticked directly in the viewer, or by adding an \"x\" inside:\n\n- [ ] Milk\n- [ ] Eggs\n- [x] Beer\n\nMath expressions can be added using the [KaTeX notation](https://khan.github.io/KaTeX/):\n\n$$\nf(x) = \\int_{-\\infty}^\\infty\n \\hat f(\\xi)\\,e^{2 \\pi i \\xi x}\n \\,d\\xi\n$$\n\nVarious other tricks are possible, such as using HTML, or customising the CSS. See the Markdown documentation for more info - https://joplinapp.org#markdown\n\n## Community and further help\n\n- For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplinapp.org/). It is possible to login with your GitHub account.\n- The latest news are posted [on the Patreon page](https://www.patreon.com/joplin).\n- For bug reports and feature requests, go to the [GitHub Issue Tracker](https://github.com/laurent22/joplin/issues).\n\n## Donations\n\nDonations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.\n\nPlease see the [donation page](https://joplinapp.org/donate/) for information on how to support the development of Joplin.",
"tags": [],
"resources": {
"./WebClipper.png": {
"id": "30cf9214f5054c4da3b23eed7211a6e0",
@@ -64,35 +53,6 @@ module.exports = {
"title": "Welcome!"
}
],
"tags": [
{
"id": "79cc5ef0f6c24f138033ce48928c2cba",
"title": "markdown"
},
{
"id": "c83be0495b5d4f1ab655c5c6dfed6804",
"title": "organising"
},
{
"id": "b5adb734bb0044f2a572a729266b610d",
"title": "importing"
},
{
"id": "bed34e2e3ab74b45af8ba473a05f56f9",
"title": "exporting"
},
{
"id": "c442fa3b2b2b4389b160c15eb73f35c9",
"title": "synchronising"
},
{
"id": "22c94167b6e94a92b560ffc31a7f4b1d",
"title": "attachment"
},
{
"id": "83eae47427df4805905103d4a91727b7",
"title": "search"
}
],
"tags": [],
"timestamp": 1529668800000
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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