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

Compare commits

..

89 Commits

Author SHA1 Message Date
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
Laurent Cozic
9de7c15e93 CLI v1.0.128 2019-05-13 22:53:08 +01:00
Laurent Cozic
61736546b4 Updated translations 2019-05-13 22:52:42 +01:00
Laurent Cozic
82b6dd23a7 CLI v1.0.127 2019-05-13 10:11:10 +01:00
Laurent Cozic
64427f0160 iOS 10.0.34 2019-05-13 10:10:37 +01:00
Laurent Cozic
cbf47cb9ee Android release v1.0.252 2019-05-13 09:53:50 +01:00
Laurent Cozic
dba3e4202d Electron release v1.0.152 2019-05-13 09:51:04 +01:00
Laurent Cozic
173cd6de4d Fixed regression 2019-05-13 09:50:39 +01:00
Laurent Cozic
3d333bd8f2 Removed build files 2019-05-13 00:43:12 +01:00
Laurent Cozic
a82f8c7dd0 CLI v1.0.126 2019-05-13 00:42:16 +01:00
Helmut K. C. Tessarek
cde079e44e Clipper release v1.0.14 2019-05-12 17:52:20 -04:00
Laurent Cozic
f8d20b61ea Update website 2019-05-12 16:18:30 +01:00
Laurent Cozic
d279435502 Android release v1.0.251 2019-05-12 16:16:39 +01:00
Laurent Cozic
eb2065128e Electron release v1.0.151 2019-05-12 16:04:01 +01:00
Laurent Cozic
10e81aa476 Trying to fix iOS release issue 2019-05-12 16:03:22 +01:00
Laurent Cozic
3e808f05fd Improved logic to set resource file size 2019-05-12 15:53:42 +01:00
Laurent Cozic
e57bfad9b1 Android release v1.0.248 2019-05-12 11:50:21 +01:00
Laurent Cozic
5f344f07d4 Electron release v1.0.150 2019-05-12 11:41:35 +01:00
Laurent Cozic
e1b7b64e1b All: Make sure resource filesize is set in all cases 2019-05-12 11:41:07 +01:00
Laurent Cozic
ed3970be81 CLI: Fix: Do not resize images if they are already below the max dimensions 2019-05-12 11:38:33 +01:00
Laurent Cozic
c27861d40f Electron release v1.0.149 2019-05-12 01:22:02 +01:00
Laurent Cozic
565dfba8c9 All: Fixes #371 (sort of): Allow resources greater than 10 MB but they won't be synced on mobile 2019-05-12 01:15:52 +01:00
Laurent Cozic
553a26eb63 Desktop, CLI: Added option to disable creation of welcome items 2019-05-12 01:10:46 +01:00
Laurent Cozic
9c85bc2cd1 All: Save size of a resource to the database; and added mechanism to run non-database migrations 2019-05-11 17:55:40 +01:00
Laurent Cozic
e96bc9c48a All: Allow specifying the log level of a log target 2019-05-11 17:53:56 +01:00
Laurent Cozic
0d036d8183 Fixed regression following fix for #1425 2019-05-11 17:35:39 +01:00
Laurent Cozic
e5f2a7f2f5 Fixed regression caused by #1472 2019-05-11 17:34:45 +01:00
Laurent Cozic
016ce3dd61 Desktop: Resolves #1502: Improved note deletion dialog 2019-05-11 13:36:44 +01:00
Laurent Cozic
afb375955e Fixed regressions following fix for #1425 2019-05-11 12:08:28 +01:00
Laurent Cozic
b702b0b40c Desktop: Fixes #1425: Improved handling of images when using external editor, so that it works in Atom, VSCode and Typora 2019-05-11 11:46:13 +01:00
Laurent Cozic
91ecab51c5 Clipper: Fixed: Added Chrome workaround to prevent it from posting the same note twice 2019-05-11 11:18:09 +01:00
Laurent Cozic
7628506926 Merge branch 'master' of github.com:laurent22/joplin 2019-05-11 11:13:23 +01:00
Laurent Cozic
4e7f7c0c9c Clipper: Fixes #1510: Fixed display of some images in preview. Also display images at correct size inside preview. 2019-05-11 11:13:13 +01:00
Laurent Cozic
863f5bcf18 Desktop, Mobile: Fixed: Some images were not being displayed 2019-05-11 09:49:56 +01:00
Laurent Cozic
dccd489fcc Clipper: Fix: Fix handling of tables with empty headers 2019-05-11 09:23:31 +01:00
Helmut K. C. Tessarek
333c3f6369 added .gitignore to /Tools 2019-05-10 20:19:42 -04:00
Helmut K. C. Tessarek
808413d0bf remove section self-signed certs from FAQ for now - no Android rel available that has this feature yet 2019-05-10 03:19:47 -04:00
Helmut K. C. Tessarek
440be3d920 remove unintentionally committed files again 2019-05-09 23:01:49 -04:00
Helmut K. C. Tessarek
9b27a4f601 Update website 2019-05-09 22:58:56 -04:00
Don Bowman
6e36ca32b4 CLI: Fix: Bump sqlite3 to v4.0.7 for node12 support (#1508) 2019-05-10 01:18:04 +01:00
simonsan
85a9c303f2 Changed download links to v1.0.145 (#1507) 2019-05-10 01:17:08 +01:00
Krešimir Klas
7e9972d99f Android: Resolves #680: New: Allow self-signed certificates (#1466)
* Allow User-added CAs in Android

This will enable connecting to servers with self-signed certificates on
android as per issue #680.

Implemented as per:
- https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html
- https://github.com/facebook/react-native/issues/20488

* Allow User-added CAs in Android

This will enable connecting to servers with self-signed certificates on
android as per issue #680.

Implemented as per:
- https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html
- https://github.com/facebook/react-native/issues/20488
2019-05-10 01:15:13 +01:00
Laurent Cozic
771975cd35 Clipper: Fixes #1462: Allow importing images from local file with file:// URLs 2019-05-10 01:06:06 +01:00
Laurent Cozic
356f8e580b Clipper: Improved: Updated Readability library to improve Simplified Page clipping 2019-05-10 00:05:23 +01:00
Laurent Cozic
68268cb35d Merge branch 'master' of github.com:laurent22/joplin 2019-05-09 23:46:41 +01:00
Laurent Cozic
8e58ed12af Clipper: Resolves #1379: Improved: Display warning icon when a page might not render well in simplified mode 2019-05-09 23:41:52 +01:00
Laurent Cozic
beb428b246 Android release v1.0.246 2019-05-08 00:54:46 +01:00
Laurent Cozic
4d81caff0b Electron release v1.0.148 2019-05-08 00:52:20 +01:00
Laurent Cozic
78372c9bac All: Improved: Make sure a revision is saved a note has not been modified for over a week 2019-05-08 00:51:56 +01:00
Laurent Cozic
a4db1bc671 All: Improved: Do not save a revision if there is already a recent one that exists 2019-05-08 00:10:36 +01:00
Laurent Cozic
8ea1c373ed Fixed position of config button 2019-05-07 23:50:12 +01:00
Laurent Cozic
8ef27dfcdc Merge branch 'master' of github.com:laurent22/joplin 2019-05-07 23:43:40 +01:00
Laurent Cozic
23e43c7bc1 Desktop: Fix: Fixed note history sort order. 2019-05-07 23:42:46 +01:00
Helmut K. C. Tessarek
edb8f4c79f Merge pull request #1503 from ruzaq/master
CLI: CZech translation updated
2019-05-07 17:46:39 -04:00
Ruzicka Pavel
e115fa4bb3 CZech translation updated 2019-05-07 23:30:18 +02:00
Laurent Cozic
52a2daddbf All: Improved: Make sure user timestamp is preserved with revision information 2019-05-07 22:15:47 +01:00
Laurent Cozic
c400142996 All: Fix: Make sure a revision is not empty before saving it 2019-05-07 20:46:58 +01:00
Laurent Cozic
219171a18c Remove dependency to git2json and improved handling of new commit messages 2019-05-06 22:18:17 +01:00
Laurent Cozic
d7d573d9dd Android release v1.0.245 2019-05-06 21:51:42 +01:00
Laurent Cozic
c4b17f8919 Electron release v1.0.147 2019-05-06 21:48:50 +01:00
Laurent Cozic
da2f4b96c7 Electron release v1.0.146 2019-05-06 21:48:37 +01:00
Laurent Cozic
08af9de190 All: Resolves #712: New: Support for note history (#1415)
* Started revisions support

* More rev changes

* More rev changes

* More revs changes

* Fixed deletion algorithm

* More tests and moved updated time to separate field

* Display info when restoring note

* Better handling of existing notes

* wip

* Further improvements and fixed tests

* Better handling of changes created via sync

* Enable chokidar again

* Testing special case

* Further improved logic to handle notes that existed before the revision service

* Added tests

* Better handling of encrypted revisions

* Improved handling of deleted note revisions by moving logic to collectRevision

* Improved handling of old notes by moving logic to collectRevision()

* Handle case when deleting revisions while one is still encrypted

* UI tweaks

* Added revision service to mobile app

* Fixed config screens on mobile and desktop

* Enabled revisions on CLI app
2019-05-06 21:35:29 +01:00
Laurent Cozic
9e2982992a Merge branch 'master' of github.com:laurent22/joplin 2019-05-06 21:31:45 +01:00
Laurent Cozic
c03ac5c5f1 Make sure Appveyor only build tags 2019-05-06 21:31:37 +01:00
Luis Orozco
5934f2f08e Desktop: Fixes #355: Resets the undo manager when creating new notes (#1495) 2019-05-06 21:30:04 +01:00
Caleb John
f136f40fdc Doc: Update readme to warn about restarting the editor for userstyle (#1487) 2019-05-06 21:26:34 +01:00
Luis Orozco
d213e4ab57 All: Fixed: Prevents notes with no title to break after synchronize (#1472)
Tests to confirm serialize/unserialize don't change body and title

check if item title exists, otherwise display default title.

added test checking serializing/unserializing Folders don't modify data
2019-05-06 21:25:14 +01:00
Laurent Cozic
782aae4ddf Doc: Better handling of platform in changelog autogenerate 2019-05-03 15:02:32 +00:00
193 changed files with 6148 additions and 1456 deletions

View File

@@ -23,6 +23,7 @@ const fs = require('fs-extra');
const { cliUtils } = require('./cli-utils.js');
const Cache = require('lib/Cache');
const WelcomeUtils = require('lib/WelcomeUtils');
const RevisionService = require('lib/services/RevisionService');
class Application extends BaseApplication {
@@ -422,6 +423,8 @@ class Application extends BaseApplication {
const tags = await Tag.allWithNotes();
ResourceService.runInBackground();
RevisionService.instance().runInBackground();
this.dispatch({
type: 'TAG_UPDATE_ALL',

View File

@@ -22,6 +22,7 @@ const Tag = require('lib/models/Tag.js');
const NoteTag = require('lib/models/NoteTag.js');
const MasterKey = require('lib/models/MasterKey');
const Setting = require('lib/models/Setting.js');
const Revision = require('lib/models/Revision.js');
const { Logger } = require('lib/logger.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { shimInit } = require('lib/shim-init-node.js');
@@ -43,6 +44,7 @@ BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', 'net.cozic.joplin-cli');
Setting.setConstant('appType', 'cli');

View File

@@ -643,6 +643,9 @@ msgstr "أخف %s"
msgid "Quit"
msgstr "إغلاق"
msgid "Close Window"
msgstr ""
msgid "&Edit"
msgstr "ت&حرير"
@@ -809,12 +812,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "الملاحظات و الإعدادات مخزّنة في: %s"
msgid "Browse..."
msgstr "استعراض..."
msgid "Check synchronisation configuration"
msgstr "فحص ضبط المزامنة"
msgid "Browse..."
msgstr "استعراض..."
msgid "Apply"
msgstr "تطبيق"
@@ -974,9 +977,33 @@ msgstr "المكان"
msgid "URL"
msgstr "عنوان URL"
#, fuzzy
msgid "Note History"
msgstr "قائمة ملاحظات"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "خصائص الملاحظة"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "جرى تعديل هذه الملاحظة:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "فتح..."
@@ -1283,6 +1310,10 @@ msgstr "قيد التقدم"
msgid "Synchronisation is already in progress. State: %s"
msgstr "المزامنة قيد التقدم بالفعل. الحال: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "مشفّر"
@@ -1533,6 +1564,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "تجاهل أخطاء شهادات TLS"
#, fuzzy
msgid "Enable note history"
msgstr "تفعيل التشفير"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "قيمة خيار غير صحيحة: \"%s\". القيم الممكنة هي: %s."
@@ -1602,12 +1647,19 @@ msgstr "لا يوجد بيانات للتصدير."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "فضلاً حدّد دفتر الملاحظات الذي ترغب استيراد الملاحظات إليه."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "العناصر التي لا يمكن مزامنتها"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "لم نتمكن من فتح هذا الملف: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "لم نتمكن من فتح هذا الملف: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1880,9 +1932,8 @@ msgstr "ليس لديك دفاتر ملاحظات حالياً. أنشئ واح
msgid "Welcome"
msgstr "مرحباً"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "جرى تعديل هذه الملاحظة:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -668,6 +668,9 @@ msgstr "Amaga %s"
msgid "Quit"
msgstr "Surt"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Edita"
@@ -844,12 +847,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Les notes i la configuració es desen a: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Comprova la configuració de la sincronització"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr ""
@@ -1013,9 +1016,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "Blocs de notes"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Aquesta nota s'ha modificat:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Obre..."
@@ -1326,6 +1353,10 @@ msgstr "En progés"
msgid "Synchronisation is already in progress. State: %s"
msgstr "La sincronització ja és en procés. Estat: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Xifrat"
@@ -1572,6 +1603,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr ""
#, fuzzy
msgid "Enable note history"
msgstr "Activa el xifratge"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "El valor de l'opció no és vàlid: «%s». Els valors possibles són: %s."
@@ -1646,12 +1691,19 @@ msgstr "No hi ha dades per exportar."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Indiqueu el bloc de notes on s'haurien d'importar les notes."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Elements que no s'han pogut sincronitzar"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "No s'ha pogut desar el bloc de notes: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "No s'ha pogut desar el bloc de notes: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1930,9 +1982,8 @@ msgstr ""
msgid "Welcome"
msgstr "Benvingut"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Aquesta nota s'ha modificat:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -23,7 +23,7 @@ msgid "Please select the note or notebook to be deleted first."
msgstr "Nejprve prosím vyberte poznámku či zápisník ke smazání."
msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Pro ukončení aplikace stiskněte Ctrl+D nebo napište \"exit\""
msgstr "Stiskněte Ctrl+D nebo napište \"exit\" pro ukončení aplikace"
#, javascript-format
msgid "More than one item match \"%s\". Please narrow down your query."
@@ -124,7 +124,6 @@ msgstr "Označí to-do jako hotové."
msgid "Note is not a to-do: \"%s\""
msgstr "Poznámka není to-do: \"%s\""
#, fuzzy
msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status`, `decrypt-file` and `target-status`."
@@ -248,9 +247,8 @@ msgstr ""
"Pro pohyb v seznamech a textových polích (včetně této konzole) používejte "
"šipky a page up/down."
#, fuzzy
msgid "To maximise/minimise the console, press \"tc\"."
msgstr "Pro maximalizaci/minimalizaci konzole stiskněte \"TC\"."
msgstr "Pro maximalizaci/minimalizaci konzole stiskněte \"tc\"."
msgid "To enter command line mode, press \":\""
msgstr "Pro přepnutí do příkazové řádky stiskněte \":\""
@@ -416,12 +414,16 @@ msgstr "Autentizace nebyla dokončena (nedostali jsme autentizační token)"
msgid ""
"To allow Joplin to synchronise with Dropbox, please follow the steps below:"
msgstr ""
"Chcete-li povolit synchronizaci služby Joplin se službou Dropbox, postupujte "
"podle následujících kroků:"
msgid "Step 1: Open this URL in your browser to authorise the application:"
msgstr ""
"Krok 1: Chcete-li povolit aplikaci, otevřete tuto adresu URL ve svém "
"prohlížeči:"
msgid "Step 2: Enter the code provided by Dropbox:"
msgstr ""
msgstr "Krok 2: Zadejte kód poskytnutý službou Dropbox:"
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
@@ -450,20 +452,20 @@ msgid "Starting synchronisation..."
msgstr "Zahajuji synchronizaci..."
msgid "Downloading resources..."
msgstr ""
msgstr "Stahování zdrojů..."
msgid "Cancelling... Please wait."
msgstr "Zastavuji, chvíli strpení."
#, fuzzy
msgid ""
"<tag-command> can be \"add\", \"remove\" or \"list\" to assign or remove "
"[tag] from [note], or to list the notes associated with [tag]. The command "
"`tag list` can be used to list all the tags (use -l for long option)."
msgstr ""
"<tag-command> může být \"add\", \"remove\" nebo \"list\" - přidat (add) či "
"<tag-command> může být \"add\", \"remove\" nebo \"list\" - přidat (add) či "
"odebrat (remove) [tag] k [poznámce], nebo vypsat (list) seznam poznámek "
"přiřazených k [tagu]. Příkaz `tag list` vypíše všechny tagy."
"přiřazených k [tagu]. Příkaz `tag list` vypíše všechny tagy. S parametrem -l "
"pro dlouhý výpis."
#, javascript-format
msgid "Invalid command: \"%s\""
@@ -516,7 +518,7 @@ msgid "Possible keys/values:"
msgstr "Možné klíče/hodnoty:"
msgid "Type `joplin help` for usage information."
msgstr "Pro nápovědu zadejte `joplin help`"
msgstr "Zadejte `joplin help` pro nápovědu."
msgid "Fatal error:"
msgstr "Fatální chyba:"
@@ -573,18 +575,17 @@ msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "Exportuji do \"%s\" jako formát \"%s\". Chvíli strpení..."
msgid "Sidebar"
msgstr ""
msgstr "Postranní lišta"
msgid "Note list"
msgstr ""
msgstr "Seznam položek"
#, fuzzy
msgid "Note title"
msgstr "Název zápisníku:"
msgstr "Název zápisníku"
#, fuzzy
msgid "Note body"
msgstr "Zápisníky"
msgstr "Zápisník"
#, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
@@ -615,7 +616,7 @@ msgid "Encryption options"
msgstr "Nastavení šifrování"
msgid "Web clipper options"
msgstr ""
msgstr "Web clipper volby"
#, javascript-format
msgid "%s %s (%s, %s)"
@@ -629,7 +630,7 @@ msgid "About Joplin"
msgstr "O aplikaci Joplin"
msgid "Preferences..."
msgstr ""
msgstr "Nastavení..."
msgid "Check for updates..."
msgstr "Zkontrolovat updaty..."
@@ -650,6 +651,9 @@ msgstr "Schovat %s"
msgid "Quit"
msgstr "Ukončit"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Upravit"
@@ -663,38 +667,36 @@ msgstr "Vyjmout"
msgid "Paste"
msgstr "Vložit"
#, fuzzy
msgid "Select all"
msgstr "Vybrat datum"
msgstr "Vybrat vše"
msgid "Bold"
msgstr ""
msgstr "Tučně"
msgid "Italic"
msgstr ""
msgstr "Kurzíva"
msgid "Link"
msgstr ""
msgstr "Odkaz"
msgid "Insert Date Time"
msgstr ""
msgstr "Vložit datum a čas"
msgid "Edit in external editor"
msgstr ""
msgstr "Upravit externím editorem"
msgid "Search in all the notes"
msgstr "Hledat ve všech poznámkách"
#, fuzzy
msgid "Search in current note"
msgstr "Hledat ve všech poznámkách"
msgstr "Hledat v aktuální poznámce"
#, fuzzy
msgid "&View"
msgstr "Zobrazit"
msgid "Toggle sidebar"
msgstr ""
msgstr "Přepnout postranní lištu"
msgid "Toggle editor layout"
msgstr "Změňit layout editoru"
@@ -735,18 +737,18 @@ msgstr "Současná verze je aktuální."
#, javascript-format
msgid "%s (pre-release)"
msgstr ""
msgstr "%s (pre-release)"
msgid "An update is available, do you want to download it now?"
msgstr "Je k dispozici update, chcete jej stáhnout?"
#, javascript-format
msgid "Your version: %s"
msgstr ""
msgstr "Vaše verze: %s"
#, javascript-format
msgid "New version: %s"
msgstr ""
msgstr "Nová verze: %s"
msgid "Yes"
msgstr "Ano"
@@ -755,81 +757,88 @@ msgid "No"
msgstr "Ne"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgstr "Token byl zkopírován do schránky!"
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
msgstr "Služba Web clipper je povolena a nastavena pro spouštění při startu."
#, javascript-format
msgid "Status: Started on port %d"
msgstr ""
msgstr "Stav: Nastartováno na portu %d"
#, fuzzy, javascript-format
#, javascript-format
msgid "Status: %s"
msgstr "Stav: %s."
msgstr "Stav: %s"
msgid "Disable Web Clipper Service"
msgstr ""
msgstr "Služba Web clipper vypnuta"
msgid "The web clipper service is not enabled."
msgstr ""
msgstr "Služba Web clipper není povolena."
msgid "Enable Web Clipper Service"
msgstr ""
msgstr "Povolit službu Web clipper"
msgid ""
"Joplin Web Clipper allows saving web pages and screenshots from your browser "
"to Joplin."
msgstr ""
"Joplin Web Clipper umožňuje ukládat webové stránky a screenshoty z vašeho "
"prohlížeče do Joplin."
msgid "In order to use the web clipper, you need to do the following:"
msgstr ""
msgstr "Pro použití web clipper musíte udělat následující:"
msgid "Step 1: Enable the clipper service"
msgstr ""
msgstr "Krok 1: Zapnout službu clipper"
msgid ""
"This service allows the browser extension to communicate with Joplin. When "
"enabling it your firewall may ask you to give permission to Joplin to listen "
"to a particular port."
msgstr ""
"Tato služba umožňuje rozšíření prohlížeče o komunikaci s Joplin. Pokud jej "
"povolíte, může vás firewall požádat, abyste Joplin dali svolení naslouchat "
"na určitém portu."
msgid "Step 2: Install the extension"
msgstr ""
msgstr "Krok 2: Nainstalovat rozšíření"
msgid "Download and install the relevant extension for your browser:"
msgstr ""
"Stáhnout a nainstalovat odpovídající rozšíření pro váš webový prohlížeč:"
#, fuzzy
msgid "Advanced options"
msgstr "Ukázat pokročilé volby"
msgid "Authorisation token:"
msgstr ""
msgstr "Autorizační token:"
msgid "Copy token"
msgstr ""
msgstr "Zkopírovat token"
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
"Tento autorizační token je potřeba pouze pro povolení komunikace Joplin s "
"aplikacemi třetích stran."
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Poznámky a nastavení uloženo v: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Zkontrolujte nastavení synchronizace"
msgid "Browse..."
msgstr "Procházet..."
msgid "Apply"
msgstr ""
msgstr "Použít"
msgid "Submit"
msgstr ""
msgstr "Odeslat"
msgid "Save"
msgstr "Uložit"
@@ -907,6 +916,8 @@ msgid ""
"For more information about End-To-End Encryption (E2EE) and advices on how "
"to enable it please check the documentation:"
msgstr ""
"Pro více informací o End-To-End šifrování (E2EE) a návod jak je povolit "
"náhledněte do dokumentace:"
msgid "Status"
msgstr "Status"
@@ -914,7 +925,6 @@ msgstr "Status"
msgid "Encryption is:"
msgstr "Šifrování je:"
#, fuzzy
msgid "Usage"
msgstr "Použití: %s"
@@ -944,9 +954,8 @@ msgstr "Tagy oddělujte čárkami."
msgid "Rename notebook:"
msgstr "Přejmenovat zápisník:"
#, fuzzy
msgid "Rename tag:"
msgstr "Přejmenovat"
msgstr "Přejmenovat tag:"
msgid "Set alarm:"
msgstr "Nastavit alarm:"
@@ -954,9 +963,8 @@ msgstr "Nastavit alarm:"
msgid "Layout"
msgstr "Layout"
#, fuzzy
msgid "Search..."
msgstr "Hledání"
msgstr "Hledat..."
msgid "Some items cannot be synchronised."
msgstr "Některé položky nelze synchronizovat."
@@ -978,32 +986,56 @@ msgid ""
msgstr "Nemáte žádný zápisník. Vytvořte jeden kliknutím na \"Nový zápisník\"."
msgid "Location"
msgstr ""
msgstr "Lokace"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Seznam položek"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "Nastavení poznámek"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Poznámka byla upravena:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Otevřít..."
#, fuzzy, javascript-format
#, javascript-format
msgid "This file could not be opened: %s"
msgstr "Nebylo možné uložit zápisník: %s"
msgstr "Soubor se nepodařilo otevřít: %s"
msgid "Save as..."
msgstr "Uložit jako..."
msgid "Copy path to clipboard"
msgstr ""
msgstr "Kopírovat cestu do schránky"
msgid "Copy Link Address"
msgstr ""
msgstr "Kopírovat adresu odkazu"
msgid "This attachment is not downloaded or not decrypted yet."
msgstr ""
msgstr "Tato příloha není ještě stažena nebo rozšifrována."
#, javascript-format
msgid "Unsupported link or message: %s"
@@ -1017,18 +1049,19 @@ msgstr "Tato poznámka je prázdný. Klikněte na \"%s\" pro otevření editoru
msgid "Only one note can be printed or exported to PDF at a time."
msgstr ""
"Pouze jedna poznámka může být zároveň vytištěna nebo exportována do PDF."
msgid "strong text"
msgstr ""
msgstr "tučný text"
msgid "emphasized text"
msgstr ""
msgstr "zvýrazněný text"
msgid "List item"
msgstr ""
msgstr "Seznam položek"
msgid "Insert Hyperlink"
msgstr ""
msgstr "Vložit odkaz"
msgid "Attach file"
msgstr "Přiložit soubor"
@@ -1044,32 +1077,32 @@ msgid "In: %s"
msgstr "%s: %s"
msgid "Hyperlink"
msgstr ""
msgstr "Odkaz"
msgid "Code"
msgstr ""
msgstr "Kód"
msgid "Numbered List"
msgstr ""
msgstr "Číslovanáý seznam"
msgid "Bulleted List"
msgstr ""
msgstr "Seznam s odrážkami"
msgid "Checkbox"
msgstr ""
msgstr "Zaškrtávací pole"
msgid "Heading"
msgstr ""
msgstr "Nadpis"
msgid "Horizontal Rule"
msgstr ""
msgstr "Horizontální čára"
msgid "Click to stop external editing"
msgstr ""
msgstr "Kliknutím ukončíte externí úpravy"
#, fuzzy
msgid "Watching..."
msgstr "Zastavuji..."
msgstr "Sleduji..."
msgid "to-do"
msgstr "to-do"
@@ -1091,7 +1124,7 @@ msgid "OneDrive Login"
msgstr "Přihlášení s OneDrive"
msgid "Dropbox Login"
msgstr ""
msgstr "Dropbox přihlášení"
msgid "Options"
msgstr "Nastavení"
@@ -1113,9 +1146,9 @@ msgid ""
"All notes and sub-notebooks within this notebook will also be deleted."
msgstr "Smazat zápisník? Budou smazány i všechny poznámky v něm obsažené."
#, fuzzy, javascript-format
#, javascript-format
msgid "Remove tag \"%s\" from all notes?"
msgstr "Odebrat tento tag ze všech poznámek?"
msgstr "Odebrat tag \"%s\" ze všech poznámek?"
msgid "Remove this search from the sidebar?"
msgstr "Smazat tento hledaný výraz z panelu?"
@@ -1129,13 +1162,13 @@ msgstr "Přejmenovat"
msgid "Notebooks"
msgstr "Zápisníky"
#, fuzzy, javascript-format
#, javascript-format
msgid "Decrypting items: %d/%d"
msgstr "Získané položky: %d/%d."
msgstr "Rozšifrované položky: %d/%d"
#, fuzzy, javascript-format
#, javascript-format
msgid "Fetching resources: %d"
msgstr "Zdroje: %d."
msgstr "Stahování zdrojů: %d"
msgid "Please select where the sync status should be exported to"
msgstr "Prosím vyberte, kam má být stav synchronizace exportován"
@@ -1143,13 +1176,12 @@ msgstr "Prosím vyberte, kam má být stav synchronizace exportován"
msgid "Add or remove tags"
msgstr "Přidat či odebrat tagy"
#, fuzzy
msgid "Duplicate"
msgstr "Ukončí aplikaci."
msgstr "Duplikovat"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s - Copy"
msgstr "Kopírovat"
msgstr "%s - Kopírovat"
msgid "Switch between note and to-do type"
msgstr "Přepnout mezi poznámkou a to-do"
@@ -1166,21 +1198,23 @@ msgstr "Přepnout mezi poznámkou a to-do"
msgid "Copy Markdown link"
msgstr "Markdown"
#, fuzzy, javascript-format
#, javascript-format
msgid "Delete note \"%s\"?"
msgstr "Smazat poznámky?"
msgstr "Smazat poznámku \"%s\"?"
#, fuzzy, javascript-format
#, javascript-format
msgid "Delete these %d notes?"
msgstr "Smazat tyto poznámky?"
msgstr "Smazat tyto \"%d\" poznámky?"
msgid ""
"Type a note title to jump to it. Or type # followed by a tag name, or @ "
"followed by a notebook name."
msgstr ""
"Zadejte název poznámky, na kterou chcete přeskočit. Nebo zadejte # "
"následovaný názvem značky/tagu nebo @ následovaným názvem poznámkového bloku."
msgid "Goto Anything..."
msgstr ""
msgstr "Přejít kamkoliv..."
#, javascript-format
msgid "Usage: %s"
@@ -1191,7 +1225,7 @@ msgid "Unknown flag: %s"
msgstr "Neznámý flag: %s"
msgid "Dropbox"
msgstr ""
msgstr "Dropbox"
msgid "File system"
msgstr "Souborový systém"
@@ -1221,7 +1255,7 @@ msgid ""
"synchronisation again may fix the problem."
msgstr ""
"Nelze obnovit token: chybí autentizační data. Restart synchronizace může "
"tento problém vyřešit. "
"tento problém vyřešit."
msgid "Untitled"
msgstr "Bez názvu"
@@ -1294,34 +1328,34 @@ msgstr "Probíhá"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronizace již probíhá. Stav: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Zašifrováno"
msgid "Encrypted items cannot be modified"
msgstr "Nelze editovat zašifrovanou položku"
#, fuzzy
msgid "title"
msgstr "Bez názvu"
msgstr "bez názvu"
#, fuzzy
msgid "updated date"
msgstr "Upraveno: %d."
msgstr "upraveno: %d"
msgid "Conflicts"
msgstr "Konflikty"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Poznámku nelze přesunout do zápisníku \"%s\""
msgstr "Poznámku nelze přesunout do zápisníku"
#, javascript-format
msgid "Notebooks cannot be named \"%s\", which is a reserved title."
msgstr "Zápisník se nemůže jmenovat \"%s\", tento název je rezervován."
#, fuzzy
msgid "created date"
msgstr "Vytvořeno: %d."
msgstr "vytvořeno: %d"
msgid "This note does not have geolocation information."
msgstr "Tato poznámka nemá informace o poloze."
@@ -1332,7 +1366,7 @@ msgstr "Poznámku \"%s\" nelze zkopírovat do zápisníku"
#, javascript-format
msgid "Cannot move note to \"%s\" notebook"
msgstr "Poznámku nelze přesunout do zápisníku \"%s\""
msgstr "Poznámku nelze přesunout do \"%s\" zápisníku"
#, javascript-format
msgid ""
@@ -1340,6 +1374,9 @@ msgid ""
"to it before syncing, otherwise all files will be removed! See the FAQ for "
"more details: %s"
msgstr ""
"Upozornění: Pokud toto umístění změníte, před synchronizací se ujistěte, že "
"jste do něj zkopírovali veškerý obsah, jinak budou všechny soubory "
"odstraněny! Další podrobnosti naleznete v FAQ: % s"
msgid "Language"
msgstr "Jazyk"
@@ -1362,9 +1399,8 @@ msgstr "Tmavý"
msgid "Uncompleted to-dos on top"
msgstr "Nedokončené to-do listy nahoře"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Nedokončené to-do listy nahoře"
msgstr "Zobrazit dokončené to-do listy"
msgid "Sort notes by"
msgstr "Řadit poznámky podle"
@@ -1372,7 +1408,6 @@ msgstr "Řadit poznámky podle"
msgid "Reverse sort order"
msgstr "Řadit od konce"
#, fuzzy
msgid "Sort notebooks by"
msgstr "Řadit poznámky podle"
@@ -1392,84 +1427,83 @@ msgid "When creating a new note:"
msgstr "Při vytváření nové poznámky:"
msgid "Enable soft breaks"
msgstr ""
msgstr "Povolit měkké zalomení"
#, fuzzy
msgid "Enable math expressions"
msgstr "Zapnout šifrování"
msgstr "Zapnout matematické výrazy"
msgid "Enable ==mark== syntax"
msgstr ""
msgstr "Povolit ==mark== syntaxi"
msgid "Enable footnotes"
msgstr ""
msgstr "Povolit poznámky pod čarou"
msgid "Enable table of contents extension"
msgstr ""
msgstr "Povolit rozšíření pro generování Obsahu"
msgid "Enable ~sub~ syntax"
msgstr ""
msgstr "Povolit ~sub~ syntaxi"
msgid "Enable ^sup^ syntax"
msgstr ""
msgstr "Povolit ~sub~ syntaxi"
msgid "Enable deflist syntax"
msgstr ""
msgstr "Povolit deflist syntaxi"
msgid "Enable abbreviation syntax"
msgstr ""
msgstr "Povolit syntaxi zkratek"
msgid "Enable markdown emoji"
msgstr ""
msgstr "Povolit markdown emoji"
msgid "Enable ++insert++ syntax"
msgstr ""
msgstr "Povolit ++insert++ syntaxi"
msgid "Enable multimarkdown table extension"
msgstr ""
msgstr "Povolit rozšíření multimarkdown tabulky"
msgid "Show tray icon"
msgstr "Zobrazovat ikonu v panelu"
msgid "Note: Does not work in all desktop environments."
msgstr ""
msgstr "Poznámka: Nefunguje v některých desktopových prostředích."
msgid ""
"This will allow Joplin to run in the background. It is recommended to enable "
"this setting so that your notes are constantly being synchronised, thus "
"reducing the number of conflicts."
msgstr ""
"Toto umožní Joplin běžet na pozadí. Doporučujeme toto nastavení povolit tak, "
"aby se vaše poznámky neustále synchronizovaly, čímž se sníží počet konfliktů."
msgid "Start application minimised in the tray icon"
msgstr ""
msgstr "Startovat aplikaci minimalizovanou do lišty ikon"
msgid "Global zoom percentage"
msgstr "Globální zoom"
#, fuzzy
msgid "Editor font size"
msgstr "Rodina písma v editoru"
msgid "Editor font family"
msgstr "Rodina písma v editoru"
#, fuzzy
msgid ""
"This must be *monospace* font or it will not work properly. If the font is "
"incorrect or empty, it will default to a generic monospace font."
msgstr ""
"Jméno fontu není kontrolováno. Pokud je neplatné či chybí, bude použit "
"defaultní monospace font."
"výchozí monospace font."
msgid "Automatically update the application"
msgstr "Automaticky updatovat aplikaci"
msgid "Get pre-releases when checking for updates"
msgstr ""
msgstr "Při hledání aktualizací zahrnout beta verze"
#, javascript-format
msgid "See the pre-release page for more details: %s"
msgstr ""
msgstr "Pro náhled beta verzí navštivte stránku: %s"
msgid "Synchronisation interval"
msgstr "Interval synchronizace"
@@ -1486,7 +1520,6 @@ msgstr "%d hodina"
msgid "%d hours"
msgstr "%d hodin"
#, fuzzy
msgid "Text editor command"
msgstr "Textový editor"
@@ -1533,7 +1566,7 @@ msgid "WebDAV password"
msgstr "WebDAV heslo"
msgid "Custom TLS certificates"
msgstr ""
msgstr "Vlastní TLS certifikát"
msgid ""
"Comma-separated list of paths to directories to load the certificates from, "
@@ -1541,39 +1574,53 @@ msgid ""
"pem. Note that if you make changes to the TLS settings, you must save your "
"changes before clicking on \"Check synchronisation configuration\"."
msgstr ""
"Čárkami oddělený seznam adresářů s certifikáty nebo cest k jednotlivým "
"souborům s certifikáty. Například: /my/cert_dir, /other/custom.pem. Uvědomte "
"si, že pokud provedete změny nastavení TLS, musíte změny uložit dříve, než "
"kliknete na \"Zkontrolovat konfiguraci synchronizace\"."
msgid "Ignore TLS certificate errors"
msgstr "Ignorovat chyby TLS certifikátu"
#, fuzzy
msgid "Enable note history"
msgstr "Povolit poznámky pod čarou"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Neplatná hodnota: \"%s\". Přípustné hodnoty jsou: %s."
#, fuzzy
msgid "General"
msgstr "Obecná nastavení"
#, fuzzy
msgid "Synchronisation"
msgstr "Stav synchronizace"
msgstr "Synchronizace"
msgid "Appearance"
msgstr ""
msgstr "Vzhled"
#, fuzzy
msgid "Note"
msgstr "Zápisníky"
msgid "Plugins"
msgstr ""
msgstr "Rozšíření"
#, fuzzy
msgid "Application"
msgstr "Ukončí aplikaci."
msgstr "Aplikace"
#, javascript-format
msgid "The tag \"%s\" already exists. Please choose a different name."
msgstr ""
msgstr "Tag \"%s\" již existuje. Zvolte jiný název."
msgid "Joplin Export File"
msgstr "Soubor Joplin Export"
@@ -1587,9 +1634,8 @@ msgstr "Složka pro export"
msgid "Evernote Export File"
msgstr "Soubor Evernote Exportu"
#, fuzzy
msgid "Json Export Directory"
msgstr "Složka pro export"
msgstr "Složka pro JSON export"
msgid "File"
msgstr "Soubor"
@@ -1619,12 +1665,19 @@ msgstr "Nic k exportování."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Prosím specifikujte sešit, do kterého mají být poznámky importovány."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Položky jež nelze synchronizovat"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Soubor se nepodařilo otevřít: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Soubor se nepodařilo otevřít: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1669,10 +1722,10 @@ msgid "On %s: %s"
msgstr "Na %s: %s"
msgid "Permission to use camera"
msgstr ""
msgstr "Oprávnění použít kameru"
msgid "Your permission to use your camera is required."
msgstr ""
msgstr "Je vyžadováno oprávnění použít vaši kameru."
msgid "There are currently no notes. Create one by clicking on the (+) button."
msgstr "Nemáte žádné poznámky. Vytvořte jednu kliknutím na tlačítko (+)."
@@ -1702,11 +1755,9 @@ msgstr "Přesunout poznámky %d do zápisníku \"%s\"?"
msgid "Press to set the decryption password."
msgstr "Stiskněte pro zadání hesla k dešifrování."
#, fuzzy
msgid "Clear alarm"
msgstr "Nastavit alarm"
msgstr "Zrušit alarm"
#, fuzzy
msgid "Save alarm"
msgstr "Nastavit alarm"
@@ -1723,18 +1774,18 @@ msgstr "Zrušit synchronizaci"
msgid "Checking... Please wait."
msgstr "Zastavuji, chvíli strpení."
#, fuzzy
msgid "Success! Synchronisation configuration appears to be correct."
msgstr "Zkontrolujte nastavení synchronizace"
msgstr "Úspěch! Nastavení synchronizace se zdá být v pořádku."
msgid ""
"Error. Please check that URL, username, password, etc. are correct and that "
"the sync target is accessible. The reported error was:"
msgstr ""
"Chyba. Zkontrolujte, zda jsou adresa URL, uživatelské jméno, heslo, atd. "
"správné a zda je cíl synchronizace dostupný. Zjištěná chyba byla:"
#, fuzzy
msgid "The application has been authorised!"
msgstr "Aplikace byla úspěšně autorizována."
msgstr "Aplikace byla úspěšně autorizována!"
#, javascript-format
msgid ""
@@ -1744,10 +1795,15 @@ msgid ""
"\n"
"Please try again."
msgstr ""
"Aplikaci se nepodařilo autorizovat:\\n\n"
"\\n\n"
"%s\\n\n"
"\\n\n"
"Prosím, zkuste to znovu."
#, fuzzy, javascript-format
#, javascript-format
msgid "Decrypted items: %s / %s"
msgstr "Získané položky: %d/%d."
msgstr "Rozšifrované položky: %s/%s"
msgid "New tags:"
msgstr "Nové tagy:"
@@ -1759,35 +1815,39 @@ msgid ""
"To work correctly, the app needs the following permissions. Please enable "
"them in your phone settings, in Apps > Joplin > Permissions"
msgstr ""
"Aby aplikace fungovala správně, potřebuje následující oprávnění. Povolte je "
"v nastavení telefonu v aplikaci Aplikace> Joplin> Oprávnění"
msgid ""
"- Storage: to allow attaching files to notes and to enable filesystem "
"synchronisation."
msgstr ""
"- Úložiště: umožňuje připojení souborů k poznámkám a umožňuje synchronizaci "
"souborového systému."
msgid "- Camera: to allow taking a picture and attaching it to a note."
msgstr ""
"- Fotoaparát: umožňuje pořízení fotografie a její připojení k poznámce."
msgid "- Location: to allow attaching geo-location information to a note."
msgstr ""
msgstr "Lokace: umožňuje k poznámce připojit informaci o zeměpisné poloze."
msgid "Joplin website"
msgstr "Web Joplinu"
#, javascript-format
msgid "Database v%s"
msgstr ""
msgstr "Databáze v%s"
#, fuzzy, javascript-format
msgid "FTS enabled: %d"
msgstr "K smazání: %d"
#, fuzzy
msgid "Login with Dropbox"
msgstr "Přihlásit se pomocí OneDrive"
msgstr "Přihlášení Dropbox"
msgid "Enter code here"
msgstr ""
msgstr "Zde vložte kód"
#, javascript-format
msgid "Master Key %s"
@@ -1830,19 +1890,18 @@ msgstr "Zahodit změny"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
msgstr "Nenalezena položka s ID %s"
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
msgstr "Mobilní aplikace Joplin aktuálně nepodporuje tento typ odkazu:% s"
#, javascript-format
msgid "Unsupported image type: %s"
msgstr "Nepodporovaný formát obrázku: %s"
#, fuzzy
msgid "Take photo"
msgstr "Přiložit obrázek"
msgstr "Přiložit foto"
msgid "Attach photo"
msgstr "Přiložit obrázek"
@@ -1851,7 +1910,7 @@ msgid "Attach any file"
msgstr "Přiložit soubor"
msgid "Share"
msgstr ""
msgstr "Sdílet"
msgid "Convert to note"
msgstr "Konvertovat na poznámku"
@@ -1869,7 +1928,7 @@ msgid "View on map"
msgstr "Zobrazit na map+"
msgid "Go to source URL"
msgstr ""
msgstr "Jít na zdrojovou URL"
msgid "Edit"
msgstr "Upravit"
@@ -1896,9 +1955,8 @@ msgstr "Nemáte žádný zápisník. Vytvořte jeden kliknutím na tlačítko (+
msgid "Welcome"
msgstr "Vítejte"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Poznámka byla upravena:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -655,6 +655,9 @@ msgstr "Skjul %s"
msgid "Quit"
msgstr "Afslut"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Ret"
@@ -824,12 +827,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Noter og indstillinger er gemt i: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Check synkroniserings Indstillinger"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr ""
@@ -992,9 +995,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "Notesbøger"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Denne note er ændret:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Åben..."
@@ -1303,6 +1330,10 @@ msgstr "I gang"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Synkronisering er allerede i gang: Tilstand: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Krypteret"
@@ -1554,6 +1585,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr ""
#, fuzzy
msgid "Enable note history"
msgstr "Start kryptering"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ulovlig værdi: \"%s\". Mulige valg er: %s."
@@ -1628,12 +1673,19 @@ msgstr "Der er ingen data at eksportere."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Angiv hvilken notesbog, noter skal importeres til."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Emner kan ikke synkroniseres"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Notesbogen kan ikke gemmes: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Notesbogen kan ikke gemmes: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1905,9 +1957,8 @@ msgstr "Du har ingen notesbøger. Opret en ved at klikke på (+) knappen."
msgid "Welcome"
msgstr "Velkommen"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Denne note er ændret:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -674,6 +674,9 @@ msgstr "%s ausblenden"
msgid "Quit"
msgstr "Verlassen"
msgid "Close Window"
msgstr ""
msgid "&Edit"
msgstr "&Bearbeiten"
@@ -846,12 +849,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Notizen und Einstellungen werden gespeichert in: %s"
msgid "Browse..."
msgstr "Durchsuche..."
msgid "Check synchronisation configuration"
msgstr "Überprüfen der Synchronisationseinstellungen"
msgid "Browse..."
msgstr "Durchsuche..."
msgid "Apply"
msgstr "Anwenden"
@@ -1018,9 +1021,33 @@ msgstr "Standort"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Notizen-Liste"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "Notiz-Eigenschaften"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Diese Notiz wurde verändert:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Öffne..."
@@ -1332,6 +1359,10 @@ msgstr "In Bearbeitung"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronisation ist bereits im Gange. Status: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Verschlüsselt"
@@ -1586,6 +1617,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "Ignoriere TLS-Zertifikatfehler"
#, fuzzy
msgid "Enable note history"
msgstr "Aktiviere Fußnoten"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
@@ -1658,12 +1703,19 @@ msgid "Please specify the notebook where the notes should be imported to."
msgstr ""
"Bitte wähle aus, wohin der Synchronisations-Status exportiert werden soll."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Objekte können nicht synchronisiert werden"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Dieses Notizbuch konnte nicht geöffnet werden: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Dieses Notizbuch konnte nicht geöffnet werden: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1948,9 +2000,8 @@ msgstr ""
msgid "Welcome"
msgstr "Willkommen"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Diese Notiz wurde verändert:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid "Table of contents"
#~ msgstr "Inhaltsverzeichnis"

View File

@@ -583,6 +583,9 @@ msgstr ""
msgid "Quit"
msgstr ""
msgid "Close Window"
msgstr ""
msgid "&Edit"
msgstr ""
@@ -744,10 +747,10 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Browse..."
msgid "Check synchronisation configuration"
msgstr ""
msgid "Check synchronisation configuration"
msgid "Browse..."
msgstr ""
msgid "Apply"
@@ -894,9 +897,31 @@ msgstr ""
msgid "URL"
msgstr ""
msgid "Note History"
msgstr ""
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
msgid "This note has no history"
msgstr ""
msgid "Restore"
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 ""
msgid "Open..."
msgstr ""
@@ -1191,6 +1216,10 @@ msgstr ""
msgid "Synchronisation is already in progress. State: %s"
msgstr ""
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr ""
@@ -1425,6 +1454,19 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr ""
msgid "Enable note history"
msgstr ""
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@@ -1492,11 +1534,18 @@ msgstr ""
msgid "Please specify the notebook where the notes should be imported to."
msgstr ""
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "%s (%s): %s"
msgid "%s (%s) could not be uploaded: %s"
msgstr ""
#, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr ""
msgid ""

View File

@@ -589,6 +589,9 @@ msgstr ""
msgid "Quit"
msgstr ""
msgid "Close Window"
msgstr ""
msgid "&Edit"
msgstr ""
@@ -752,12 +755,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Check synchronization configuration"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr ""
@@ -912,9 +915,31 @@ msgstr ""
msgid "URL"
msgstr ""
msgid "Note History"
msgstr ""
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
msgid "This note has no history"
msgstr ""
msgid "Restore"
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 ""
msgid "Open..."
msgstr ""
@@ -1211,6 +1236,10 @@ msgstr ""
msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronization is already in progress. State: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr ""
@@ -1450,6 +1479,19 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr ""
msgid "Enable note history"
msgstr ""
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@@ -1517,11 +1559,18 @@ msgstr ""
msgid "Please specify the notebook where the notes should be imported to."
msgstr ""
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Items that cannot be synchronized"
#, javascript-format
msgid "%s (%s): %s"
msgid "%s (%s) could not be uploaded: %s"
msgstr ""
#, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr ""
msgid ""

View File

@@ -662,6 +662,9 @@ msgstr "Oculta %s"
msgid "Quit"
msgstr "Salir"
msgid "Close Window"
msgstr ""
msgid "&Edit"
msgstr "&Editar"
@@ -830,12 +833,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Las notas y los ajustes se guardan en: %s"
msgid "Browse..."
msgstr "Explorar..."
msgid "Check synchronisation configuration"
msgstr "Comprobar sincronización"
msgid "Browse..."
msgstr "Explorar..."
msgid "Apply"
msgstr "Aplicar"
@@ -995,9 +998,33 @@ msgstr "Localización"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Lista de notas"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "Propiedades de nota"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Esta nota ha sido modificada:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Abrir..."
@@ -1305,6 +1332,10 @@ msgstr "En progreso"
msgid "Synchronisation is already in progress. State: %s"
msgstr "La sincronización ya está en progreso. Estado: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Cifrado"
@@ -1562,6 +1593,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "Ignorar errores en certificados TLS"
#, fuzzy
msgid "Enable note history"
msgstr "Habilitar cifrado"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Opción inválida: «%s». Los valores posibles son: %s."
@@ -1631,12 +1676,19 @@ msgstr "No hay datos para exportar."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Por favor especifique la libreta donde las notas deben ser importadas."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Elementos que no se pueden sincronizar"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "No se ha podido abrir este archivo: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "No se ha podido abrir este archivo: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1917,9 +1969,8 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenido"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Esta nota ha sido modificada:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -663,6 +663,9 @@ msgstr ""
msgid "Quit"
msgstr "Irten"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Editatu"
@@ -832,13 +835,13 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Oharrak eta ezarpenak hemen daude gordeta: %s"
msgid "Browse..."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Sinkronizazioa utzi"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr ""
@@ -1000,9 +1003,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "Koadernoak"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Ohar hau mugitua izan da:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr ""
@@ -1316,6 +1343,10 @@ msgstr ""
msgid "Synchronisation is already in progress. State: %s"
msgstr "Sinkronizazioa hasita dago. Egoera: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Zifratuta"
@@ -1574,6 +1605,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr ""
#, fuzzy
msgid "Enable note history"
msgstr "Zifratua gaitu"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Balio aukera baliogabea: \"%s\". Litezkeen balioak: %s."
@@ -1649,12 +1694,19 @@ msgstr ""
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Aukeratu nora esportatu sinkronizazioaren egoera, mesedez"
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Itemok ezin sinkronizatu"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Koadernoa ezin gorde daiteke: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Koadernoa ezin gorde daiteke: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1926,9 +1978,8 @@ msgstr "Oraindik ez duzu koadernorik. Sortu bat (+) botoian sakatuta."
msgid "Welcome"
msgstr "Ongi etorri!"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Ohar hau mugitua izan da:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -659,6 +659,9 @@ msgstr "Cacher %s"
msgid "Quit"
msgstr "Quitter"
msgid "Close Window"
msgstr ""
msgid "&Edit"
msgstr "&Édition"
@@ -830,12 +833,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Les notes et paramètres se trouve dans : %s"
msgid "Browse..."
msgstr "Parcourir…"
msgid "Check synchronisation configuration"
msgstr "Vérifier config synchronisation"
msgid "Browse..."
msgstr "Parcourir…"
msgid "Apply"
msgstr "Appliquer"
@@ -1002,9 +1005,33 @@ msgstr "Lieu"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Liste de notes"
msgid "Previous versions of this note"
msgstr ""
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 ""
#, fuzzy
msgid "This note has no history"
msgstr "Cette note a été modifiée :"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Ouvrir..."
@@ -1317,6 +1344,10 @@ msgstr "En cours"
msgid "Synchronisation is already in progress. State: %s"
msgstr "La synchronisation est déjà en cours. État : %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Chiffré"
@@ -1570,6 +1601,20 @@ 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"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
@@ -1639,12 +1684,19 @@ msgstr "Il n'y a pas de données à exporter."
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 ""
msgid "Items that cannot be synchronised"
msgstr "Objets qui ne peuvent pas être synchronisés"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s) : %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Ce fichier n'a pas pu être ouvert : %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Ce fichier n'a pas pu être ouvert : %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1927,9 +1979,8 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenue"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Cette note a été modifiée :"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s) : %s"
#~ msgid "Table of contents"
#~ msgstr "Table des matières"

View File

@@ -655,6 +655,9 @@ msgstr "Ocultar %s"
msgid "Quit"
msgstr "Saír"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Edtar"
@@ -824,12 +827,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "As notas e axustes gardáronse en: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Comprobar a configuración da sincronización"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr ""
@@ -989,9 +992,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "Cadernos"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Esta nota foi modificada:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Abrir…"
@@ -1303,6 +1330,10 @@ msgstr "En proceso"
msgid "Synchronisation is already in progress. State: %s"
msgstr "A sincronización xa está en proceso. Estado: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Cifrado"
@@ -1554,6 +1585,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr ""
#, fuzzy
msgid "Enable note history"
msgstr "Activar cifrado"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Valor incorrecto de opción: «%s». Os valores posíbeis son: %s."
@@ -1628,12 +1673,19 @@ msgstr "Non hai datos para exportar."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Indique o caderno ao que importar as notas."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Elementos que non se poden sincronizar"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Non foi posíbel gardar o caderno: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Non foi posíbel gardar o caderno: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1905,9 +1957,8 @@ msgstr "Non ten cadernos actualmente. Cree un premendo no botón (+)."
msgid "Welcome"
msgstr "Benvido/a"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Esta nota foi modificada:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -660,6 +660,9 @@ msgstr ""
msgid "Quit"
msgstr "Izađi"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Uredi"
@@ -830,13 +833,13 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Bilješke i postavke su pohranjene u: %s"
msgid "Browse..."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Prekini sinkronizaciju"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr ""
@@ -987,9 +990,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "Bilježnice"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Bilješka je promijenjena:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr ""
@@ -1299,6 +1326,10 @@ msgstr ""
msgid "Synchronisation is already in progress. State: %s"
msgstr "Sinkronizacija je već u toku. Stanje: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr ""
@@ -1550,6 +1581,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr ""
#, fuzzy
msgid "Enable note history"
msgstr "Bilješka je promijenjena:"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s."
@@ -1625,12 +1670,19 @@ msgstr ""
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Odaberi lokaciju za izvoz statusa sinkronizacije"
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Stavke koje se ne mogu sinkronizirati"
#, fuzzy, javascript-format
msgid "%s (%s): %s"
msgstr "%s %s (%s)"
msgid "%s (%s) could not be uploaded: %s"
msgstr "Bilježnicu nije moguće snimiti: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Bilježnicu nije moguće snimiti: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1902,8 +1954,8 @@ msgid "Welcome"
msgstr "Dobro došli"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Bilješka je promijenjena:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s %s (%s)"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -668,6 +668,9 @@ msgstr "Nascondi %s"
msgid "Quit"
msgstr "Esci"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Modifica"
@@ -841,12 +844,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Le note e le impostazioni sono memorizzate in: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Controlla la configurazione della sincronizzazione"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr "Applica"
@@ -1011,9 +1014,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "Taccuini"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Questa note è stata modificata:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Apri ..."
@@ -1321,6 +1348,10 @@ msgstr "In corso"
msgid "Synchronisation is already in progress. State: %s"
msgstr "La sincronizzazione è già in corso. Stato: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Crittografato"
@@ -1573,6 +1604,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "Ignora gli errori del certificato TLS"
#, fuzzy
msgid "Enable note history"
msgstr "Attiva Crittografia"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Opzione non valida: \"%s\". I valori possibili sono: %s."
@@ -1646,12 +1691,19 @@ msgstr "Non ci sono dati da esportare."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Si prega di specificare il Taccuino in cui importare le note."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Elementi che non possono essere sincronizzati"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Questo file non può essere aperto: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Questo file non può essere aperto: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1930,9 +1982,8 @@ msgstr ""
msgid "Welcome"
msgstr "Benvenuto"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Questa note è stata modificata:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -650,6 +650,9 @@ msgstr "%s を隠す"
msgid "Quit"
msgstr "終了"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "編集"
@@ -823,12 +826,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "ノートと設定は次の場所に保存されます: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "同期の設定を確認する"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr "適用"
@@ -989,9 +992,33 @@ msgstr "場所"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "ノートブック"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "ノートのプロパティ"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "ノートは変更されています:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "開く..."
@@ -1298,6 +1325,10 @@ msgstr "実行中"
msgid "Synchronisation is already in progress. State: %s"
msgstr "同期作業はすでに実行中です。状態: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "暗号化済"
@@ -1551,6 +1582,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "TLS証明書のエラーを無視"
#, fuzzy
msgid "Enable note history"
msgstr "暗号化を有効にする"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "無効な設定値: \"%s\"。有効な値は: %sです。"
@@ -1624,12 +1669,19 @@ msgstr "エクスポートするデータがありません。"
msgid "Please specify the notebook where the notes should be imported to."
msgstr "ノートをどのノートブックにインポートするのか指定してください。"
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "同期ができなかったアイテム"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "次のファイルは開くことができません:%s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "次のファイルは開くことができません:%s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1908,9 +1960,8 @@ msgstr ""
msgid "Welcome"
msgstr "ようこそ"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "ノートは変更されています:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -583,6 +583,9 @@ msgstr ""
msgid "Quit"
msgstr ""
msgid "Close Window"
msgstr ""
msgid "&Edit"
msgstr ""
@@ -744,10 +747,10 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Browse..."
msgid "Check synchronisation configuration"
msgstr ""
msgid "Check synchronisation configuration"
msgid "Browse..."
msgstr ""
msgid "Apply"
@@ -894,9 +897,31 @@ msgstr ""
msgid "URL"
msgstr ""
msgid "Note History"
msgstr ""
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
msgid "This note has no history"
msgstr ""
msgid "Restore"
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 ""
msgid "Open..."
msgstr ""
@@ -1191,6 +1216,10 @@ msgstr ""
msgid "Synchronisation is already in progress. State: %s"
msgstr ""
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr ""
@@ -1425,6 +1454,19 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr ""
msgid "Enable note history"
msgstr ""
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@@ -1492,11 +1534,18 @@ msgstr ""
msgid "Please specify the notebook where the notes should be imported to."
msgstr ""
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr ""
#, javascript-format
msgid "%s (%s): %s"
msgid "%s (%s) could not be uploaded: %s"
msgstr ""
#, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr ""
msgid ""

View File

@@ -647,6 +647,9 @@ msgstr "%s 숨기기"
msgid "Quit"
msgstr "종료"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "편집"
@@ -819,12 +822,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "노트와 설정값이 다음에 저장되었습니다: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "동기화 설정 확인"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr "적용"
@@ -983,9 +986,33 @@ msgstr "위치"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "노트북"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "노트 속성"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "노트가 변경되었습니다:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "열기..."
@@ -1292,6 +1319,10 @@ msgstr "진행 중"
msgid "Synchronisation is already in progress. State: %s"
msgstr "동기화가 이미 진행되고 있습니다. 상태: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "암호화됨"
@@ -1543,6 +1574,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "TLS 인증서 오류 무시"
#, fuzzy
msgid "Enable note history"
msgstr "암호화 사용"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "올바르지 않은 옵션 값: \"%s\". 가능한 값은 다음과 같습니다: \"%s\"."
@@ -1616,12 +1661,19 @@ msgstr "내보낼 데이터가 없습니다."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "노트를 가져와서 저장할 노트북을 지정하세요."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "동기화를 할 수 없는 항목"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "다음 파일을 열 수 없습니다: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "다음 파일을 열 수 없습니다: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1897,9 +1949,8 @@ msgstr "노트북이 없습니다. (+) 버튼을 눌러 새로 만드세요."
msgid "Welcome"
msgstr "환영합니다"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "노트가 변경되었습니다:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -650,6 +650,9 @@ msgstr "Skjul %s"
msgid "Quit"
msgstr "Avslutt"
msgid "Close Window"
msgstr ""
msgid "&Edit"
msgstr "&Rediger"
@@ -818,12 +821,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Notater og innstillinger er lagret i: %s"
msgid "Browse..."
msgstr "Utforsk..."
msgid "Check synchronisation configuration"
msgstr "Sjekk synkroniseringskonfigurasjon"
msgid "Browse..."
msgstr "Utforsk..."
msgid "Apply"
msgstr "Bruk"
@@ -987,9 +990,33 @@ msgstr "Lokasjon"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Notatliste"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "Notategenskaper"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Dette notatet har blitt endret:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Åpne..."
@@ -1297,6 +1324,10 @@ msgstr "Pågår"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Synkronisering pågår allerede. Status: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Kryptert"
@@ -1552,6 +1583,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "Ignorer TLS-sertifikatfeil"
#, fuzzy
msgid "Enable note history"
msgstr "Liste over innhold"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ugyldig verdi: \"%s\". Mulige verdier er: %s."
@@ -1621,12 +1666,19 @@ msgstr "Det er ingen data for eksportering."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Velg notatbok som notatene skal importeres til."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Elementer som ikke vil synkronisere"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Filen kunne ikke åpnes: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Filen kunne ikke åpnes: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1902,9 +1954,8 @@ msgstr "Du har enda ingen notatbok. Opprett en ved å klikke på (+)-knappen."
msgid "Welcome"
msgstr "Velkommen"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Dette notatet har blitt endret:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid "Table of contents"
#~ msgstr "Liste over innhold"

View File

@@ -665,6 +665,9 @@ msgstr ""
msgid "Quit"
msgstr "Stop"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Bewerk"
@@ -834,13 +837,13 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Notities en instellingen zijn opgeslaan in %s"
msgid "Browse..."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Annuleer synchronisatie"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr ""
@@ -1004,9 +1007,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "Notitieboeken"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Deze notitie werd aangepast:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr ""
@@ -1320,6 +1347,10 @@ msgstr ""
msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronisatie is reeds bezig. Status: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Versleuteld"
@@ -1575,6 +1606,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr ""
#, fuzzy
msgid "Enable note history"
msgstr "Schakel encryptie in"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ongeldige optie: \"%s\". Geldige waarden zijn: %s."
@@ -1650,12 +1695,19 @@ msgstr ""
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Selecteer waar de synchronisatie status naar geëxporteerd moet worden"
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Items die niet gesynchroniseerd kunnen worden"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Het notitieboek kon niet opgeslaan worden: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Het notitieboek kon niet opgeslaan worden: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1931,9 +1983,8 @@ msgstr ""
msgid "Welcome"
msgstr "Welkom"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Deze notitie werd aangepast:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -664,6 +664,9 @@ msgstr "%s verbergen"
msgid "Quit"
msgstr "Afsluiten"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Bewerken"
@@ -839,12 +842,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Notities en instellingen worden opgeslagen in: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Synchronisatieconfiguratie controleren"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr "Toepassen"
@@ -1012,9 +1015,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "Notitieboeken"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Deze notitie is bewerkt:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Openen..."
@@ -1323,6 +1350,10 @@ msgstr "Bezig"
msgid "Synchronisation is already in progress. State: %s"
msgstr "De synchronisatie loopt al. Status: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Versleuteld"
@@ -1574,6 +1605,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "TLS-certificaatfouten negeren"
#, fuzzy
msgid "Enable note history"
msgstr "Versleuteling inschakelen"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ongeldige optie: \"%s\". Geldige waarden zijn: %s."
@@ -1649,12 +1694,19 @@ msgid "Please specify the notebook where the notes should be imported to."
msgstr ""
"Geef het notitieboek op waar de notities naar moeten worden geïmporteerd."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Items die niet kunnen worden gesynchroniseerd"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Dit bestand kan niet worden geopend: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Dit bestand kan niet worden geopend: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1934,9 +1986,8 @@ msgstr ""
msgid "Welcome"
msgstr "Welkom"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Deze notitie is bewerkt:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -661,6 +661,9 @@ msgstr "Ocultar %s"
msgid "Quit"
msgstr "Sair"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Editar"
@@ -835,12 +838,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Notas e configurações estão armazenadas em: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Verificar a configuração da sincronização"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr "Aplicar"
@@ -1002,9 +1005,33 @@ msgstr "Localização"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Cadernos"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "Propriedades da nota"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Esta nota foi modificada:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Abrir..."
@@ -1313,6 +1340,10 @@ msgstr "Em andamento"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Sincronização já em andamento. Estado: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Encriptado"
@@ -1568,6 +1599,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "Ignorar erros de certificados TLS"
#, fuzzy
msgid "Enable note history"
msgstr "Habilitar encriptação"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Valor da opção inválida: \"%s\". Os valores possíveis são: %s."
@@ -1642,12 +1687,19 @@ msgid "Please specify the notebook where the notes should be imported to."
msgstr ""
"Por favor, especifique o caderno para onde as notas deveriam ser importadas."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Os itens não podem ser sincronizados"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Este arquivo não pôde ser aberto: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Este arquivo não pôde ser aberto: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1928,9 +1980,8 @@ msgstr "Você não possui cadernos. Crie um clicando no botão (+)."
msgid "Welcome"
msgstr "Bem-vindo"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Esta nota foi modificada:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -599,6 +599,9 @@ msgstr "Ascundeți %s"
msgid "Quit"
msgstr "Ieșiți"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Editați"
@@ -767,10 +770,10 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Browse..."
msgid "Check synchronisation configuration"
msgstr ""
msgid "Check synchronisation configuration"
msgid "Browse..."
msgstr ""
msgid "Apply"
@@ -918,9 +921,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "Caiete de notițe"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Această notiță a fost modificată:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Deschideți..."
@@ -1218,6 +1245,10 @@ msgstr "În progres"
msgid "Synchronisation is already in progress. State: %s"
msgstr ""
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Criptat"
@@ -1455,6 +1486,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "Ignoră erorile de certificat TLS"
#, fuzzy
msgid "Enable note history"
msgstr "Activați criptarea"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@@ -1526,12 +1571,19 @@ msgstr "Nu există date de exportat."
msgid "Please specify the notebook where the notes should be imported to."
msgstr ""
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Itemii nu pot fi sincronizați"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Acest fișier nu a putut fi deschis: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Acest fișier nu a putut fi deschis: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1797,9 +1849,8 @@ msgstr ""
msgid "Welcome"
msgstr "Bine ați venit"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Această notiță a fost modificată:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#, fuzzy
#~ msgid "Joplin v%s"

View File

@@ -663,6 +663,9 @@ msgstr "Скрыть %s"
msgid "Quit"
msgstr "Выход"
msgid "Close Window"
msgstr ""
msgid "&Edit"
msgstr "&Правка"
@@ -831,12 +834,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Заметки и настройки сохранены в: %s"
msgid "Browse..."
msgstr "Обзор..."
msgid "Check synchronisation configuration"
msgstr "Проверить настройки синхронизации"
msgid "Browse..."
msgstr "Обзор..."
msgid "Apply"
msgstr "Применить"
@@ -999,9 +1002,33 @@ msgstr "Местоположение"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Список заметок"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "Свойства заметки"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Эта заметка была изменена:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Открыть..."
@@ -1310,6 +1337,10 @@ msgstr "Выполнение"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Синхронизация уже выполняется. Состояние: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Зашифровано"
@@ -1566,6 +1597,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "Игнорировать ошибки сертификата TLS"
#, fuzzy
msgid "Enable note history"
msgstr "Включить шифрование"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Неверное значение параметра: \"%s\". Доступные значения: %s."
@@ -1636,12 +1681,19 @@ msgid "Please specify the notebook where the notes should be imported to."
msgstr ""
"Пожалуйста, укажите блокнот, в который должны быть импортированы заметки."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Элементы, которые не могут быть синхронизированы"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Этот файл не может быть открыт: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Этот файл не может быть открыт: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1921,9 +1973,8 @@ msgstr "У вас сейчас нет блокнота. Создайте его
msgid "Welcome"
msgstr "Добро пожаловать"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Эта заметка была изменена:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -661,6 +661,9 @@ msgstr "Skrij %s"
msgid "Quit"
msgstr "Izhod"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Uredi"
@@ -830,12 +833,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Zabeležke in nastavitve so shranjene v: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Preveri nastavitve sinhronizacije"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr ""
@@ -1003,9 +1006,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "Beležnice"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Ta zabeležka je bila spremenjena:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Odpri..."
@@ -1318,6 +1345,10 @@ msgstr "V postopku"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Sinhronizacija je že v postopku. Stanje: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Enkriptirano"
@@ -1569,6 +1600,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr ""
#, fuzzy
msgid "Enable note history"
msgstr "Omogoči enkripcijo"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Neveljavna vrednost: \"%s\". Možne vrednosti so : %s."
@@ -1643,12 +1688,19 @@ msgstr "Ni datotek za izvoz."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Prosim navedite beležnico, kamor želite uvoziti zabeležke."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Predmeti ne morejo biti sinhronizirani"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Beležnica ne more biti shranjena: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Beležnica ne more biti shranjena: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1920,9 +1972,8 @@ msgstr "Trenutno nimate nobene beležnice. Ustvarite jo s klikom na (+) gumb."
msgid "Welcome"
msgstr "Dobrodošli"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Ta zabeležka je bila spremenjena:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -665,6 +665,9 @@ msgstr "Dölj %s"
msgid "Quit"
msgstr "Avsluta"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Redigera"
@@ -837,12 +840,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Anteckningar och inställningar lagras i: %s"
msgid "Browse..."
msgstr "Bläddra..."
msgid "Check synchronisation configuration"
msgstr "Kontrollera synkroniseringskonfigurationen"
msgid "Browse..."
msgstr "Bläddra..."
msgid "Apply"
msgstr "Tillämpa"
@@ -1007,9 +1010,33 @@ msgstr "Plats"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Anteckningsboken"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "Anteckningens egenskaper"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Denna anteckning har ändrats:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Öppna..."
@@ -1319,6 +1346,10 @@ msgstr "Pågår"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Synkronisering pågår redan. Tillstånd: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Krypterad"
@@ -1575,6 +1606,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "Ignorera TLS-certifikatfel"
#, fuzzy
msgid "Enable note history"
msgstr "Aktivera kryptering"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ogiltigt inställningsvärde: \"%s\". Möjliga värden är: %s."
@@ -1645,12 +1690,19 @@ msgstr "Det finns ingen data att exportera."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Ange anteckningsboken som anteckningarna ska importeras till."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Objekt som inte kan synkroniseras"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Den här filen kunde inte öppnas: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Den här filen kunde inte öppnas: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1936,9 +1988,8 @@ msgstr ""
msgid "Welcome"
msgstr "Välkommen"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Denna anteckning har ändrats:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -632,6 +632,9 @@ msgstr "Gizle %s"
msgid "Quit"
msgstr "Çıkış"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "Düzenle"
@@ -805,12 +808,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "Notlar ve ayarlar şu konumda saklanır: %s"
msgid "Browse..."
msgstr "Bul..."
msgid "Check synchronisation configuration"
msgstr "Senkronizasyon yapılandırmasını kontrol et"
msgid "Browse..."
msgstr "Bul..."
msgid "Apply"
msgstr "Uygula"
@@ -976,9 +979,33 @@ msgstr "Konum"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "Not listesi"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "Not özellikleri"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "Bu not değiştirildi:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "Aç..."
@@ -1288,6 +1315,10 @@ msgstr "Devam etmekte"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Senkronizasyon zaten devam ediyor. Durum: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "Şifrelenmiş"
@@ -1542,6 +1573,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "TLS sertifikası hatalarını yoksay"
#, fuzzy
msgid "Enable note history"
msgstr "Şifrelemeyi etkinleştir"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Geçersiz seçenek değeri: \"%s\". Mümkün değerler: %s."
@@ -1611,12 +1656,19 @@ msgstr "Çıkartılacak veri bulunmuyor."
msgid "Please specify the notebook where the notes should be imported to."
msgstr "Lütfen notların alınacağı not defterini belirtin."
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "Senkronize edilemeyen öğeler"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "Bu dosya açılamadı: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "Bu dosya açılamadı: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1895,9 +1947,8 @@ msgstr ""
msgid "Welcome"
msgstr "Hoşgeldiniz"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "Bu not değiştirildi:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -622,6 +622,9 @@ msgstr "隐藏 %s"
msgid "Quit"
msgstr "退出"
msgid "Close Window"
msgstr ""
msgid "&Edit"
msgstr "编辑 (&E)"
@@ -785,12 +788,12 @@ msgstr "该授权令牌仅用于允许第三方应用程序访问 Joplin。"
msgid "Notes and settings are stored in: %s"
msgstr "笔记与设置文件储存在:%s"
msgid "Browse..."
msgstr "浏览..."
msgid "Check synchronisation configuration"
msgstr "检查同步配置"
msgid "Browse..."
msgstr "浏览..."
msgid "Apply"
msgstr "应用"
@@ -944,9 +947,33 @@ msgstr "位置"
msgid "URL"
msgstr "URL"
#, fuzzy
msgid "Note History"
msgstr "笔记列表"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr "笔记属性"
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "该笔记已被修改:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "打开…"
@@ -1249,6 +1276,10 @@ msgstr "正在进行"
msgid "Synchronisation is already in progress. State: %s"
msgstr "已经在同步。状态:%s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "已加密"
@@ -1499,6 +1530,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "忽略 TLS 证书错误"
#, fuzzy
msgid "Enable note history"
msgstr "目录"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "无效的选项值:\"%s\"。可用值有:%s。"
@@ -1566,12 +1611,19 @@ msgstr "没有可导出的数据。"
msgid "Please specify the notebook where the notes should be imported to."
msgstr "请指定导入笔记的目标笔记本。"
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "无法同步项目"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "该笔记本无法打开:%s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "该笔记本无法打开:%s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1842,9 +1894,8 @@ msgstr "您目前未有笔记本。点击 (+) 按钮创建。"
msgid "Welcome"
msgstr "欢迎"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "该笔记已被修改:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid "Table of contents"
#~ msgstr "目录"

View File

@@ -627,6 +627,9 @@ msgstr "隱藏 %s"
msgid "Quit"
msgstr "結束"
msgid "Close Window"
msgstr ""
#, fuzzy
msgid "&Edit"
msgstr "編輯"
@@ -798,12 +801,12 @@ msgstr ""
msgid "Notes and settings are stored in: %s"
msgstr "所有記事和設置均儲存於: %s"
msgid "Browse..."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "檢測同步設置"
msgid "Browse..."
msgstr ""
msgid "Apply"
msgstr "套用"
@@ -958,9 +961,33 @@ msgstr ""
msgid "URL"
msgstr ""
#, fuzzy
msgid "Note History"
msgstr "記事本"
msgid "Previous versions of this note"
msgstr ""
msgid "Note properties"
msgstr ""
#, javascript-format
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr ""
#, fuzzy
msgid "This note has no history"
msgstr "此記事已被修改:"
msgid "Restore"
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 ""
msgid "Open..."
msgstr "開啟..."
@@ -1262,6 +1289,10 @@ msgstr "進行中"
msgid "Synchronisation is already in progress. State: %s"
msgstr "同步已在進行中。狀態: %s"
msgid ""
"Unknown item type downloaded - please upgrade Joplin to the latest version"
msgstr ""
msgid "Encrypted"
msgstr "已加密"
@@ -1508,6 +1539,20 @@ msgstr ""
msgid "Ignore TLS certificate errors"
msgstr "忽略 TLS 證書錯誤"
#, fuzzy
msgid "Enable note history"
msgstr "啟用加密"
msgid "days"
msgstr ""
#, javascript-format
msgid "%d days"
msgstr ""
msgid "Keep note history for"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "不正確選項值: \"%s\"。可能的值為: %s。"
@@ -1580,12 +1625,19 @@ msgstr "沒有資料可匯出。"
msgid "Please specify the notebook where the notes should be imported to."
msgstr "請指定將記事匯入到的筆記本。"
msgid "Restored Notes"
msgstr ""
msgid "Items that cannot be synchronised"
msgstr "無法同步的項目"
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s (%s): %s"
#, fuzzy, javascript-format
msgid "%s (%s) could not be uploaded: %s"
msgstr "無法開啟檔案: %s"
#, fuzzy, javascript-format
msgid "Item \"%s\" could not be downloaded: %s"
msgstr "無法開啟檔案: %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
@@ -1854,9 +1906,8 @@ msgstr "您當前沒有任何筆記本。通過按一下 (+) 鍵去建立一本
msgid "Welcome"
msgstr "歡迎"
#, fuzzy
#~ msgid "This note has no history"
#~ msgstr "此記事已被修改:"
#~ msgid "%s (%s): %s"
#~ msgstr "%s (%s): %s"
#~ msgid ""
#~ "The path to synchronise with when file system synchronisation is enabled. "

View File

@@ -1,6 +1,6 @@
{
"name": "joplin",
"version": "1.0.125",
"version": "1.0.137",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -210,7 +210,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@@ -560,6 +560,11 @@
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
"integrity": "sha1-PvqHMj67hj5mls67AILUj/PW96E="
},
"diff-match-patch": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz",
"integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg=="
},
"domexception": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
@@ -739,6 +744,11 @@
"resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz",
"integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU="
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
@@ -1435,9 +1445,9 @@
}
},
"joplin-turndown-plugin-gfm": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.7.tgz",
"integrity": "sha512-z0SveNcchtWwglkO7SgvDzPnVHYk1WumD0QRcWvUchIihqXwDVlve3G8AHkIhM69LY1YdC0HCZJlSMp2spBe/g=="
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.8.tgz",
"integrity": "sha512-uXgq2zGvjiMl/sXG7946EGhh1pyGbZ0L/6z21LBi8D6BJgHQufmXdve/UP3zpgnhiFhfXvzGY10uNaTuDQ99iQ=="
},
"jpeg-js": {
"version": "0.1.2",
@@ -1881,13 +1891,28 @@
}
},
"needle": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.2.1.tgz",
"integrity": "sha512-t/ZswCM9JTWjAdXS9VpvqhI2Ct2sL2MdY4fUXqGJaGBk13ge99ObqRksRTbBE56K+wxUXwwfZYOuZHifFW9q+Q==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.3.1.tgz",
"integrity": "sha512-CaLXV3W8Vnbps8ZANqDGz7j4x7Yj1LW4TWF/TQuDfj7Cfx4nAPTvw98qgTevtto1oHDrh3pQkaODbqupXlsWTg==",
"requires": {
"debug": "^2.1.2",
"debug": "^4.1.0",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"nextgen-events": {
@@ -1944,13 +1969,13 @@
}
},
"node-pre-gyp": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.2.tgz",
"integrity": "sha512-16lql9QTqs6KsB9fl3neWyZm02KxIKdI9FlJjrB0y7eMTP5Nyz+xalwPbOlw3iw7EejllJPmlJSnY711PLD1ug==",
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
"integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==",
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
"needle": "^2.2.0",
"needle": "^2.2.1",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
@@ -1958,13 +1983,6 @@
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4"
},
"dependencies": {
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
}
}
},
"noop-logger": {
@@ -1982,14 +2000,14 @@
}
},
"npm-bundled": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz",
"integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow=="
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g=="
},
"npm-packlist": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz",
"integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz",
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1"
@@ -2216,7 +2234,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"simple-get": {
@@ -2583,18 +2601,136 @@
"integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw="
},
"sqlite3": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.1.tgz",
"integrity": "sha512-i8LtU2fdEGFEt4Kcs7eNjYdGmnAQ8zWlaOv6Esbq/jfVfR0Qbn/1dgVyKebrMc2zN7h3oHsqla9zq7AJ0+34ZA==",
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.7.tgz",
"integrity": "sha512-TGEeSBB8O48bEu8KUUMqzeB22WrfTxzhIf0lFm8wLTo3a6yJBonF2sPKMYrYtOne1F1t9AHAEn+DTISq8WebQg==",
"requires": {
"nan": "~2.10.0",
"node-pre-gyp": "~0.10.1"
"nan": "^2.12.1",
"node-pre-gyp": "^0.11.0",
"request": "^2.87.0"
},
"dependencies": {
"nan": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
"ajv": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
"requires": {
"fast-deep-equal": "^2.0.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"aws4": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
},
"combined-stream": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"har-validator": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"requires": {
"ajv": "^6.5.5",
"har-schema": "^2.0.0"
}
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.0",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.4.3",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
}
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
}
}
},

View File

@@ -20,7 +20,7 @@
],
"owner": "Laurent Cozic"
},
"version": "1.0.125",
"version": "1.0.137",
"bin": {
"joplin": "./main.js"
},
@@ -33,7 +33,9 @@
"base-64": "^0.1.0",
"compare-version": "^0.1.2",
"diacritics": "^1.3.0",
"diff-match-patch": "^1.0.4",
"es6-promise-pool": "^2.5.0",
"file-uri-to-path": "^1.0.0",
"follow-redirects": "^1.2.4",
"form-data": "^2.1.4",
"fs-extra": "^5.0.0",
@@ -42,7 +44,7 @@
"image-data-uri": "^2.0.0",
"image-type": "^3.0.0",
"joplin-turndown": "^4.0.11",
"joplin-turndown-plugin-gfm": "^1.0.7",
"joplin-turndown-plugin-gfm": "^1.0.8",
"jssha": "^2.3.0",
"levenshtein": "^1.0.5",
"lodash": "^4.17.4",
@@ -62,7 +64,7 @@
"server-destroy": "^1.0.1",
"sharp": "^0.22.1",
"sprintf-js": "^1.1.1",
"sqlite3": "^4.0.1",
"sqlite3": "^4.0.7",
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.0",
"strip-ansi": "^4.0.0",

View File

@@ -31,6 +31,7 @@ npm test tests-build/models_Folder.js
npm test tests-build/models_ItemChange.js
npm test tests-build/models_Note.js
npm test tests-build/models_Resource.js
npm test tests-build/models_Revision.js
npm test tests-build/models_Setting.js
npm test tests-build/models_Tag.js
npm test tests-build/pathUtils.js
@@ -38,6 +39,7 @@ npm test tests-build/services_InteropService.js
npm test tests-build/services_ResourceService.js
npm test tests-build/services_rest_Api.js
npm test tests-build/services_SearchEngine.js
npm test tests-build/services_Revision.js
npm test tests-build/StringUtils.js
npm test tests-build/synchronizer.js
npm test tests-build/urlUtils.js

View File

@@ -25,8 +25,7 @@ describe('Encryption', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
//await setupDatabaseAndSynchronizer(2);
//await switchClient(1);
await switchClient(1);
service = new EncryptionService();
BaseItem.encryptionService_ = service;
Setting.setValue('encryption.enabled', true);

View File

@@ -0,0 +1,42 @@
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Official Things</strong></td>
<td></td>
</tr>
<tr>
<td><a href="https://nim-lang.org">Web Site</a></td>
<td>The project’s entry point</td>
</tr>
<tr>
<td><a href="https://github.com/nim-lang/nim">Source</a></td>
<td>The github project</td>
</tr>
<tr>
<td><a href="https://github.com/nim-lang/nimble">nimble</a></td>
<td>The nim package manager</td>
</tr>
<tr>
<td><a href="https://github.com/dom96/choosenim">choosenim</a></td>
<td>Toolchain installer</td>
</tr>
<tr>
<td>&nbsp;</td>
<td></td>
</tr>
<tr>
<td><strong>Community</strong></td>
<td></td>
</tr>
<tr>
<td><a href="https://forum.nim-lang.org">Forums</a></td>
<td>An async discussion board</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,9 @@
| | |
| --- | --- |
| **Official Things** | |
| [Web Site](https://nim-lang.org) | The project’s entry point |
| [Source](https://github.com/nim-lang/nim) | The github project |
| [nimble](https://github.com/nim-lang/nimble) | The nim package manager |
| [choosenim](https://github.com/dom96/choosenim) | Toolchain installer |
| **Community** | |
| [Forums](https://forum.nim-lang.org) | An async discussion board |

View File

@@ -47,5 +47,20 @@ describe('models_BaseItem', function() {
expect('ignore_me' in unserialized).toBe(false);
}));
it('should not modify title when unserializing', asyncTest(async () => {
let folder1 = await Folder.save({ title: "" });
let folder2 = await Folder.save({ title: "folder1" });
let serialized1 = await Folder.serialize(folder1);
let unserialized1 = await Folder.unserialize(serialized1);
expect(unserialized1.title).toBe(folder1.title);
let serialized2 = await Folder.serialize(folder2);
let unserialized2 = await Folder.unserialize(serialized2);
expect(unserialized2.title).toBe(folder2.title);
}));
});

View File

@@ -1,7 +1,7 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const { asyncTest, fileContentEqual, revisionService, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const SearchEngine = require('lib/services/SearchEngine');
const ResourceService = require('lib/services/ResourceService');
const ItemChangeUtils = require('lib/services/ItemChangeUtils');
@@ -34,19 +34,17 @@ describe('models_ItemChange', function() {
const resourceService = new ResourceService();
await searchEngine.syncTables();
// If we run this now, it should not delete any change because
// the resource service has not yet processed the change
await ItemChangeUtils.deleteProcessedChanges();
expect(await ItemChange.lastChangeId()).toBe(1);
await resourceService.indexNoteResources();
// Now that the resource service has processed the change,
// the change can be deleted.
await ItemChangeUtils.deleteProcessedChanges();
expect(await ItemChange.lastChangeId()).toBe(1);
await revisionService().collectRevisions();
await ItemChangeUtils.deleteProcessedChanges();
expect(await ItemChange.lastChangeId()).toBe(0);
}));

View File

@@ -86,5 +86,32 @@ describe('models_Note', function() {
expect(changedNote === note1).toBe(false);
expect(!!changedNote.is_todo).toBe(false);
}));
it('should serialize and unserialize without modifying data', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1"});
const testCases = [
[ {title: '', body:'Body and no title\nSecond line\nThird Line', parent_id: folder1.id},
'', 'Body and no title\nSecond line\nThird Line'],
[ {title: 'Note title', body:'Body and title', parent_id: folder1.id},
'Note title', 'Body and title'],
[ {title: 'Title and no body', body:'', parent_id: folder1.id},
'Title and no body', ''],
]
for (let i = 0; i < testCases.length; i++) {
const t = testCases[i];
const input = t[0];
const expectedTitle = t[1];
const expectedBody = t[1];
let note1 = await Note.save(input);
let serialized = await Note.serialize(note1);
let unserialized = await Note.unserialize( serialized);
expect(unserialized.title).toBe(input.title);
expect(unserialized.body).toBe(input.body);
}
}));
});

View File

@@ -8,10 +8,14 @@ const Resource = require('lib/models/Resource.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim');
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
const testImagePath = __dirname + '/../tests/support/photo.jpg';
describe('models_Resource', function() {
beforeEach(async (done) => {
@@ -23,7 +27,7 @@ describe('models_Resource', function() {
it('should have a "done" fetch_status when created locally', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
await shim.attachFileToNote(note1, testImagePath);
let resource1 = (await Resource.all())[0];
let ls = await Resource.localState(resource1);
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_DONE);
@@ -32,7 +36,7 @@ describe('models_Resource', function() {
it('should have a default local state', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
await shim.attachFileToNote(note1, testImagePath);
let resource1 = (await Resource.all())[0];
let ls = await Resource.localState(resource1);
expect(!ls.id).toBe(true);
@@ -43,7 +47,7 @@ describe('models_Resource', function() {
it('should save and delete local state', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
await shim.attachFileToNote(note1, testImagePath);
let resource1 = (await Resource.all())[0];
await Resource.setLocalState(resource1, { fetch_status: Resource.FETCH_STATUS_IDLE });
@@ -56,4 +60,31 @@ describe('models_Resource', function() {
expect(!ls.id).toBe(true);
}));
it('should resize the resource if the image is below the required dimensions', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
const previousMax = Resource.IMAGE_MAX_DIMENSION;
Resource.IMAGE_MAX_DIMENSION = 5;
await shim.attachFileToNote(note1, testImagePath);
Resource.IMAGE_MAX_DIMENSION = previousMax;
let resource1 = (await Resource.all())[0];
const originalStat = await shim.fsDriver().stat(testImagePath);
const newStat = await shim.fsDriver().stat(Resource.fullPath(resource1));
expect(newStat.size < originalStat.size).toBe(true);
}));
it('should not resize the resource if the image is below the required dimensions', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, testImagePath);
let resource1 = (await Resource.all())[0];
const originalStat = await shim.fsDriver().stat(testImagePath);
const newStat = await shim.fsDriver().stat(Resource.fullPath(resource1));
expect(originalStat.size).toBe(newStat.size);
}));
});

View File

@@ -0,0 +1,71 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const NoteTag = require('lib/models/NoteTag.js');
const Tag = require('lib/models/Tag.js');
const Revision = require('lib/models/Revision.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('models_Revision', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should create patches of text and apply it', asyncTest(async () => {
const note1 = await Note.save({ body: 'my note\nsecond line' });
const patch = Revision.createTextPatch(note1.body, 'my new note\nsecond line');
const merged = Revision.applyTextPatch(note1.body, patch);
expect(merged).toBe('my new note\nsecond line');
}));
it('should create patches of objects and apply it', asyncTest(async () => {
const oldObject = {
one: '123',
two: '456',
three: '789',
};
const newObject = {
one: '123',
three: '999',
}
const patch = Revision.createObjectPatch(oldObject, newObject);
const merged = Revision.applyObjectPatch(oldObject, patch);
expect(JSON.stringify(merged)).toBe(JSON.stringify(newObject));
}));
it('should move target revision to the top', asyncTest(async () => {
const revs = [
{ id: '123' },
{ id: '456' },
{ id: '789' },
];
let newRevs;
newRevs = Revision.moveRevisionToTop({ id: '456' }, revs);
expect(newRevs[0].id).toBe('123');
expect(newRevs[1].id).toBe('789');
expect(newRevs[2].id).toBe('456');
newRevs = Revision.moveRevisionToTop({ id: '789' }, revs);
expect(newRevs[0].id).toBe('123');
expect(newRevs[1].id).toBe('456');
expect(newRevs[2].id).toBe('789');
}));
});

View File

@@ -0,0 +1,420 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const Folder = require('lib/models/Folder.js');
const Setting = require('lib/models/Setting.js');
const Note = require('lib/models/Note.js');
const NoteTag = require('lib/models/NoteTag.js');
const ItemChange = require('lib/models/ItemChange.js');
const Tag = require('lib/models/Tag.js');
const Revision = require('lib/models/Revision.js');
const BaseModel = require('lib/BaseModel.js');
const RevisionService = require('lib/services/RevisionService.js');
const { shim } = require('lib/shim');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('services_Revision', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
Setting.setValue('revisionService.intervalBetweenRevisions', 0)
done();
});
it('should create diff and rebuild notes', asyncTest(async () => {
const service = new RevisionService();
const n1_v1 = await Note.save({ title: '', author: 'testing' });
await service.collectRevisions();
await Note.save({ id: n1_v1.id, title: 'hello', author: 'testing' });
await service.collectRevisions();
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'hello welcome', author: '' });
await service.collectRevisions();
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1_v1.id);
expect(revisions.length).toBe(2);
expect(revisions[1].parent_id).toBe(revisions[0].id);
const rev1 = await service.revisionNote(revisions, 0);
expect(rev1.title).toBe('hello');
expect(rev1.author).toBe('testing');
const rev2 = await service.revisionNote(revisions, 1);
expect(rev2.title).toBe('hello welcome');
expect(rev2.author).toBe('');
await time.sleep(0.5);
await service.deleteOldRevisions(400);
const revisions2 = await Revision.allByType(BaseModel.TYPE_NOTE, n1_v1.id);
expect(revisions2.length).toBe(0);
}));
it('should delete old revisions (1 note, 2 rev)', asyncTest(async () => {
const service = new RevisionService();
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await service.collectRevisions();
await time.sleep(1);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await service.collectRevisions();
expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1_v1.id)).length).toBe(2);
await service.deleteOldRevisions(1000);
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1_v1.id);
expect(revisions.length).toBe(1);
const rev1 = await service.revisionNote(revisions, 0);
expect(rev1.title).toBe('hello welcome');
}));
it('should delete old revisions (1 note, 3 rev)', asyncTest(async () => {
const service = new RevisionService();
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'one' });
await service.collectRevisions();
await time.sleep(1);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'one two' });
await service.collectRevisions();
await time.sleep(1);
const n1_v3 = await Note.save({ id: n1_v1.id, title: 'one two three' });
await service.collectRevisions();
{
await service.deleteOldRevisions(2000);
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1_v1.id);
expect(revisions.length).toBe(2);
const rev1 = await service.revisionNote(revisions, 0);
expect(rev1.title).toBe('one two');
const rev2 = await service.revisionNote(revisions, 1);
expect(rev2.title).toBe('one two three');
}
{
await service.deleteOldRevisions(1000);
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1_v1.id);
expect(revisions.length).toBe(1);
const rev1 = await service.revisionNote(revisions, 0);
expect(rev1.title).toBe('one two three');
}
}));
it('should delete old revisions (2 notes, 2 rev)', asyncTest(async () => {
const service = new RevisionService();
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'note 1' });
const n2_v0 = await Note.save({ title: '' });
const n2_v1 = await Note.save({ id: n2_v0.id, title: 'note 2' });
await service.collectRevisions();
await time.sleep(1);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'note 1 (v2)' });
const n2_v2 = await Note.save({ id: n2_v1.id, title: 'note 2 (v2)' });
await service.collectRevisions();
expect((await Revision.all()).length).toBe(4);
await service.deleteOldRevisions(1000);
{
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1_v1.id);
expect(revisions.length).toBe(1);
const rev1 = await service.revisionNote(revisions, 0);
expect(rev1.title).toBe('note 1 (v2)');
}
{
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n2_v1.id);
expect(revisions.length).toBe(1);
const rev1 = await service.revisionNote(revisions, 0);
expect(rev1.title).toBe('note 2 (v2)');
}
}));
it('should handle conflicts', asyncTest(async () => {
const service = new RevisionService();
// A conflict happens in this case:
// - Device 1 creates note1 (rev1)
// - Device 2 syncs and get note1
// - Device 1 modifies note1 (rev2)
// - Device 2 modifies note1 (rev3)
// When reconstructing the notes based on the revisions, we need to make sure it follow the right
// "path". For example, to reconstruct the note at rev2 it would be:
// rev1 => rev2
// To reconstruct the note at rev3 it would be:
// rev1 => rev3
// And not, for example, rev1 => rev2 => rev3
const n1_v1 = await Note.save({ title: 'hello' });
const noteId = n1_v1.id;
const rev1 = await service.createNoteRevision_(n1_v1);
const n1_v2 = await Note.save({ id: noteId, title: 'hello Paul' });
const rev2 = await service.createNoteRevision_(n1_v2, rev1.id);
const n1_v3 = await Note.save({ id: noteId, title: 'hello John' });
const rev3 = await service.createNoteRevision_(n1_v3, rev1.id);
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, noteId);
expect(revisions.length).toBe(3);
expect(revisions[1].parent_id).toBe(rev1.id);
expect(revisions[2].parent_id).toBe(rev1.id);
const revNote1 = await service.revisionNote(revisions, 0);
const revNote2 = await service.revisionNote(revisions, 1);
const revNote3 = await service.revisionNote(revisions, 2);
expect(revNote1.title).toBe('hello');
expect(revNote2.title).toBe('hello Paul');
expect(revNote3.title).toBe('hello John');
}));
it('should create a revision for notes that are older than a given interval', asyncTest(async () => {
const n1 = await Note.save({ title: 'hello' });
const noteId = n1.id;
await sleep(0.1);
// Set the interval in such a way that the note is considered an old one.
Setting.setValue('revisionService.oldNoteInterval', 50);
// A revision is created the first time a note is overwritten with new content, and
// if this note doesn't already have an existing revision.
// This is mostly to handle old notes that existed before the revision service. If these
// old notes are changed, there's a chance it's accidental or due to some bug, so we
// want to preserve a revision just in case.
{
await Note.save({ id: noteId, title: 'hello 2' });
await revisionService().collectRevisions(); // Rev for old note created + Rev for new note
const all = await Revision.allByType(BaseModel.TYPE_NOTE, noteId);
expect(all.length).toBe(2);
const revNote1 = await revisionService().revisionNote(all, 0);
const revNote2 = await revisionService().revisionNote(all, 1);
expect(revNote1.title).toBe('hello');
expect(revNote2.title).toBe('hello 2');
}
// If the note is saved a third time, we don't automatically create a revision. One
// will be created x minutes later when the service collects revisions.
{
await Note.save({ id: noteId, title: 'hello 3' });
const all = await Revision.allByType(BaseModel.TYPE_NOTE, noteId);
expect(all.length).toBe(2);
}
}));
it('should create a revision for notes that get deleted (recyle bin)', asyncTest(async () => {
const n1 = await Note.save({ title: 'hello' });
const noteId = n1.id;
await Note.delete(noteId);
await revisionService().collectRevisions();
const all = await Revision.allByType(BaseModel.TYPE_NOTE, noteId);
expect(all.length).toBe(1);
const rev1 = await revisionService().revisionNote(all, 0);
expect(rev1.title).toBe('hello');
}));
it('should not create a revision for notes that get deleted if there is already a revision', asyncTest(async () => {
const n1 = await Note.save({ title: 'hello' });
await revisionService().collectRevisions();
const noteId = n1.id;
await Note.save({ id: noteId, title: 'hello Paul' });
await revisionService().collectRevisions(); // REV 1
expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1.id)).length).toBe(1);
await Note.delete(noteId);
// At this point there is no need to create a new revision for the deleted note
// because we already have the latest version as REV 1
await revisionService().collectRevisions();
expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1.id)).length).toBe(1);
}));
it('should not create a revision for new note the first time they are saved', asyncTest(async () => {
const n1 = await Note.save({ title: 'hello' });
{
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(revisions.length).toBe(0);
}
await revisionService().collectRevisions();
{
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(revisions.length).toBe(0);
}
}));
it('should abort collecting revisions when one of them is encrypted', asyncTest(async () => {
const n1 = await Note.save({ title: 'hello' }); // CHANGE 1
await revisionService().collectRevisions();
await Note.save({ id: n1.id, title: 'hello Ringo' }); // CHANGE 2
await revisionService().collectRevisions();
await Note.save({ id: n1.id, title: 'hello George' }); // CHANGE 3
await revisionService().collectRevisions();
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(revisions.length).toBe(2);
const encryptedRevId = revisions[0].id;
// Simulate receiving an encrypted revision
await Revision.save({ id: encryptedRevId, encryption_applied: 1 });
await Note.save({ id: n1.id, title: 'hello Paul' }); // CHANGE 4
await revisionService().collectRevisions();
// Although change 4 is a note update, check that it has not been processed
// by the collector, due to one of the revisions being encrypted.
expect(await ItemChange.lastChangeId()).toBe(4);
expect(Setting.value('revisionService.lastProcessedChangeId')).toBe(3);
// Simulate the revision being decrypted by DecryptionService
await Revision.save({ id: encryptedRevId, encryption_applied: 0 });
await revisionService().collectRevisions();
// Now that the revision has been decrypted, all the changes can be processed
expect(await ItemChange.lastChangeId()).toBe(4);
expect(Setting.value('revisionService.lastProcessedChangeId')).toBe(4);
}));
it('should not delete old revisions if one of them is still encrypted (1)', asyncTest(async () => {
// Test case 1: Two revisions and the first one is encrypted.
// Calling deleteOldRevisions() with low TTL, which means all revisions
// should be deleted, but they won't be due to the encrypted one.
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
await time.sleep(0.1);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await revisionService().collectRevisions(); // REV 2
await time.sleep(0.1);
expect((await Revision.all()).length).toBe(2);
const revisions = await Revision.all();
await Revision.save({ id: revisions[0].id, encryption_applied: 1 });
await revisionService().deleteOldRevisions(0);
expect((await Revision.all()).length).toBe(2);
await Revision.save({ id: revisions[0].id, encryption_applied: 0 });
await revisionService().deleteOldRevisions(0);
expect((await Revision.all()).length).toBe(0);
}));
it('should not delete old revisions if one of them is still encrypted (2)', asyncTest(async () => {
// Test case 2: Two revisions and the first one is encrypted.
// Calling deleteOldRevisions() with higher TTL, which means the oldest
// revision should be deleted, but it won't be due to the encrypted one.
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
await time.sleep(0.5);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await revisionService().collectRevisions(); // REV 2
expect((await Revision.all()).length).toBe(2);
const revisions = await Revision.all();
await Revision.save({ id: revisions[0].id, encryption_applied: 1 });
await revisionService().deleteOldRevisions(500);
expect((await Revision.all()).length).toBe(2);
}));
it('should not delete old revisions if one of them is still encrypted (3)', asyncTest(async () => {
// Test case 2: Two revisions and the second one is encrypted.
// Calling deleteOldRevisions() with higher TTL, which means the oldest
// revision should be deleted, but it won't be due to the encrypted one.
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
await time.sleep(0.5);
const n1_v2 = await Note.save({ id: n1_v1.id, title: 'hello welcome' });
await revisionService().collectRevisions(); // REV 2
expect((await Revision.all()).length).toBe(2);
const revisions = await Revision.all();
await Revision.save({ id: revisions[1].id, encryption_applied: 1 });
await revisionService().deleteOldRevisions(500);
expect((await Revision.all()).length).toBe(2);
await Revision.save({ id: revisions[1].id, encryption_applied: 0 });
await revisionService().deleteOldRevisions(500);
expect((await Revision.all()).length).toBe(1);
}));
it('should not create a revision if the note has not changed', asyncTest(async () => {
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
expect((await Revision.all()).length).toBe(1);
const n1_v2 = await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // Note has not changed (except its timestamp) so don't create a revision
expect((await Revision.all()).length).toBe(1);
}));
it('should preserve user update time', asyncTest(async () => {
// user_updated_time is kind of tricky and can be changed automatically in various
// places so make sure it is saved correctly with the revision
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
expect((await Revision.all()).length).toBe(1);
const userUpdatedTime = Date.now() - 1000 * 60 * 60;
const n1_v2 = await Note.save({ id: n1_v0.id, title: 'hello', updated_time: Date.now(), user_updated_time: userUpdatedTime }, { autoTimestamp: false });
await revisionService().collectRevisions(); // Only the user timestamp has changed, but that needs to be saved
const revisions = await Revision.all();
expect(revisions.length).toBe(2);
const revNote = await revisionService().revisionNote(revisions, 1);
expect(revNote.user_updated_time).toBe(userUpdatedTime);
}));
it('should not create a revision if there is already a recent one', asyncTest(async () => {
const n1_v0 = await Note.save({ title: '' });
const n1_v1 = await Note.save({ id: n1_v0.id, title: 'hello' });
await revisionService().collectRevisions(); // REV 1
const n1_v2 = await Note.save({ id: n1_v0.id, title: 'hello 2' });
await revisionService().collectRevisions(); // REV 2
expect((await Revision.all()).length).toBe(2);
Setting.setValue('revisionService.intervalBetweenRevisions', 1000);
const n1_v3 = await Note.save({ id: n1_v0.id, title: 'hello 3' });
await revisionService().collectRevisions(); // No rev because there's already a rev that is less than 1000 ms old
expect((await Revision.all()).length).toBe(2);
}));
});

View File

@@ -1,7 +1,7 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { setupDatabase, allSyncTargetItemsEncrypted, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js');
const { setupDatabase, allSyncTargetItemsEncrypted, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js');
const { shim } = require('lib/shim.js');
const fs = require('fs-extra');
const Folder = require('lib/models/Folder.js');
@@ -13,6 +13,7 @@ const { Database } = require('lib/database.js');
const Setting = require('lib/models/Setting.js');
const MasterKey = require('lib/models/MasterKey');
const BaseItem = require('lib/models/BaseItem.js');
const Revision = require('lib/models/Revision.js');
const BaseModel = require('lib/BaseModel.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const WelcomeUtils = require('lib/WelcomeUtils');
@@ -23,19 +24,44 @@ process.on('unhandledRejection', (reason, p) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 + 30000; // The first test is slow because the database needs to be built
async function allItems() {
async function allNotesFolders() {
let folders = await Folder.all();
let notes = await Note.all();
return folders.concat(notes);
}
async function localItemsSameAsRemote(locals, expect) {
async function remoteItemsByTypes(types) {
const list = await fileApi().list();
if (list.has_more) throw new Error('Not implemented!!!');
const files = list.items;
const output = [];
for (const file of files) {
const remoteContent = await fileApi().get(file.path);
const content = await BaseItem.unserialize(remoteContent);
if (types.indexOf(content.type_) < 0) continue;
output.push(content);
}
return output;
}
async function remoteNotesAndFolders() {
return remoteItemsByTypes([BaseModel.TYPE_NOTE, BaseModel.TYPE_FOLDER]);
}
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 {
let files = await fileApi().list();
files = files.items;
expect(locals.length).toBe(files.length);
const nf = await remoteNotesAndFolders();
expect(locals.length).toBe(nf.length);
for (let i = 0; i < locals.length; i++) {
let dbItem = locals[i];
@@ -45,12 +71,6 @@ async function localItemsSameAsRemote(locals, expect) {
expect(!!remote).toBe(true);
if (!remote) continue;
// if (syncTargetId() == SyncTargetRegistry.nameToId('filesystem')) {
// expect(remote.updated_time).toBe(Math.floor(dbItem.updated_time / 1000) * 1000);
// } else {
// expect(remote.updated_time).toBe(dbItem.updated_time);
// }
let remoteContent = await fileApi().get(path);
remoteContent = dbItem.type_ == BaseModel.TYPE_NOTE ? await Note.unserialize(remoteContent) : await Folder.unserialize(remoteContent);
@@ -82,11 +102,11 @@ describe('Synchronizer', function() {
let folder = await Folder.save({ title: "folder1" });
await Note.save({ title: "un", parent_id: folder.id });
let all = await allItems();
let all = await allNotesFolders();
await synchronizer().start();
await localItemsSameAsRemote(all, expect);
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should update remote items', asyncTest(async () => {
@@ -96,10 +116,10 @@ describe('Synchronizer', function() {
await Note.save({ title: "un UPDATE", id: note.id });
let all = await allItems();
let all = await allNotesFolders();
await synchronizer().start();
await localItemsSameAsRemote(all, expect);
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should create local items', asyncTest(async () => {
@@ -111,9 +131,9 @@ describe('Synchronizer', function() {
await synchronizer().start();
let all = await allItems();
let all = await allNotesFolders();
await localItemsSameAsRemote(all, expect);
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should update local items', asyncTest(async () => {
@@ -138,9 +158,9 @@ describe('Synchronizer', function() {
await synchronizer().start();
let all = await allItems();
let all = await allNotesFolders();
await localItemsSameAsRemote(all, expect);
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should resolve note conflicts', asyncTest(async () => {
@@ -232,11 +252,9 @@ describe('Synchronizer', function() {
await synchronizer().start();
let files = await fileApi().list();
files = files.items;
expect(files.length).toBe(1);
expect(files[0].path).toBe(Folder.systemPath(folder1));
const remotes = await remoteNotesAndFolders();
expect(remotes.length).toBe(1);
expect(remotes[0].id).toBe(folder1.id);
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
@@ -279,7 +297,7 @@ describe('Synchronizer', function() {
await switchClient(1);
context1 = await synchronizer().start({ context: context1 });
let items = await allItems();
let items = await allNotesFolders();
expect(items.length).toBe(2);
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
@@ -302,8 +320,8 @@ describe('Synchronizer', function() {
await synchronizer().start();
let all = await allItems();
await localItemsSameAsRemote(all, expect);
let all = await allNotesFolders();
await localNotesFoldersSameAsRemote(all, expect);
}));
it('should delete local folder', asyncTest(async () => {
@@ -320,8 +338,8 @@ describe('Synchronizer', function() {
await switchClient(1);
await synchronizer().start({ context: context1 });
let items = await allItems();
await localItemsSameAsRemote(items, expect);
let items = await allNotesFolders();
await localNotesFoldersSameAsRemote(items, expect);
}));
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', asyncTest(async () => {
@@ -338,7 +356,7 @@ describe('Synchronizer', function() {
let note = await Note.save({ title: "note1", parent_id: folder1.id });
await synchronizer().start();
let items = await allItems();
let items = await allNotesFolders();
expect(items.length).toBe(1);
expect(items[0].title).toBe('note1');
expect(items[0].is_conflict).toBe(1);
@@ -360,11 +378,11 @@ describe('Synchronizer', function() {
await Note.delete(note.id);
await synchronizer().start();
let items = await allItems();
let items = await allNotesFolders();
expect(items.length).toBe(1);
expect(items[0].title).toBe('folder');
await localItemsSameAsRemote(items, expect);
await localNotesFoldersSameAsRemote(items, expect);
}));
it('should cross delete all folders', asyncTest(async () => {
@@ -393,13 +411,13 @@ describe('Synchronizer', function() {
await synchronizer().start();
let items2 = await allItems();
let items2 = await allNotesFolders();
await switchClient(1);
await synchronizer().start();
let items1 = await allItems();
let items1 = await allNotesFolders();
expect(items1.length).toBe(0);
expect(items1.length).toBe(items2.length);
@@ -462,7 +480,7 @@ describe('Synchronizer', function() {
await synchronizer().start();
let items = await allItems();
let items = await allNotesFolders();
expect(items.length).toBe(1);
}));
@@ -680,7 +698,7 @@ describe('Synchronizer', function() {
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(0);
await Note.save({ id: noteId, title: "un mod", });
synchronizer().testingHooks_ = ['rejectedByTarget'];
synchronizer().testingHooks_ = ['notesRejectedByTarget'];
await synchronizer().start();
synchronizer().testingHooks_ = [];
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
@@ -833,8 +851,8 @@ describe('Synchronizer', function() {
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
let resource1 = (await Resource.all())[0];
let resourcePath1 = Resource.fullPath(resource1);
await synchronizer().start();
expect((await fileApi().list()).items.length).toBe(3);
await synchronizer().start();
expect((await remoteNotesFoldersResources()).length).toBe(3);
await switchClient(2);
@@ -885,6 +903,29 @@ describe('Synchronizer', function() {
expect(ls.fetch_error).toBe('did not work');
}));
it('should set the resource file size if it is missing', asyncTest(async () => {
while (insideBeforeEach) await time.msleep(500);
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
let r1 = (await Resource.all())[0];
await Resource.setFileSizeOnly(r1.id, -1);
r1 = await Resource.load(r1.id);
expect(r1.size).toBe(-1);
const fetcher = new ResourceFetcher(() => { return synchronizer().api() });
fetcher.queueDownload(r1.id);
await fetcher.waitForAllFinished();
r1 = await Resource.load(r1.id);
expect(r1.size).toBe(2720);
}));
it('should delete resources', asyncTest(async () => {
while (insideBeforeEach) await time.msleep(500);
@@ -901,11 +942,10 @@ describe('Synchronizer', function() {
let allResources = await Resource.all();
expect(allResources.length).toBe(1);
let all = await fileApi().list();
expect(all.items.length).toBe(3);
expect((await remoteNotesFoldersResources()).length).toBe(3);
await Resource.delete(resource1.id);
await synchronizer().start();
all = await fileApi().list();
expect(all.items.length).toBe(2);
expect((await remoteNotesFoldersResources()).length).toBe(2);
await switchClient(1);
@@ -998,6 +1038,33 @@ describe('Synchronizer', function() {
expect(allEncrypted).toBe(false);
}));
it('should set the resource file size after decryption', asyncTest(async () => {
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
let resource1 = (await Resource.all())[0];
await Resource.setFileSizeOnly(resource1.id, -1);
let resourcePath1 = Resource.fullPath(resource1);
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
await encryptionService().loadMasterKeysFromSettings();
const fetcher = new ResourceFetcher(() => { return synchronizer().api() });
fetcher.queueDownload(resource1.id);
await fetcher.waitForAllFinished();
await decryptionWorker().start();
const resource1_2 = await Resource.load(resource1.id);
expect(resource1_2.size).toBe(2720);
}));
it('should encrypt remote resources after encryption has been enabled', asyncTest(async () => {
while (insideBeforeEach) await time.msleep(100);
@@ -1036,11 +1103,11 @@ describe('Synchronizer', function() {
it('should create remote items with UTF-8 content', asyncTest(async () => {
let folder = await Folder.save({ title: "Fahrräder" });
await Note.save({ title: "Fahrräder", body: "Fahrräder", parent_id: folder.id });
let all = await allItems();
let all = await allNotesFolders();
await synchronizer().start();
await localItemsSameAsRemote(all, expect);
await localNotesFoldersSameAsRemote(all, expect);
}));
it("should update remote items but not pull remote changes", asyncTest(async () => {
@@ -1058,7 +1125,7 @@ describe('Synchronizer', function() {
await Note.save({ title: "un UPDATE", id: note.id });
await synchronizer().start({ syncSteps: ["update_remote"] });
let all = await allItems();
let all = await allNotesFolders();
expect(all.length).toBe(2);
await switchClient(2);
@@ -1110,4 +1177,178 @@ describe('Synchronizer', function() {
expect(tags.length).toBe(2);
}));
it("should not save revisions when updating a note via sync", asyncTest(async () => {
// When a note is updated, a revision of the original is created.
// Here, on client 1, the note is updated for the first time, however since it is
// via sync, we don't create a revision - that revision has already been created on client
// 2 and is going to be synced.
const n1 = await Note.save({ title: 'testing' });
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
await Note.save({ id: n1.id, title: 'mod from client 2' });
await revisionService().collectRevisions();
const allRevs1 = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs1.length).toBe(1);
await synchronizer().start();
await switchClient(1);
await synchronizer().start();
const allRevs2 = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs2.length).toBe(1);
expect(allRevs2[0].id).toBe(allRevs1[0].id);
}));
it("should not save revisions when deleting a note via sync", asyncTest(async () => {
const n1 = await Note.save({ title: 'testing' });
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
await Note.delete(n1.id);
await revisionService().collectRevisions(); // REV 1
{
const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs.length).toBe(1);
}
await synchronizer().start();
await switchClient(1);
await synchronizer().start(); // The local note gets deleted here, however a new rev is *not* created
{
const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs.length).toBe(1);
}
const notes = await Note.all();
expect(notes.length).toBe(0);
}));
it("should not save revisions when an item_change has been generated as a result of a sync", asyncTest(async () => {
// When a note is modified an item_change object is going to be created. This
// is used for example to tell the search engine, when note should be indexed. It is
// also used by the revision service to tell what note should get a new revision.
// When a note is modified via sync, this item_change object is also created. The issue
// is that we don't want to create revisions for these particular item_changes, because
// such revision has already been created on another client (whatever client initially
// modified the note), and that rev is going to be synced.
//
// So in the end we need to make sure that we don't create these unecessary additional revisions.
const n1 = await Note.save({ title: 'testing' });
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
await Note.save({ id: n1.id, title: 'mod from client 2' });
await revisionService().collectRevisions();
await synchronizer().start();
await switchClient(1);
await synchronizer().start();
{
const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs.length).toBe(1);
}
await revisionService().collectRevisions();
{
const allRevs = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(allRevs.length).toBe(1);
}
}));
it("should handle case when new rev is created on client, then older rev arrives later via sync", asyncTest(async () => {
// - C1 creates note 1
// - C1 modifies note 1 - REV1 created
// - C1 sync
// - C2 sync
// - C2 receives note 1
// - C2 modifies note 1 - REV2 created (but not based on REV1)
// - C2 receives REV1
//
// In that case, we need to make sure that REV1 and REV2 are both valid and can be retrieved.
// Even though REV1 was created before REV2, REV2 is *not* based on REV1. This is not ideal
// due to unecessary data being saved, but a possible edge case and we simply need to check
// all the data is valid.
const n1 = await Note.save({ title: 'note' });
await Note.save({ id: n1.id, title: 'note REV1' });
await revisionService().collectRevisions(); // REV1
expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1.id)).length).toBe(1);
await synchronizer().start();
await switchClient(2);
synchronizer().testingHooks_ = ['skipRevisions'];
await synchronizer().start();
synchronizer().testingHooks_ = [];
await Note.save({ id: n1.id, title: 'note REV2' });
await revisionService().collectRevisions(); // REV2
expect((await Revision.allByType(BaseModel.TYPE_NOTE, n1.id)).length).toBe(1);
await synchronizer().start(); // Sync the rev that had been skipped above with skipRevisions
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, n1.id);
expect(revisions.length).toBe(2);
expect((await revisionService().revisionNote(revisions, 0)).title).toBe('note REV1');
expect((await revisionService().revisionNote(revisions, 1)).title).toBe('note REV2');
}));
it("should not download resources over the limit", asyncTest(async () => {
const note1 = await Note.save({ title: 'note' });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
await synchronizer().start();
await switchClient(2);
const previousMax = synchronizer().maxResourceSize_;
synchronizer().maxResourceSize_ = 1;
await synchronizer().start();
synchronizer().maxResourceSize_ = previousMax;
const syncItems = await BaseItem.allSyncItems(syncTargetId());
expect(syncItems.length).toBe(2);
expect(syncItems[1].item_location).toBe(BaseItem.SYNC_ITEM_LOCATION_REMOTE);
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);
}));
});

View File

@@ -8,6 +8,7 @@ const ItemChange = require('lib/models/ItemChange.js');
const Resource = require('lib/models/Resource.js');
const Tag = require('lib/models/Tag.js');
const NoteTag = require('lib/models/NoteTag.js');
const Revision = require('lib/models/Revision.js');
const { Logger } = require('lib/logger.js');
const Setting = require('lib/models/Setting.js');
const MasterKey = require('lib/models/MasterKey');
@@ -31,12 +32,14 @@ const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
const EncryptionService = require('lib/services/EncryptionService.js');
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
const ResourceService = require('lib/services/ResourceService.js');
const RevisionService = require('lib/services/RevisionService.js');
const WebDavApi = require('lib/WebDavApi');
const DropboxApi = require('lib/DropboxApi');
let databases_ = [];
let synchronizers_ = [];
let encryptionServices_ = [];
let revisionServices_ = [];
let decryptionWorkers_ = [];
let resourceServices_ = [];
let fileApi_ = null;
@@ -71,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' });
@@ -82,6 +90,7 @@ BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', 'net.cozic.joplin-cli');
Setting.setConstant('appType', 'cli');
@@ -115,9 +124,11 @@ async function switchClient(id) {
Note.db_ = databases_[id];
BaseItem.db_ = databases_[id];
Setting.db_ = databases_[id];
Resource.db_ = databases_[id];
BaseItem.encryptionService_ = encryptionServices_[id];
Resource.encryptionService_ = encryptionServices_[id];
BaseItem.revisionService_ = revisionServices_[id];
Setting.setConstant('resourceDir', resourceDir(id));
@@ -129,21 +140,28 @@ async function clearDatabase(id = null) {
await ItemChange.waitForAllSaved();
let queries = [
'DELETE FROM notes',
'DELETE FROM folders',
'DELETE FROM resources',
'DELETE FROM tags',
'DELETE FROM note_tags',
'DELETE FROM master_keys',
'DELETE FROM item_changes',
'DELETE FROM note_resources',
'DELETE FROM settings',
'DELETE FROM deleted_items',
'DELETE FROM sync_items',
'DELETE FROM notes_normalized',
const tableNames = [
'notes',
'folders',
'resources',
'tags',
'note_tags',
'master_keys',
'item_changes',
'note_resources',
'settings',
'deleted_items',
'sync_items',
'notes_normalized',
'revisions',
];
const queries = [];
for (const n of tableNames) {
queries.push('DELETE FROM ' + n);
queries.push('DELETE FROM sqlite_sequence WHERE name="' + n + '"'); // Reset autoincremented IDs
}
await databases_[id].transactionExecBatch(queries);
}
@@ -168,6 +186,7 @@ async function setupDatabase(id = null) {
};
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
databases_[id].setLogger(dbLogger);
await databases_[id].open({ name: filePath });
BaseModel.db_ = databases_[id];
@@ -200,6 +219,7 @@ async function setupDatabaseAndSynchronizer(id = null) {
}
encryptionServices_[id] = new EncryptionService();
revisionServices_[id] = new RevisionService();
decryptionWorkers_[id] = new DecryptionWorker();
decryptionWorkers_[id].setEncryptionService(encryptionServices_[id]);
resourceServices_[id] = new ResourceService();
@@ -222,6 +242,11 @@ function encryptionService(id = null) {
return encryptionServices_[id];
}
function revisionService(id = null) {
if (id === null) id = currentClient_;
return revisionServices_[id];
}
function decryptionWorker(id = null) {
if (id === null) id = currentClient_;
return decryptionWorkers_[id];
@@ -354,4 +379,4 @@ async function allSyncTargetItemsEncrypted() {
return totalCount === encryptedCount;
}
module.exports = { resourceService, allSyncTargetItemsEncrypted, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest };
module.exports = { resourceService, allSyncTargetItemsEncrypted, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest };

View File

@@ -1,3 +1,5 @@
// https://github.com/mozilla/readability/tree/814f0a3884350b6f1adfdebb79ca3599e9806605
/*eslint-env es6:false*/
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
@@ -496,17 +498,9 @@
},
setValue: function(newValue) {
this._value = newValue;
delete this._decodedValue;
},
setDecodedValue: function(newValue) {
this._value = encodeHTML(newValue);
this._decodedValue = newValue;
},
getDecodedValue: function() {
if (typeof this._decodedValue === "undefined") {
this._decodedValue = (this._value && decodeHTML(this._value)) || "";
}
return this._decodedValue;
getEncodedValue: function() {
return encodeHTML(this._value);
},
};
@@ -611,6 +605,13 @@
};
var Element = function (tag) {
// We use this to find the closing tag.
this._matchingTag = tag;
// We're explicitly a non-namespace aware parser, we just pretend it's all HTML.
var lastColonIndex = tag.lastIndexOf(":");
if (lastColonIndex != -1) {
tag = tag.substring(lastColonIndex + 1);
}
this.attributes = [];
this.childNodes = [];
this.children = [];
@@ -659,6 +660,14 @@
this.setAttribute("src", str);
},
get srcset() {
return this.getAttribute("srcset") || "";
},
set srcset(str) {
this.setAttribute("srcset", str);
},
get nodeName() {
return this.tagName;
},
@@ -675,9 +684,9 @@
for (var j = 0; j < child.attributes.length; j++) {
var attr = child.attributes[j];
// the attribute value will be HTML escaped.
var val = attr.value;
var val = attr.getEncodedValue();
var quote = (val.indexOf('"') === -1 ? '"' : "'");
arr.push(" " + attr.name + '=' + quote + val + quote);
arr.push(" " + attr.name + "=" + quote + val + quote);
}
if (child.localName in voidElems && !child.childNodes.length) {
@@ -753,8 +762,9 @@
getAttribute: function (name) {
for (var i = this.attributes.length; --i >= 0;) {
var attr = this.attributes[i];
if (attr.name === name)
return attr.getDecodedValue();
if (attr.name === name) {
return attr.value;
}
}
return undefined;
},
@@ -763,11 +773,11 @@
for (var i = this.attributes.length; --i >= 0;) {
var attr = this.attributes[i];
if (attr.name === name) {
attr.setDecodedValue(value);
attr.setValue(value);
return;
}
}
this.attributes.push(new Attribute(name, encodeHTML(value)));
this.attributes.push(new Attribute(name, value));
},
removeAttribute: function (name) {
@@ -778,7 +788,13 @@
break;
}
}
}
},
hasAttribute: function (name) {
return this.attributes.some(function (attr) {
return attr.name == name;
});
},
};
var Style = function (node) {
@@ -925,7 +941,7 @@
// Read the attribute value (and consume the matching quote)
var value = this.readString(c);
node.attributes.push(new Attribute(name, value));
node.attributes.push(new Attribute(name, decodeHTML(value)));
return;
},
@@ -950,7 +966,7 @@
strBuf.push(c);
c = this.nextChar();
}
var tag = strBuf.join('');
var tag = strBuf.join("");
if (!tag)
return false;
@@ -961,7 +977,9 @@
while (c !== "/" && c !== ">") {
if (c === undefined)
return false;
while (whitespace.indexOf(this.html[this.currentChar++]) != -1);
while (whitespace.indexOf(this.html[this.currentChar++]) != -1) {
// Advance cursor to first non-whitespace char.
}
this.currentChar--;
c = this.nextChar();
if (c !== "/" && c !== ">") {
@@ -1055,9 +1073,10 @@
return null;
// Read any text as Text node
var textNode;
if (c !== "<") {
--this.currentChar;
var textNode = new Text();
textNode = new Text();
var n = this.html.indexOf("<", this.currentChar);
if (n === -1) {
textNode.innerHTML = this.html.substring(this.currentChar, this.html.length);
@@ -1069,6 +1088,18 @@
return textNode;
}
if (this.match("![CDATA[")) {
var endChar = this.html.indexOf("]]>", this.currentChar);
if (endChar === -1) {
this.error("unclosed CDATA section");
return null;
}
textNode = new Text();
textNode.textContent = this.html.substring(this.currentChar, endChar);
this.currentChar = endChar + ("]]>").length;
return textNode;
}
c = this.peekNext();
// Read Comment node. Normally, Comment nodes know their inner
@@ -1100,7 +1131,7 @@
// If this isn't a void Element, read its child nodes
if (!closed) {
this.readChildren(node);
var closingTag = "</" + localName + ">";
var closingTag = "</" + node._matchingTag + ">";
if (!this.match(closingTag)) {
this.error("expected '" + closingTag + "' and got " + this.html.substr(this.currentChar, closingTag.length));
return null;

View File

@@ -0,0 +1,99 @@
// https://github.com/mozilla/readability/tree/814f0a3884350b6f1adfdebb79ca3599e9806605
/* eslint-env es6:false */
/* globals exports */
/*
* Copyright (c) 2010 Arc90 Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This code is heavily based on Arc90's readability.js (1.7.1) script
* available at: http://code.google.com/p/arc90labs-readability
*/
var REGEXPS = {
// NOTE: These two regular expressions are duplicated in
// Readability.js. Please keep both copies in sync.
unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
};
function isNodeVisible(node) {
// Have to null-check node.style to deal with SVG and MathML nodes.
return (!node.style || node.style.display != "none") && !node.hasAttribute("hidden");
}
/**
* Decides whether or not the document is reader-able without parsing the whole thing.
*
* @return boolean Whether or not we suspect Readability.parse() will suceeed at returning an article object.
*/
function isProbablyReaderable(doc, isVisible) {
if (!isVisible) {
isVisible = isNodeVisible;
}
var nodes = doc.querySelectorAll("p, pre");
// Get <div> nodes which have <br> node(s) and append them into the `nodes` variable.
// Some articles' DOM structures might look like
// <div>
// Sentences<br>
// <br>
// Sentences<br>
// </div>
var brNodes = doc.querySelectorAll("div > br");
if (brNodes.length) {
var set = new Set(nodes);
[].forEach.call(brNodes, function(node) {
set.add(node.parentNode);
});
nodes = Array.from(set);
}
var score = 0;
// This is a little cheeky, we use the accumulator 'score' to decide what to return from
// this callback:
return [].some.call(nodes, function(node) {
if (!isVisible(node))
return false;
var matchString = node.className + " " + node.id;
if (REGEXPS.unlikelyCandidates.test(matchString) &&
!REGEXPS.okMaybeItsACandidate.test(matchString)) {
return false;
}
if (node.matches("li p")) {
return false;
}
var textContentLength = node.textContent.trim().length;
if (textContentLength < 140) {
return false;
}
score += Math.sqrt(textContentLength - 140);
if (score > 20) {
return true;
}
return false;
});
}
if (typeof exports === "object") {
exports.isProbablyReaderable = isProbablyReaderable;
}

View File

@@ -1,3 +1,5 @@
// https://github.com/mozilla/readability/tree/814f0a3884350b6f1adfdebb79ca3599e9806605
/*eslint-env es6:false*/
/*
* Copyright (c) 2010 Arc90 Inc
@@ -39,6 +41,7 @@ function Readability(doc, options) {
this._articleTitle = null;
this._articleByline = null;
this._articleDir = null;
this._articleSiteName = null;
this._attempts = [];
// Configurable options
@@ -111,15 +114,18 @@ Readability.prototype = {
// All of the regular expressions in use within readability.
// Defined up here so we don't instantiate them repeatedly in loops.
REGEXPS: {
unlikelyCandidates: /banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
// NOTE: These two regular expressions are duplicated in
// Readability-readerable.js. Please keep both copies in sync.
unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
byline: /byline|author|dateline|writtenby|p-author/i,
replaceFonts: /<(\/?)font[^>]*>/gi,
normalize: /\s{2,}/g,
videos: /\/\/(www\.)?(dailymotion|youtube|youtube-nocookie|player\.vimeo)\.com/i,
videos: /\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i,
nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
prevLink: /(prev|earl|old|new|<|«)/i,
whitespace: /^\s*$/,
@@ -260,7 +266,7 @@ Readability.prototype = {
_getAllNodesWithTag: function(node, tagNames) {
if (node.querySelectorAll) {
return node.querySelectorAll(tagNames.join(','));
return node.querySelectorAll(tagNames.join(","));
}
return [].concat.apply([], tagNames.map(function(tag) {
var collection = node.getElementsByTagName(tag);
@@ -320,7 +326,7 @@ Readability.prototype = {
return uri;
}
var links = articleContent.getElementsByTagName("a");
var links = this._getAllNodesWithTag(articleContent, ["a"]);
this._forEachNode(links, function(link) {
var href = link.getAttribute("href");
if (href) {
@@ -335,7 +341,7 @@ Readability.prototype = {
}
});
var imgs = articleContent.getElementsByTagName("img");
var imgs = this._getAllNodesWithTag(articleContent, ["img"]);
this._forEachNode(imgs, function(img) {
var src = img.getAttribute("src");
if (src) {
@@ -355,11 +361,11 @@ Readability.prototype = {
var origTitle = "";
try {
curTitle = origTitle = doc.title;
curTitle = origTitle = doc.title.trim();
// If they had an element with id "title" in their HTML
if (typeof curTitle !== "string")
curTitle = origTitle = this._getInnerText(doc.getElementsByTagName('title')[0]);
curTitle = origTitle = this._getInnerText(doc.getElementsByTagName("title")[0]);
} catch (e) {/* ignore exceptions setting the title. */}
var titleHadHierarchicalSeparators = false;
@@ -370,44 +376,45 @@ Readability.prototype = {
// If there's a separator in the title, first remove the final part
if ((/ [\|\-\\\/>»] /).test(curTitle)) {
titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle);
curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, '$1');
curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, "$1");
// If the resulting title is too short (3 words or fewer), remove
// the first part instead:
if (wordCount(curTitle) < 3)
curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, '$1');
} else if (curTitle.indexOf(': ') !== -1) {
curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, "$1");
} else if (curTitle.indexOf(": ") !== -1) {
// Check if we have an heading containing this exact string, so we
// could assume it's the full title.
var headings = this._concatNodeLists(
doc.getElementsByTagName('h1'),
doc.getElementsByTagName('h2')
doc.getElementsByTagName("h1"),
doc.getElementsByTagName("h2")
);
var trimmedTitle = curTitle.trim();
var match = this._someNode(headings, function(heading) {
return heading.textContent === curTitle;
return heading.textContent.trim() === trimmedTitle;
});
// If we don't, let's extract the title out of the original title string.
if (!match) {
curTitle = origTitle.substring(origTitle.lastIndexOf(':') + 1);
curTitle = origTitle.substring(origTitle.lastIndexOf(":") + 1);
// If the title is now too short, try the first colon instead:
if (wordCount(curTitle) < 3) {
curTitle = origTitle.substring(origTitle.indexOf(':') + 1);
curTitle = origTitle.substring(origTitle.indexOf(":") + 1);
// But if we have too many words before the colon there's something weird
// with the titles and the H tags so let's just use the original title instead
} else if (wordCount(origTitle.substr(0, origTitle.indexOf(':'))) > 5) {
} else if (wordCount(origTitle.substr(0, origTitle.indexOf(":"))) > 5) {
curTitle = origTitle;
}
}
} else if (curTitle.length > 150 || curTitle.length < 15) {
var hOnes = doc.getElementsByTagName('h1');
var hOnes = doc.getElementsByTagName("h1");
if (hOnes.length === 1)
curTitle = this._getInnerText(hOnes[0]);
}
curTitle = curTitle.trim();
curTitle = curTitle.trim().replace(this.REGEXPS.normalize, " ");
// If we now have 4 words or fewer as our title, and either no
// 'hierarchical' separators (\, /, > or ») were found in the original
// title or we decreased the number of words by more than 1 word, use
@@ -497,7 +504,8 @@ Readability.prototype = {
break;
}
if (!this._isPhrasingContent(next)) break;
if (!this._isPhrasingContent(next))
break;
// Otherwise, make this node a child of the new <p>.
var sibling = next.nextSibling;
@@ -505,7 +513,12 @@ Readability.prototype = {
next = sibling;
}
while (p.lastChild && this._isWhitespace(p.lastChild)) p.removeChild(p.lastChild);
while (p.lastChild && this._isWhitespace(p.lastChild)) {
p.removeChild(p.lastChild);
}
if (p.parentNode.tagName === "P")
this._setNodeTag(p.parentNode, "DIV");
}
});
},
@@ -527,7 +540,16 @@ Readability.prototype = {
replacement.readability = node.readability;
for (var i = 0; i < node.attributes.length; i++) {
replacement.setAttribute(node.attributes[i].name, node.attributes[i].value);
try {
replacement.setAttribute(node.attributes[i].name, node.attributes[i].value);
} catch (ex) {
/* it's possible for setAttribute() to throw if the attribute name
* isn't a valid XML Name. Such attributes can however be parsed from
* source in HTML docs, see https://github.com/whatwg/html/issues/4275,
* so we can hit them here and then throw. We don't care about such
* attributes so we ignore them.
*/
}
}
return replacement;
},
@@ -547,6 +569,8 @@ Readability.prototype = {
// visually linked to other content-ful elements (text, images, etc.).
this._markDataTables(articleContent);
this._fixLazyImages(articleContent);
// Clean out junk from the article content
this._cleanConditionally(articleContent, "form");
this._cleanConditionally(articleContent, "fieldset");
@@ -557,16 +581,21 @@ Readability.prototype = {
this._clean(articleContent, "link");
this._clean(articleContent, "aside");
// Clean out elements have "share" in their id/class combinations from final top candidates,
// Clean out elements with little content that have "share" in their id/class combinations from final top candidates,
// which means we don't remove the top candidates even they have "share".
this._forEachNode(articleContent.children, function(topCandidate) {
this._cleanMatchedNodes(topCandidate, /share/);
var shareElementThreshold = this.DEFAULT_CHAR_THRESHOLD;
this._forEachNode(articleContent.children, function (topCandidate) {
this._cleanMatchedNodes(topCandidate, function (node, matchString) {
return /share/.test(matchString) && node.textContent.length < shareElementThreshold;
});
});
// If there is only one h2 and its text content substantially equals article title,
// they are probably using it as a header and not a subheader,
// so remove it since we already extract the title separately.
var h2 = articleContent.getElementsByTagName('h2');
var h2 = articleContent.getElementsByTagName("h2");
if (h2.length === 1) {
var lengthSimilarRate = (h2[0].textContent.length - this._articleTitle.length) / this._articleTitle.length;
if (Math.abs(lengthSimilarRate) < 0.5) {
@@ -596,12 +625,12 @@ Readability.prototype = {
this._cleanConditionally(articleContent, "div");
// Remove extra paragraphs
this._removeNodes(articleContent.getElementsByTagName('p'), function (paragraph) {
var imgCount = paragraph.getElementsByTagName('img').length;
var embedCount = paragraph.getElementsByTagName('embed').length;
var objectCount = paragraph.getElementsByTagName('object').length;
this._removeNodes(articleContent.getElementsByTagName("p"), function (paragraph) {
var imgCount = paragraph.getElementsByTagName("img").length;
var embedCount = paragraph.getElementsByTagName("embed").length;
var objectCount = paragraph.getElementsByTagName("object").length;
// At this point, nasty iframes have been removed, only remain embedded video ones.
var iframeCount = paragraph.getElementsByTagName('iframe').length;
var iframeCount = paragraph.getElementsByTagName("iframe").length;
var totalCount = imgCount + embedCount + objectCount + iframeCount;
return totalCount === 0 && !this._getInnerText(paragraph, false);
@@ -612,6 +641,19 @@ Readability.prototype = {
if (next && next.tagName == "P")
br.parentNode.removeChild(br);
});
// Remove single-cell tables
this._forEachNode(this._getAllNodesWithTag(articleContent, ["table"]), function(table) {
var tbody = this._hasSingleTagInsideElement(table, "TBODY") ? table.firstElementChild : table;
if (this._hasSingleTagInsideElement(tbody, "TR")) {
var row = tbody.firstElementChild;
if (this._hasSingleTagInsideElement(row, "TD")) {
var cell = row.firstElementChild;
cell = this._setNodeTag(cell, this._everyNode(cell.childNodes, this._isPhrasingContent) ? "P" : "DIV");
table.parentNode.replaceChild(cell, table);
}
}
});
},
/**
@@ -625,34 +667,34 @@ Readability.prototype = {
node.readability = {"contentScore": 0};
switch (node.tagName) {
case 'DIV':
case "DIV":
node.readability.contentScore += 5;
break;
case 'PRE':
case 'TD':
case 'BLOCKQUOTE':
case "PRE":
case "TD":
case "BLOCKQUOTE":
node.readability.contentScore += 3;
break;
case 'ADDRESS':
case 'OL':
case 'UL':
case 'DL':
case 'DD':
case 'DT':
case 'LI':
case 'FORM':
case "ADDRESS":
case "OL":
case "UL":
case "DL":
case "DD":
case "DT":
case "LI":
case "FORM":
node.readability.contentScore -= 3;
break;
case 'H1':
case 'H2':
case 'H3':
case 'H4':
case 'H5':
case 'H6':
case 'TH':
case "H1":
case "H2":
case "H3":
case "H4":
case "H5":
case "H6":
case "TH":
node.readability.contentScore -= 5;
break;
}
@@ -691,37 +733,6 @@ Readability.prototype = {
return node && node.nextElementSibling;
},
/**
* Like _getNextNode, but for DOM implementations with no
* firstElementChild/nextElementSibling functionality...
*/
_getNextNodeNoElementProperties: function(node, ignoreSelfAndKids) {
function nextSiblingEl(n) {
do {
n = n.nextSibling;
} while (n && n.nodeType !== n.ELEMENT_NODE);
return n;
}
// First check for kids if those aren't being ignored
if (!ignoreSelfAndKids && node.children[0]) {
return node.children[0];
}
// Then for siblings...
var next = nextSiblingEl(node);
if (next) {
return next;
}
// And finally, move up the parent chain *and* find a sibling
// (because this is depth-first traversal, we will have already
// seen the parent nodes themselves).
do {
node = node.parentNode;
if (node)
next = nextSiblingEl(node);
} while (node && !next);
return node && next;
},
_checkByline: function(node, matchString) {
if (this._articleByline) {
return false;
@@ -729,9 +740,10 @@ Readability.prototype = {
if (node.getAttribute !== undefined) {
var rel = node.getAttribute("rel");
var itemprop = node.getAttribute("itemprop");
}
if ((rel === "author" || this.REGEXPS.byline.test(matchString)) && this._isValidByline(node.textContent)) {
if ((rel === "author" || (itemprop && itemprop.indexOf("author") !== -1) || this.REGEXPS.byline.test(matchString)) && this._isValidByline(node.textContent)) {
this._articleByline = node.textContent.trim();
return true;
}
@@ -784,6 +796,12 @@ Readability.prototype = {
while (node) {
var matchString = node.className + " " + node.id;
if (!this._isProbablyVisible(node)) {
this.log("Removing hidden node - " + matchString);
node = this._removeAndGetNext(node);
continue;
}
// Check to see if this node is a byline, and remove it if it is.
if (this._checkByline(node, matchString)) {
node = this._removeAndGetNext(node);
@@ -794,6 +812,7 @@ Readability.prototype = {
if (stripUnlikelyCandidates) {
if (this.REGEXPS.unlikelyCandidates.test(matchString) &&
!this.REGEXPS.okMaybeItsACandidate.test(matchString) &&
!this._hasAncestorTag(node, "table") &&
node.tagName !== "BODY" &&
node.tagName !== "A") {
this.log("Removing unlikely candidate - " + matchString);
@@ -826,12 +845,14 @@ Readability.prototype = {
if (p !== null) {
p.appendChild(childNode);
} else if (!this._isWhitespace(childNode)) {
p = doc.createElement('p');
p = doc.createElement("p");
node.replaceChild(p, childNode);
p.appendChild(childNode);
}
} else if (p !== null) {
while (p.lastChild && this._isWhitespace(p.lastChild)) p.removeChild(p.lastChild);
while (p.lastChild && this._isWhitespace(p.lastChild)) {
p.removeChild(p.lastChild);
}
p = null;
}
childNode = nextSibling;
@@ -841,7 +862,7 @@ Readability.prototype = {
// element. DIVs with only a P element inside and no text content can be
// safely converted into plain P elements to avoid confusing the scoring
// algorithm with DIVs with are, in practice, paragraphs.
if (this._hasSinglePInsideElement(node) && this._getLinkDensity(node) < 0.25) {
if (this._hasSingleTagInsideElement(node, "P") && this._getLinkDensity(node) < 0.25) {
var newNode = node.children[0];
node.parentNode.replaceChild(newNode, node);
node = newNode;
@@ -862,7 +883,7 @@ Readability.prototype = {
**/
var candidates = [];
this._forEachNode(elementsToScore, function(elementToScore) {
if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === 'undefined')
if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === "undefined")
return;
// If this paragraph is less than 25 characters, don't even count it.
@@ -881,17 +902,17 @@ Readability.prototype = {
contentScore += 1;
// Add points for any commas within this paragraph.
contentScore += innerText.split(',').length;
contentScore += innerText.split(",").length;
// For every 100 characters in this paragraph, add another point. Up to 3 points.
contentScore += Math.min(Math.floor(innerText.length / 100), 3);
// Initialize and score ancestors.
this._forEachNode(ancestors, function(ancestor, level) {
if (!ancestor.tagName || !ancestor.parentNode || typeof(ancestor.parentNode.tagName) === 'undefined')
if (!ancestor.tagName || !ancestor.parentNode || typeof(ancestor.parentNode.tagName) === "undefined")
return;
if (typeof(ancestor.readability) === 'undefined') {
if (typeof(ancestor.readability) === "undefined") {
this._initializeNode(ancestor);
candidates.push(ancestor);
}
@@ -922,7 +943,7 @@ Readability.prototype = {
var candidateScore = candidate.readability.contentScore * (1 - this._getLinkDensity(candidate));
candidate.readability.contentScore = candidateScore;
this.log('Candidate:', candidate, "with score " + candidateScore);
this.log("Candidate:", candidate, "with score " + candidateScore);
for (var t = 0; t < this._nbTopCandidates; t++) {
var aTopCandidate = topCandidates[t];
@@ -1041,8 +1062,8 @@ Readability.prototype = {
var sibling = siblings[s];
var append = false;
this.log("Looking at sibling node:", sibling, sibling.readability ? ("with score " + sibling.readability.contentScore) : '');
this.log("Sibling has score", sibling.readability ? sibling.readability.contentScore : 'Unknown');
this.log("Looking at sibling node:", sibling, sibling.readability ? ("with score " + sibling.readability.contentScore) : "");
this.log("Sibling has score", sibling.readability ? sibling.readability.contentScore : "Unknown");
if (sibling === topCandidate) {
append = true;
@@ -1076,7 +1097,7 @@ Readability.prototype = {
if (this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName) === -1) {
// We have a node that isn't a common block level element, like a form or td tag.
// Turn it into a div so it doesn't get filtered out later by accident.
this.log("Altering sibling:", sibling, 'to div.');
this.log("Altering sibling:", sibling, "to div.");
sibling = this._setNodeTag(sibling, "DIV");
}
@@ -1144,7 +1165,7 @@ Readability.prototype = {
this._attempts.push({articleContent: articleContent, textLength: textLength});
// No luck after removing flags, just return the longest text we found during the different loops
this._attempts.sort(function (a, b) {
return a.textLength < b.textLength;
return b.textLength - a.textLength;
});
// But first check if we actually have something
@@ -1184,7 +1205,7 @@ Readability.prototype = {
* @return Boolean - whether the input string is a byline.
*/
_isValidByline: function(byline) {
if (typeof byline == 'string' || byline instanceof String) {
if (typeof byline == "string" || byline instanceof String) {
byline = byline.trim();
return (byline.length > 0) && (byline.length < 100);
}
@@ -1201,61 +1222,75 @@ Readability.prototype = {
var values = {};
var metaElements = this._doc.getElementsByTagName("meta");
// Match "description", or Twitter's "twitter:description" (Cards)
// in name attribute.
var namePattern = /^\s*((twitter)\s*:\s*)?(description|title)\s*$/gi;
// property is a space-separated list of values
var propertyPattern = /\s*(dc|dcterm|og|twitter)\s*:\s*(author|creator|description|title|site_name)\s*/gi;
// Match Facebook's Open Graph title & description properties.
var propertyPattern = /^\s*og\s*:\s*(description|title)\s*$/gi;
// name is a single value
var namePattern = /^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i;
// Find description tags.
this._forEachNode(metaElements, function(element) {
var elementName = element.getAttribute("name");
var elementProperty = element.getAttribute("property");
if ([elementName, elementProperty].indexOf("author") !== -1) {
metadata.byline = element.getAttribute("content");
var content = element.getAttribute("content");
if (!content) {
return;
}
var matches = null;
var name = null;
if (namePattern.test(elementName)) {
name = elementName;
} else if (propertyPattern.test(elementProperty)) {
name = elementProperty;
}
if (name) {
var content = element.getAttribute("content");
if (elementProperty) {
matches = elementProperty.match(propertyPattern);
if (matches) {
for (var i = matches.length - 1; i >= 0; i--) {
// Convert to lowercase, and remove any whitespace
// so we can match below.
name = matches[i].toLowerCase().replace(/\s/g, "");
// multiple authors
values[name] = content.trim();
}
}
}
if (!matches && elementName && namePattern.test(elementName)) {
name = elementName;
if (content) {
// Convert to lowercase and remove any whitespace
// so we can match below.
name = name.toLowerCase().replace(/\s/g, '');
// Convert to lowercase, remove any whitespace, and convert dots
// to colons so we can match below.
name = name.toLowerCase().replace(/\s/g, "").replace(/\./g, ":");
values[name] = content.trim();
}
}
});
if ("description" in values) {
metadata.excerpt = values["description"];
} else if ("og:description" in values) {
// Use facebook open graph description.
metadata.excerpt = values["og:description"];
} else if ("twitter:description" in values) {
// Use twitter cards description.
metadata.excerpt = values["twitter:description"];
// get title
metadata.title = values["dc:title"] ||
values["dcterm:title"] ||
values["og:title"] ||
values["weibo:article:title"] ||
values["weibo:webpage:title"] ||
values["title"] ||
values["twitter:title"];
if (!metadata.title) {
metadata.title = this._getArticleTitle();
}
metadata.title = this._getArticleTitle();
if (!metadata.title) {
if ("og:title" in values) {
// Use facebook open graph title.
metadata.title = values["og:title"];
} else if ("twitter:title" in values) {
// Use twitter cards title.
metadata.title = values["twitter:title"];
}
}
// get author
metadata.byline = values["dc:creator"] ||
values["dcterm:creator"] ||
values["author"];
// get description
metadata.excerpt = values["dc:description"] ||
values["dcterm:description"] ||
values["og:description"] ||
values["weibo:article:description"] ||
values["weibo:webpage:description"] ||
values["description"] ||
values["twitter:description"];
// get site name
metadata.siteName = values["og:site_name"];
return metadata;
},
@@ -1266,24 +1301,25 @@ Readability.prototype = {
* @param Element
**/
_removeScripts: function(doc) {
this._removeNodes(doc.getElementsByTagName('script'), function(scriptNode) {
this._removeNodes(doc.getElementsByTagName("script"), function(scriptNode) {
scriptNode.nodeValue = "";
scriptNode.removeAttribute('src');
scriptNode.removeAttribute("src");
return true;
});
this._removeNodes(doc.getElementsByTagName('noscript'));
this._removeNodes(doc.getElementsByTagName("noscript"));
},
/**
* Check if this node has only whitespace and a single P element
* Check if this node has only whitespace and a single element with given tag
* Returns false if the DIV node contains non-empty text nodes
* or if it contains no P or more than 1 element.
* or if it contains no element with given tag or more than 1 element.
*
* @param Element
* @param string tag of child element
**/
_hasSinglePInsideElement: function(element) {
// There should be exactly 1 element child which is a P:
if (element.children.length != 1 || element.children[0].tagName !== "P") {
_hasSingleTagInsideElement: function(element, tag) {
// There should be exactly 1 element child with given tag
if (element.children.length != 1 || element.children[0].tagName !== tag) {
return false;
}
@@ -1337,7 +1373,7 @@ Readability.prototype = {
* @return string
**/
_getInnerText: function(e, normalizeSpaces) {
normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces;
normalizeSpaces = (typeof normalizeSpaces === "undefined") ? true : normalizeSpaces;
var textContent = e.textContent.trim();
if (normalizeSpaces) {
@@ -1366,7 +1402,7 @@ Readability.prototype = {
* @return void
**/
_cleanStyles: function(e) {
if (!e || e.tagName.toLowerCase() === 'svg')
if (!e || e.tagName.toLowerCase() === "svg")
return;
// Remove `style` and deprecated presentational attributes
@@ -1375,8 +1411,8 @@ Readability.prototype = {
}
if (this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName) !== -1) {
e.removeAttribute('width');
e.removeAttribute('height');
e.removeAttribute("width");
e.removeAttribute("height");
}
var cur = e.firstElementChild;
@@ -1422,7 +1458,7 @@ Readability.prototype = {
var weight = 0;
// Look for a special classname
if (typeof(e.className) === 'string' && e.className !== '') {
if (typeof(e.className) === "string" && e.className !== "") {
if (this.REGEXPS.negative.test(e.className))
weight -= 25;
@@ -1431,7 +1467,7 @@ Readability.prototype = {
}
// Look for a special ID
if (typeof(e.id) === 'string' && e.id !== '') {
if (typeof(e.id) === "string" && e.id !== "") {
if (this.REGEXPS.negative.test(e.id))
weight -= 25;
@@ -1456,17 +1492,17 @@ Readability.prototype = {
this._removeNodes(e.getElementsByTagName(tag), function(element) {
// Allow youtube and vimeo videos through as people usually want to see those.
if (isEmbed) {
var attributeValues = [].map.call(element.attributes, function(attr) {
return attr.value;
}).join("|");
// First, check the elements attributes to see if any of them contain youtube or vimeo
if (this.REGEXPS.videos.test(attributeValues))
return false;
for (var i = 0; i < element.attributes.length; i++) {
if (this.REGEXPS.videos.test(element.attributes[i].value)) {
return false;
}
}
// Then check the elements inside this element for the same.
if (this.REGEXPS.videos.test(element.innerHTML))
// For embed with <object> tag, check inner HTML as well.
if (element.tagName === "object" && this.REGEXPS.videos.test(element.innerHTML)) {
return false;
}
}
return true;
@@ -1584,6 +1620,39 @@ Readability.prototype = {
}
},
/* convert images and figures that have properties like data-src into images that can be loaded without JS */
_fixLazyImages: function (root) {
this._forEachNode(this._getAllNodesWithTag(root, ["img", "picture", "figure"]), function (elem) {
// also check for "null" to work around https://github.com/jsdom/jsdom/issues/2580
if ((!elem.src && (!elem.srcset || elem.srcset == "null")) || elem.className.toLowerCase().indexOf("lazy") !== -1) {
for (var i = 0; i < elem.attributes.length; i++) {
var attr = elem.attributes[i];
if (attr.name === "src" || attr.name === "srcset") {
continue;
}
var copyTo = null;
if (/\.(jpg|jpeg|png|webp)\s+\d/.test(attr.value)) {
copyTo = "srcset";
} else if (/^\s*\S+\.(jpg|jpeg|png|webp)\S*\s*$/.test(attr.value)) {
copyTo = "src";
}
if (copyTo) {
//if this is an img or picture, set the attribute directly
if (elem.tagName === "IMG" || elem.tagName === "PICTURE") {
elem.setAttribute(copyTo, attr.value);
} else if (elem.tagName === "FIGURE" && !this._getAllNodesWithTag(elem, ["img", "picture"]).length) {
//if the item is a <figure> that does not contain an image or picture, create one and place it inside the figure
//see the nytimes-3 testcase for an example
var img = this._doc.createElement("img");
img.setAttribute(copyTo, attr.value);
elem.appendChild(img);
}
}
}
}
});
},
/**
* Clean an element of all tags of type "tag" if they look fishy.
* "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc.
@@ -1602,11 +1671,16 @@ Readability.prototype = {
//
// TODO: Consider taking into account original contentScore here.
this._removeNodes(e.getElementsByTagName(tag), function(node) {
// First check if we're in a data table, in which case don't remove us.
// First check if this node IS data table, in which case don't remove it.
var isDataTable = function(t) {
return t._readabilityDataTable;
};
if (tag === "table" && isDataTable(node)) {
return false;
}
// Next check if we're inside a data table, in which case don't remove it as well.
if (this._hasAncestorTag(node, "table", -1, isDataTable)) {
return false;
}
@@ -1620,7 +1694,7 @@ Readability.prototype = {
return true;
}
if (this._getCharCount(node, ',') < 10) {
if (this._getCharCount(node, ",") < 10) {
// If there are not very many commas, and the number of
// non-paragraph elements is more than paragraphs or other
// ominous signs, remove the element.
@@ -1630,10 +1704,25 @@ Readability.prototype = {
var input = node.getElementsByTagName("input").length;
var embedCount = 0;
var embeds = node.getElementsByTagName("embed");
for (var ei = 0, il = embeds.length; ei < il; ei += 1) {
if (!this.REGEXPS.videos.test(embeds[ei].src))
embedCount += 1;
var embeds = this._concatNodeLists(
node.getElementsByTagName("object"),
node.getElementsByTagName("embed"),
node.getElementsByTagName("iframe"));
for (var i = 0; i < embeds.length; i++) {
// If this embed has attribute that matches video regex, don't delete it.
for (var j = 0; j < embeds[i].attributes.length; j++) {
if (this.REGEXPS.videos.test(embeds[i].attributes[j].value)) {
return false;
}
}
// For embed with <object> tag, check inner HTML as well.
if (embeds[i].tagName === "object" && this.REGEXPS.videos.test(embeds[i].innerHTML)) {
return false;
}
embedCount++;
}
var linkDensity = this._getLinkDensity(node);
@@ -1654,17 +1743,17 @@ Readability.prototype = {
},
/**
* Clean out elements whose id/class combinations match specific string.
* Clean out elements that match the specified conditions
*
* @param Element
* @param RegExp match id/class combination.
* @param Function determines whether a node should be removed
* @return void
**/
_cleanMatchedNodes: function(e, regex) {
_cleanMatchedNodes: function(e, filter) {
var endOfSearchMarkerNode = this._getNextNode(e, true);
var next = this._getNextNode(e);
while (next && next != endOfSearchMarkerNode) {
if (regex.test(next.className + " " + next.id)) {
if (filter(next, next.className + " " + next.id)) {
next = this._removeAndGetNext(next);
} else {
next = this._getNextNode(next);
@@ -1680,7 +1769,7 @@ Readability.prototype = {
**/
_cleanHeaders: function(e) {
for (var headerIndex = 1; headerIndex < 3; headerIndex += 1) {
this._removeNodes(e.getElementsByTagName('h' + headerIndex), function (header) {
this._removeNodes(e.getElementsByTagName("h" + headerIndex), function (header) {
return this._getClassWeight(header) < 0;
});
}
@@ -1694,63 +1783,8 @@ Readability.prototype = {
this._flags = this._flags & ~flag;
},
/**
* Decides whether or not the document is reader-able without parsing the whole thing.
*
* @return boolean Whether or not we suspect parse() will suceeed at returning an article object.
*/
isProbablyReaderable: function(helperIsVisible) {
var nodes = this._getAllNodesWithTag(this._doc, ["p", "pre"]);
// Get <div> nodes which have <br> node(s) and append them into the `nodes` variable.
// Some articles' DOM structures might look like
// <div>
// Sentences<br>
// <br>
// Sentences<br>
// </div>
var brNodes = this._getAllNodesWithTag(this._doc, ["div > br"]);
if (brNodes.length) {
var set = new Set();
[].forEach.call(brNodes, function(node) {
set.add(node.parentNode);
});
nodes = [].concat.apply(Array.from(set), nodes);
}
// FIXME we should have a fallback for helperIsVisible, but this is
// problematic because of jsdom's elem.style handling - see
// https://github.com/mozilla/readability/pull/186 for context.
var score = 0;
// This is a little cheeky, we use the accumulator 'score' to decide what to return from
// this callback:
return this._someNode(nodes, function(node) {
if (helperIsVisible && !helperIsVisible(node))
return false;
var matchString = node.className + " " + node.id;
if (this.REGEXPS.unlikelyCandidates.test(matchString) &&
!this.REGEXPS.okMaybeItsACandidate.test(matchString)) {
return false;
}
if (node.matches && node.matches("li p")) {
return false;
}
var textContentLength = node.textContent.trim().length;
if (textContentLength < 140) {
return false;
}
score += Math.sqrt(textContentLength - 140);
if (score > 20) {
return true;
}
return false;
});
_isProbablyVisible: function(node) {
return (!node.style || node.style.display != "none") && !node.hasAttribute("hidden");
},
/**
@@ -1774,9 +1808,6 @@ Readability.prototype = {
}
}
if (typeof this._doc.documentElement.firstElementChild === "undefined") {
this._getNextNode = this._getNextNodeNoElementProperties;
}
// Remove script tags from the document.
this._removeScripts(this._doc);
@@ -1812,6 +1843,7 @@ Readability.prototype = {
textContent: textContent,
length: textContent.length,
excerpt: metadata.excerpt,
siteName: metadata.siteName || this._articleSiteName
};
}
};

View File

@@ -14,6 +14,20 @@
browserSupportsPromises_ = false;
}
function absoluteUrl(url) {
if (!url) return url;
const protocol = url.toLowerCase().split(':')[0];
if (['http', 'https', 'file'].indexOf(protocol) >= 0) return url;
if (url.indexOf('//')) {
return location.protocol + url;
} else if (url[0] === '/') {
return location.protocol + '//' + location.host + url;
} else {
return baseUrl() + '/' + url;
}
}
function pageTitle() {
const titleElements = document.getElementsByTagName("title");
if (titleElements.length) return titleElements[0].text.trim();
@@ -30,12 +44,13 @@
return output;
}
function getImageSizes(element) {
function getImageSizes(element, forceAbsoluteUrls = false) {
const images = element.getElementsByTagName('img');
const output = {};
for (let i = 0; i < images.length; i++) {
const img = images[i];
output[img.src] = {
const src = forceAbsoluteUrls ? absoluteUrl(img.src) : img.src;
output[src] = {
width: img.width,
height: img.height,
naturalWidth: img.naturalWidth,
@@ -46,7 +61,7 @@
}
// Cleans up element by removing all its invisible children (which we don't want to render as Markdown)
function cleanUpElement(element) {
function cleanUpElement(element, imageSizes) {
const childNodes = element.childNodes;
for (let i = 0; i < childNodes.length; i++) {
@@ -58,11 +73,27 @@
if (!isVisible) {
element.removeChild(node);
} else {
cleanUpElement(node);
if (node.nodeName.toLowerCase() === 'img') {
node.src = absoluteUrl(node.src);
const imageSize = imageSizes[node.src];
if (imageSize) {
node.width = imageSize.width;
node.height = imageSize.height;
}
}
cleanUpElement(node, imageSizes);
}
}
}
function documentForReadability() {
// Readability directly change the passed document so clone it so as
// to preserve the original web page.
return document.cloneNode(true);
}
function readabilityProcess() {
var uri = {
spec: location.href,
@@ -72,10 +103,7 @@
pathBase: location.protocol + "//" + location.host + location.pathname.substr(0, location.pathname.lastIndexOf("/") + 1)
};
// Readability directly change the passed document so clone it so as
// to preserve the original web page.
const documentClone = document.cloneNode(true);
const readability = new Readability(documentClone); // new window.Readability(uri, documentClone);
const readability = new Readability(documentForReadability());
const article = readability.parse();
if (!article) throw new Error('Could not parse HTML document with Readability');
@@ -117,11 +145,18 @@
}
return clippedContentResponse(article.title, article.body, getImageSizes(document));
} else if (command.name === "isProbablyReaderable") {
const ok = isProbablyReaderable(documentForReadability());
console.info('isProbablyReaderable', ok);
return { name: 'isProbablyReaderable', value: ok };
} else if (command.name === "completePageHtml") {
const cleanDocument = document.body.cloneNode(true);
cleanUpElement(cleanDocument);
return clippedContentResponse(pageTitle(), cleanDocument.innerHTML, getImageSizes(document));
const imageSizes = getImageSizes(document, true);
cleanUpElement(cleanDocument, imageSizes);
return clippedContentResponse(pageTitle(), cleanDocument.innerHTML, imageSizes);
} else if (command.name === "selectedHtml") {
@@ -250,6 +285,7 @@
return {};
} else if (command.name === "pageUrl") {
let url = location.origin + location.pathname + location.search;
return clippedContentResponse(pageTitle(), url, getImageSizes(document));

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper [DEV]",
"version": "1.0.13",
"version": "1.0.14",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",

View File

@@ -7,6 +7,39 @@ import led_orange from './led_orange.png';
const { connect } = require('react-redux');
const { bridge } = require('./bridge');
class PreviewComponent extends React.PureComponent {
constructor() {
super();
this.bodyRef = React.createRef();
}
componentDidMount() {
// Because the text size is made twice smaller with CSS, we need
// to also reduce the size of the images
const imgs = this.bodyRef.current.getElementsByTagName('img');
for (const img of imgs) {
img.width /= 2;
img.height /= 2;
}
}
render() {
return (
<div className="Preview">
<a className={"Confirm Button"} onClick={this.props.onConfirmClick}>Confirm</a>
<h2>Preview:</h2>
<input className={"Title"} value={this.props.title} onChange={this.props.onTitleChange}/>
<div className={"BodyWrapper"}>
<div className={"Body"} ref={this.bodyRef} dangerouslySetInnerHTML={{__html: this.props.body_html}}></div>
</div>
</div>
);
}
}
class AppComponent extends Component {
constructor() {
@@ -123,6 +156,7 @@ class AppComponent extends Component {
async loadContentScripts() {
await bridge().tabsExecuteScript({file: "/content_scripts/JSDOMParser.js"});
await bridge().tabsExecuteScript({file: "/content_scripts/Readability.js"});
await bridge().tabsExecuteScript({file: "/content_scripts/Readability-readerable.js"});
await bridge().tabsExecuteScript({file: "/content_scripts/index.js"});
}
@@ -158,6 +192,8 @@ class AppComponent extends Component {
id: newFolderId,
});
}
bridge().sendCommandToActiveTab({ name: 'isProbablyReaderable' });
}
componentDidUpdate() {
@@ -208,16 +244,12 @@ class AppComponent extends Component {
</div>
);
} else if (hasContent) {
previewComponent = (
<div className="Preview">
<a className={"Confirm Button"} onClick={this.confirm_click}>Confirm</a>
<h2>Preview:</h2>
<input className={"Title"} value={content.title} onChange={this.contentTitle_change}/>
<div className={"BodyWrapper"}>
<div className={"Body"} dangerouslySetInnerHTML={{__html: content.body_html}}></div>
</div>
</div>
);
previewComponent = <PreviewComponent
onConfirmClick={this.confirm_click}
title={content.title}
body_html={content.body_html}
onTitleChange={this.contentTitle_change}
/>
}
const clipperStatusComp = () => {
@@ -278,11 +310,10 @@ class AppComponent extends Component {
const tagsComp = () => {
const comps = [];
for (let i = 0; i < this.state.selectedTags.length; i++) {
comps.push(<div>
comps.push(<div key={i}>
<input
ref={'tagSelector' + i}
data-index={i}
key={i}
type="text"
list="tags"
value={this.state.selectedTags[i]}
@@ -306,11 +337,18 @@ class AppComponent extends Component {
tagDataListOptions.push(<option key={tag.id}>{tag.title}</option>);
}
let simplifiedPageButtonLabel = 'Clip simplified page';
let simplifiedPageButtonTooltip = '';
if (!this.props.isProbablyReaderable) {
simplifiedPageButtonLabel += ' ⚠️';
simplifiedPageButtonTooltip = 'It might not be possible to create a good simplified version of this page.\nYou may want to clip the complete page instead.';
}
return (
<div className="App">
<div className="Controls">
<ul>
<li><a className="Button" onClick={this.clipSimplified_click}>Clip simplified page</a></li>
<li><a className="Button" onClick={this.clipSimplified_click} title={simplifiedPageButtonTooltip}>{simplifiedPageButtonLabel}</a></li>
<li><a className="Button" onClick={this.clipComplete_click}>Clip complete page</a></li>
<li><a className="Button" onClick={this.clipSelection_click}>Clip selection</a></li>
<li><a className="Button" onClick={this.clipScreenshot_click}>Clip screenshot</a></li>
@@ -343,6 +381,7 @@ const mapStateToProps = (state) => {
folders: state.folders,
tags: state.tags,
selectedFolderId: state.selectedFolderId,
isProbablyReaderable: state.isProbablyReaderable,
};
};

View File

@@ -2,6 +2,10 @@ const randomClipperPort = require('./randomClipperPort');
class Bridge {
constructor() {
this.nounce_ = Date.now();
}
async init(browser, browserSupportsPromises, dispatch) {
console.info('Popup: Init bridge');
@@ -34,6 +38,10 @@ class Bridge {
this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content });
}
if (command.name === 'isProbablyReaderable') {
this.dispatch({ type: 'IS_PROBABLY_READERABLE', value: command.value });
}
}
this.browser_.runtime.onMessage.addListener(this.browser_notify);
@@ -264,7 +272,7 @@ class Bridge {
await this.tabsSendMessage(tabs[0].id, command);
}
async clipperApiExec(method, path, body) {
async clipperApiExec(method, path, query, body) {
console.info('Popup: ' + method + ' ' + path);
const baseUrl = await this.clipperServerBaseUrl();
@@ -278,7 +286,18 @@ class Bridge {
if (body) fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
const response = await fetch(baseUrl + "/" + path, fetchOptions)
let queryString = '';
if (query) {
const s = [];
for (const k in query) {
if (!query.hasOwnProperty(k)) continue;
s.push(encodeURIComponent(k) + '=' + encodeURIComponent(query[k]));
}
queryString = s.join('&');
if (queryString) queryString = '?' + queryString;
}
const response = await fetch(baseUrl + "/" + path + queryString, fetchOptions)
if (!response.ok) {
const msg = await response.text();
throw new Error(msg);
@@ -296,11 +315,39 @@ class Bridge {
if (!content) throw new Error('Cannot send empty content');
await this.clipperApiExec('POST', 'notes', content);
// There is a bug in Chrome that somehow makes the app send the same request twice, which
// results in Joplin having the same note twice. There's a 2-3 sec delay between
// each request. The bug only happens the first time the extension popup is open and the
// Complete button is clicked.
//
// It's beyond my understanding how it's happening. I don't know how this sendContentToJoplin function
// can be called twice. But even if it is, logically, it's impossible that this
// call below would be done with twice the same nounce. Even if the function sendContentToJoplin
// is called twice in parallel, the increment is atomic and should result in two nounces
// being generated. But it's not. Somehow the function below is called twice with the exact same nounce.
//
// It's also not something internal to Chrome that repeat the request since the error is caught
// so it really seems like a double function call.
//
// So this is why below, when we get the duplicate nounce error, we just ignore it so as not to display
// a useless error message. The whole nounce feature is not for security (it's not to prevent replay
// attacks), but simply to detect these double-requests and ignore them on Joplin side.
//
// This nounce feature is optional, it's only active when the nounce query parameter is provided
// so it shouldn't affect any other call.
//
// This is the perfect Heisenbug - it happens always when opening the popup the first time EXCEPT
// when the debugger is open. Then everything is working fine and the bug NEVER EVER happens,
// so it's impossible to understand what's going on.
await this.clipperApiExec('POST', 'notes', { nounce: this.nounce_++ }, content);
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });
} catch (error) {
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: false, errorMessage: error.message } });
if (error.message === '{"error":"Duplicate Nounce"}') {
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });
} else {
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: false, errorMessage: error.message } });
}
}
}

View File

@@ -19,6 +19,7 @@ const defaultState = {
tags: [],
selectedFolderId: null,
env: 'prod',
isProbablyReaderable: true,
};
const reduxMiddleware = store => next => async (action) => {
@@ -40,6 +41,11 @@ function reducer(state = defaultState, action) {
newState = Object.assign({}, state);
newState.warning = action.text;
} else if (action.type === 'IS_PROBABLY_READERABLE') {
newState = Object.assign({}, state);
newState.isProbablyReaderable = action.value;
} else if (action.type === 'CLIPPED_CONTENT_SET') {
newState = Object.assign({}, state);

View File

@@ -81,7 +81,7 @@ class ElectronAppWrapper {
}))
// Uncomment this to view errors if the application does not start
// if (this.env_ === 'dev') this.win_.webContents.openDevTools();
if (this.env_ === 'dev') this.win_.webContents.openDevTools();
this.win_.on('close', (event) => {
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)

View File

@@ -29,6 +29,8 @@ const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const PluginManager = require('lib/services/PluginManager');
const RevisionService = require('lib/services/RevisionService');
const MigrationService = require('lib/services/MigrationService');
const pluginClasses = [
require('./plugins/GotoAnything.min'),
@@ -1031,6 +1033,12 @@ class Application extends BaseApplication {
ExternalEditWatcher.instance().setLogger(reg.logger());
ExternalEditWatcher.instance().dispatch = this.store().dispatch;
RevisionService.instance().runInBackground();
// Make it available to the console window - useful to call revisionService.collectRevisions()
window.revisionService = RevisionService.instance();
window.migrationService = MigrationService.instance();
}
}

View File

@@ -77,13 +77,16 @@ class Bridge {
});
}
showConfirmMessageBox(message) {
const result = this.showMessageBox_(this.window(), {
showConfirmMessageBox(message, options = null) {
if (options === null) options = {};
const result = this.showMessageBox_(this.window(), Object.assign({}, {
type: 'question',
message: message,
cancelId: 1,
buttons: [_('OK'), _('Cancel')],
});
}, options));
return result === 0;
}

View File

@@ -76,6 +76,26 @@ class ConfigScreenComponent extends React.Component {
</div>
);
if (section.name === 'sync') {
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
if (syncTargetMd.supportsConfigCheck) {
const messages = shared.checkSyncConfigMessages(this);
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
const statusComp = !messages.length ? null : (
<div style={statusStyle}>
{messages[0]}
{messages.length >= 1 ? (<p>{messages[1]}</p>) : null}
</div>);
settingComps.push(
<div key="check_sync_config_button" style={this.rowStyle_}>
<button disabled={this.state.checkSyncConfigResult === 'checking'} style={theme.buttonStyle} onClick={this.checkSyncConfig_}>{_('Check synchronisation configuration')}</button>
{ statusComp }
</div>);
}
}
return (
<div key={key} style={sectionStyle}>
<h2 style={headerStyle}>{Setting.sectionNameToLabel(section.name)}</h2>
@@ -265,9 +285,12 @@ class ConfigScreenComponent extends React.Component {
updateSettingValue(key, event.target.value);
};
const label = [md.label()];
if (md.unitLabel) label.push('(' + md.unitLabel() + ')');
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}><label>{md.label()}</label></div>
<div style={labelStyle}><label>{label.join(' ')}</label></div>
<input type="number" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onNumChange(event)}} min={md.minimum} max={md.maximum} step={md.step}/>
{ descriptionComp }
</div>
@@ -320,24 +343,6 @@ class ConfigScreenComponent extends React.Component {
const settingComps = shared.settingsToComponents2(this, 'desktop', settings);
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
if (syncTargetMd.supportsConfigCheck) {
const messages = shared.checkSyncConfigMessages(this);
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
const statusComp = !messages.length ? null : (
<div style={statusStyle}>
{messages[0]}
{messages.length >= 1 ? (<p>{messages[1]}</p>) : null}
</div>);
settingComps.push(
<div key="check_sync_config_button" style={this.rowStyle_}>
<button disabled={this.state.checkSyncConfigResult === 'checking'} style={buttonStyle} onClick={this.checkSyncConfig_}>{_('Check synchronisation configuration')}</button>
{ statusComp }
</div>);
}
const buttonBarStyle = {
display: 'flex',
alignItems: 'center',

View File

@@ -0,0 +1,39 @@
const React = require('react');
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
class HelpButtonComponent extends React.Component {
constructor() {
super();
this.onClick = this.onClick.bind(this);
}
onClick() {
if (this.props.onClick) this.props.onClick();
}
render() {
const theme = themeStyle(this.props.theme);
let style = Object.assign({}, this.props.style, {color: theme.color, textDecoration: 'none'});
const helpIconStyle = {flex:0, width: 16, height: 16, marginLeft: 10};
const extraProps = {};
if (this.props.tip) extraProps['data-tip'] = this.props.tip;
return <a href="#" style={style} onClick={this.onClick} {...extraProps}><i style={helpIconStyle} className={"fa fa-question-circle"}></i></a>
}
}
const mapStateToProps = (state) => {
return {
theme: state.settings.theme,
};
};
const HelpButton = connect(mapStateToProps)(HelpButtonComponent);
module.exports = HelpButton;

View File

@@ -227,6 +227,7 @@ class MainScreenComponent extends React.Component {
notePropertiesDialogOptions: {
noteId: command.noteId,
visible: true,
onRevisionLinkClick: command.onRevisionLinkClick,
},
});
} else if (command.name === 'toggleVisiblePanes') {
@@ -474,6 +475,7 @@ class MainScreenComponent extends React.Component {
theme={this.props.theme}
noteId={notePropertiesDialogOptions.noteId}
onClose={this.notePropertiesDialog_close}
onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick}
/> }
<PromptDialog

View File

@@ -17,6 +17,7 @@ class NotePropertiesDialog extends React.Component {
this.okButton_click = this.okButton_click.bind(this);
this.cancelButton_click = this.cancelButton_click.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.revisionsLink_click = this.revisionsLink_click.bind(this);
this.okButton = React.createRef();
this.state = {
@@ -31,6 +32,7 @@ class NotePropertiesDialog extends React.Component {
user_updated_time: _('Updated'),
location: _('Location'),
source_url: _('URL'),
revisionsLink: _('Note History'),
};
}
@@ -79,6 +81,7 @@ class NotePropertiesDialog extends React.Component {
formNote.location = note.latitude + ', ' + note.longitude;
}
formNote.revisionsLink = note.id;
formNote.id = note.id;
return formNote;
@@ -102,26 +105,6 @@ class NotePropertiesDialog extends React.Component {
this.styles_ = {};
this.styleKey_ = styleKey;
// this.styles_.modalLayer = {
// zIndex: 9999,
// display: 'flex',
// position: 'absolute',
// top: 0,
// left: 0,
// width: '100%',
// height: '100%',
// backgroundColor: 'rgba(0,0,0,0.6)',
// alignItems: 'flex-start',
// justifyContent: 'center',
// };
// this.styles_.dialogBox = {
// backgroundColor: theme.backgroundColor,
// padding: 16,
// boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
// marginTop: 20,
// }
this.styles_.controlBox = {
marginBottom: '1em',
color: 'black', //This will apply for the calendar
@@ -153,8 +136,6 @@ class NotePropertiesDialog extends React.Component {
borderColor: theme.dividerColor,
};
// this.styles_.dialogTitle = Object.assign({}, theme.h1Style, { marginBottom: '1.2em' });
return this.styles_;
}
@@ -181,6 +162,11 @@ class NotePropertiesDialog extends React.Component {
this.closeDialog(false);
}
revisionsLink_click() {
this.closeDialog(false);
if (this.props.onRevisionLinkClick) this.props.onRevisionLinkClick();
}
onKeyDown(event) {
if (event.keyCode === 13) {
this.closeDialog(true);
@@ -300,11 +286,13 @@ class NotePropertiesDialog extends React.Component {
url = Note.geoLocationUrlFromLatLong(ll.latitude, ll.longitude);
}
controlComp = <a href="#" onClick={() => bridge().openExternal(url)} style={theme.urlStyle}>{displayedValue}</a>
} else if (key === 'revisionsLink') {
controlComp = <a href="#" onClick={this.revisionsLink_click} style={theme.urlStyle}>{_('Previous versions of this note')}</a>
} else {
controlComp = <div style={Object.assign({}, theme.textStyle, {display: 'inline-block'})}>{displayedValue}</div>
}
if (key !== 'id') {
if (key !== 'id' && key !== 'revisionsLink') {
editCompHandler = () => {this.editPropertyButtonClick(key, value)};
editCompIcon = 'fa-edit';
}

View File

@@ -0,0 +1,175 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const NoteTextViewer = require('./NoteTextViewer.min');
const HelpButton = require('./HelpButton.min');
const BaseModel = require('lib/BaseModel');
const Revision = require('lib/models/Revision');
const Setting = require('lib/models/Setting');
const RevisionService = require('lib/services/RevisionService');
const shared = require('lib/components/shared/note-screen-shared.js');
const MdToHtml = require('lib/MdToHtml');
const { time } = require('lib/time-utils.js');
const ReactTooltip = require('react-tooltip');
const { substrWithEllipsis } = require('lib/string-utils');
class NoteRevisionViewerComponent extends React.PureComponent {
constructor() {
super();
this.state = {
revisions: [],
currentRevId: '',
note: null,
restoring: false,
};
this.viewerRef_ = React.createRef();
this.viewer_domReady = this.viewer_domReady.bind(this);
this.revisionList_onChange = this.revisionList_onChange.bind(this);
this.importButton_onClick = this.importButton_onClick.bind(this);
this.backButton_click = this.backButton_click.bind(this);
}
style() {
const theme = themeStyle(this.props.theme);
let style = {
root: {
backgroundColor: theme.backgroundColor,
display: 'flex',
flex: 1,
flexDirection: 'column',
},
titleInput: Object.assign({}, theme.inputStyle, { flex: 1 }),
revisionList: Object.assign({}, theme.dropdownList, { marginLeft: 10, flex: 0.5 }),
};
return style;
}
async viewer_domReady() {
// this.viewerRef_.current.wrappedInstance.openDevTools();
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, this.props.noteId);
this.setState({
revisions: revisions,
currentRevId: revisions.length ? revisions[revisions.length - 1].id : '',
}, () => {
this.reloadNote();
});
}
async importButton_onClick() {
if (!this.state.note) return;
this.setState({ restoring: true });
await RevisionService.instance().importRevisionNote(this.state.note);
this.setState({ restoring: false });
alert(_('The note "%s" has been successfully restored to the notebook "%s".', substrWithEllipsis(this.state.note.title, 0, 32), RevisionService.instance().restoreFolderTitle()));
}
backButton_click() {
if (this.props.onBack) this.props.onBack();
}
revisionList_onChange(event) {
const value = event.target.value;
if (!value) {
if (this.props.onBack) this.props.onBack();
} else {
this.setState({
currentRevId: value,
}, () => {
this.reloadNote();
});
}
}
async reloadNote() {
let noteBody = '';
if (!this.state.revisions.length || !this.state.currentRevId) {
noteBody = _('This note has no history');
this.setState({ note: null });
} else {
const revIndex = BaseModel.modelIndexById(this.state.revisions, this.state.currentRevId);
const note = await RevisionService.instance().revisionNote(this.state.revisions, revIndex);
if (!note) return;
noteBody = note.body;
this.setState({ note: note });
}
const theme = themeStyle(this.props.theme);
const mdToHtml = new MdToHtml({
resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/',
});
const result = mdToHtml.render(noteBody, theme, {
codeTheme: theme.codeThemeCss,
userCss: this.props.customCss ? this.props.customCss : '',
resources: await shared.attachedResources(noteBody),
});
this.viewerRef_.current.wrappedInstance.send('setHtml', result.html, { cssFiles: result.cssFiles });
}
render() {
const theme = themeStyle(this.props.theme);
const style = this.style();
const revisionListItems = [];
const revs = this.state.revisions.slice().reverse();
for (let i = 0; i < revs.length; i++) {
const rev = revs[i];
revisionListItems.push(<option
key={rev.id}
value={rev.id}
>{time.formatMsToLocal(rev.item_updated_time)}</option>);
}
const restoreButtonTitle = _('Restore');
const helpMessage = _('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.', restoreButtonTitle, RevisionService.instance().restoreFolderTitle());
const titleInput = (
<div style={{display:'flex', flexDirection: 'row', alignItems:'center', marginBottom: 10, borderWidth: 1, borderBottomStyle: 'solid', borderColor: theme.dividerColor, paddingBottom:10}}>
<button onClick={this.backButton_click} style={Object.assign({}, theme.buttonStyle, { marginRight: 10, height: theme.inputStyle.height })}>{'⬅ ' + _('Back')}</button>
<input readOnly type="text" style={style.titleInput} value={this.state.note ? this.state.note.title : ''}/>
<select disabled={!this.state.revisions.length} value={this.state.currentRevId} style={style.revisionList} onChange={this.revisionList_onChange}>
{revisionListItems}
</select>
<button disabled={!this.state.revisions.length || this.state.restoring} onClick={this.importButton_onClick} style={Object.assign({}, theme.buttonStyle, { marginLeft: 10, height: theme.inputStyle.height })}>{restoreButtonTitle}</button>
<HelpButton tip={helpMessage} id="noteRevisionHelpButton" onClick={this.helpButton_onClick}/>
</div>
);
const viewer = <NoteTextViewer
viewerStyle={{display:'flex', flex:1}}
ref={this.viewerRef_}
onDomReady={this.viewer_domReady}
/>
return (
<div style={style.root}>
{titleInput}
{viewer}
<ReactTooltip place="bottom" delayShow={300} className="help-tooltip"/>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
theme: state.settings.theme,
};
};
const NoteRevisionViewer = connect(mapStateToProps)(NoteRevisionViewerComponent);
module.exports = NoteRevisionViewer;

View File

@@ -38,6 +38,7 @@ const { clipboard } = require('electron');
const SearchEngine = require('lib/services/SearchEngine');
const ModelCache = require('lib/services/ModelCache');
const NoteTextViewer = require('./NoteTextViewer.min');
const NoteRevisionViewer = require('./NoteRevisionViewer.min');
require('brace/mode/markdown');
// https://ace.c9.io/build/kitchen-sink.html
@@ -70,6 +71,7 @@ class NoteTextComponent extends React.Component {
editorScrollTop: 0,
newNote: null,
noteTags: [],
showRevisions: false,
// If the current note was just created, and the title has never been
// changed by the user, this variable contains that note ID. Used
@@ -268,6 +270,7 @@ class NoteTextComponent extends React.Component {
this.titleField_keyDown = this.titleField_keyDown.bind(this);
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
this.webview_domReady = this.webview_domReady.bind(this);
this.noteRevisionViewer_onBack = this.noteRevisionViewer_onBack.bind(this);
}
// Note:
@@ -501,7 +504,7 @@ class NoteTextComponent extends React.Component {
// 2. It resets the undo manager - fixes https://github.com/laurent22/joplin/issues/355
// Note: calling undoManager.reset() doesn't work
try {
this.editor_.editor.getSession().setValue(note ? note.body : '');
this.editor_.editor.getSession().setValue(note && note.body? note.body : '');
} catch (error) {
if (error.message === "Cannot read property 'match' of undefined") {
// The internals of Ace Editor throws an exception when creating a new note,
@@ -530,7 +533,8 @@ class NoteTextComponent extends React.Component {
webviewReady: webviewReady,
folder: parentFolder,
lastKeys: [],
noteTags: noteTags
noteTags: noteTags,
showRevisions: false,
};
if (!note) {
@@ -619,6 +623,13 @@ class NoteTextComponent extends React.Component {
return shared.refreshNoteMetadata(this, force);
}
async noteRevisionViewer_onBack() {
this.setState({ showRevisions: false });
this.lastSetHtml_ = '';
this.scheduleReloadNote(this.props);
}
title_changeText(event) {
shared.noteComponent_change(this, 'title', event.target.value);
this.setState({ newAndNoTitleChangeNoteId: null });
@@ -1529,6 +1540,7 @@ class NoteTextComponent extends React.Component {
type: 'WINDOW_COMMAND',
name: 'commandNoteProperties',
noteId: n.id,
onRevisionLinkClick: () => { this.setState({ showRevisions: true}) },
});
},
});
@@ -1610,6 +1622,18 @@ class NoteTextComponent extends React.Component {
const innerWidth = rootStyle.width - rootStyle.paddingLeft - rootStyle.paddingRight - borderWidth;
if (this.state.showRevisions && note && note.id) {
rootStyle.paddingRight = rootStyle.paddingLeft;
rootStyle.paddingTop = rootStyle.paddingLeft;
rootStyle.paddingBottom = rootStyle.paddingLeft;
rootStyle.display = 'inline-flex';
return (
<div style={rootStyle}>
<NoteRevisionViewer noteId={note.id} customCss={this.props.customCss} onBack={this.noteRevisionViewer_onBack}/>
</div>
);
}
if (this.props.selectedNoteIds.length > 1) {
return this.renderMultiNotes(rootStyle);
} else if (!note || !!note.encryption_applied) { //|| (note && !this.props.newNote && this.props.noteId && note.id !== this.props.noteId)) { // note.id !== props.noteId is when the note has not been loaded yet, and the previous one is still in the state
@@ -1710,7 +1734,7 @@ class NoteTextComponent extends React.Component {
viewerStyle.borderLeft = 'none';
}
if (this.state.webviewReady) {
if (this.state.webviewReady && this.webviewRef_.current) {
let html = this.state.bodyHtml;
const htmlHasChanged = this.lastSetHtml_ !== html;

View File

@@ -18,11 +18,11 @@ class NoteTextViewerComponent extends React.Component {
}
webview_domReady(event) {
this.props.onDomReady(event);
if (this.props.onDomReady) this.props.onDomReady(event);
}
webview_ipcMessage(event) {
this.props.onIpcMessage(event);
if (this.props.onIpcMessage) this.props.onIpcMessage(event);
}
initWebview() {
@@ -67,13 +67,21 @@ class NoteTextViewerComponent extends React.Component {
}
}
componentDidUpdate() {
tryInit() {
if (!this.initialized_ && this.webviewRef_.current) {
this.initWebview();
this.initialized_ = true;
}
}
componentDidMount() {
this.tryInit();
}
componentDidUpdate() {
this.tryInit();
}
componentWillUnmount() {
this.destroyWebview();
}

View File

@@ -221,7 +221,7 @@ class SideBarComponent extends React.Component {
}
}
} else if (command.name === 'synchronize') {
this.sync_click();
if (!this.props.syncStarted) this.sync_click();
} else {
commandProcessed = false;
}

View File

@@ -127,7 +127,11 @@ class NoteListUtils {
msg = _('Delete these %d notes?', noteIds.length);
}
const ok = bridge().showConfirmMessageBox(msg);
const ok = bridge().showConfirmMessageBox(msg, {
buttons: [_('Delete'), _('Cancel')],
defaultId: 1,
});
if (!ok) return;
await Note.batchDelete(noteIds);
}

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

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

@@ -21,6 +21,7 @@ const Tag = require('lib/models/Tag.js');
const NoteTag = require('lib/models/NoteTag.js');
const MasterKey = require('lib/models/MasterKey');
const Setting = require('lib/models/Setting.js');
const Revision = require('lib/models/Revision.js');
const { Logger } = require('lib/logger.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { shimInit } = require('lib/shim-init-node.js');
@@ -42,6 +43,7 @@ BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', 'net.cozic.joplin-desktop');
Setting.setConstant('appType', 'desktop');

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.0.145",
"version": "1.0.153",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -23,9 +23,9 @@
"optional": true
},
"7zip-bin-win": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/7zip-bin-win/-/7zip-bin-win-2.1.1.tgz",
"integrity": "sha512-6VGEW7PXGroTsoI2QW3b0ea95HJmbVBHvfANKLLMzSzFA1zKqVX5ybNuhmeGpf6vA0x8FJTt6twpprDANsY5WQ==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/7zip-bin-win/-/7zip-bin-win-2.2.0.tgz",
"integrity": "sha512-uPHXapEmUtlUKTBx4asWMlxtFUWXzEY0KVEgU7QKhgO2LJzzM3kYxM6yOyUZTtYE6mhK4dDn3FDut9SCQWHzgg==",
"optional": true
},
"@types/node": {
@@ -450,15 +450,13 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true,
"optional": true
"dev": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
"optional": true,
"requires": {
"is-extglob": "^1.0.0"
}
@@ -1214,6 +1212,11 @@
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
"classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
},
"cli-boxes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
@@ -2201,6 +2204,11 @@
"resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz",
"integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU="
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"filename-regex": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
@@ -2357,25 +2365,28 @@
"dependencies": {
"abbrev": {
"version": "1.1.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true,
"optional": true
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true
},
"aproba": {
"version": "1.2.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true,
"optional": true
},
"are-we-there-yet": {
"version": "1.1.4",
"bundled": true,
"resolved": false,
"integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=",
"dev": true,
"optional": true,
"requires": {
@@ -2385,15 +2396,15 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -2401,37 +2412,40 @@
},
"chownr": {
"version": "1.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=",
"dev": true,
"optional": true
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true
},
"core-util-is": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true,
"optional": true
},
"debug": {
"version": "2.6.9",
"bundled": true,
"resolved": false,
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"optional": true,
"requires": {
@@ -2440,25 +2454,29 @@
},
"deep-extend": {
"version": "0.5.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==",
"dev": true,
"optional": true
},
"delegates": {
"version": "1.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"dev": true,
"optional": true
},
"detect-libc": {
"version": "1.0.3",
"bundled": true,
"resolved": false,
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"dev": true,
"optional": true
},
"fs-minipass": {
"version": "1.2.5",
"bundled": true,
"resolved": false,
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
"dev": true,
"optional": true,
"requires": {
@@ -2467,13 +2485,15 @@
},
"fs.realpath": {
"version": "1.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true,
"optional": true
},
"gauge": {
"version": "2.7.4",
"bundled": true,
"resolved": false,
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"dev": true,
"optional": true,
"requires": {
@@ -2489,7 +2509,8 @@
},
"glob": {
"version": "7.1.2",
"bundled": true,
"resolved": false,
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"optional": true,
"requires": {
@@ -2503,13 +2524,15 @@
},
"has-unicode": {
"version": "2.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"dev": true,
"optional": true
},
"iconv-lite": {
"version": "0.4.21",
"bundled": true,
"resolved": false,
"integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==",
"dev": true,
"optional": true,
"requires": {
@@ -2518,7 +2541,8 @@
},
"ignore-walk": {
"version": "3.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"dev": true,
"optional": true,
"requires": {
@@ -2527,7 +2551,8 @@
},
"inflight": {
"version": "1.0.6",
"bundled": true,
"resolved": false,
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"optional": true,
"requires": {
@@ -2537,51 +2562,53 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
},
"ini": {
"version": "1.3.5",
"bundled": true,
"resolved": false,
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true,
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
},
"isarray": {
"version": "1.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true,
"optional": true
},
"minimatch": {
"version": "3.0.4",
"bundled": true,
"resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"resolved": false,
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -2589,7 +2616,8 @@
},
"minizlib": {
"version": "1.1.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==",
"dev": true,
"optional": true,
"requires": {
@@ -2598,22 +2626,24 @@
},
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "2.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true,
"optional": true
},
"needle": {
"version": "2.2.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==",
"dev": true,
"optional": true,
"requires": {
@@ -2624,7 +2654,8 @@
},
"node-pre-gyp": {
"version": "0.10.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==",
"dev": true,
"optional": true,
"requires": {
@@ -2642,7 +2673,8 @@
},
"nopt": {
"version": "4.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"dev": true,
"optional": true,
"requires": {
@@ -2652,13 +2684,15 @@
},
"npm-bundled": {
"version": "1.0.3",
"bundled": true,
"resolved": false,
"integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==",
"dev": true,
"optional": true
},
"npm-packlist": {
"version": "1.1.10",
"bundled": true,
"resolved": false,
"integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==",
"dev": true,
"optional": true,
"requires": {
@@ -2668,7 +2702,8 @@
},
"npmlog": {
"version": "4.1.2",
"bundled": true,
"resolved": false,
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dev": true,
"optional": true,
"requires": {
@@ -2680,40 +2715,44 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true
},
"object-assign": {
"version": "4.1.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true,
"optional": true
},
"once": {
"version": "1.4.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
},
"os-homedir": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true,
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true,
"optional": true
},
"osenv": {
"version": "0.1.5",
"bundled": true,
"resolved": false,
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"dev": true,
"optional": true,
"requires": {
@@ -2723,19 +2762,22 @@
},
"path-is-absolute": {
"version": "1.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true,
"optional": true
},
"process-nextick-args": {
"version": "2.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true,
"optional": true
},
"rc": {
"version": "1.2.7",
"bundled": true,
"resolved": false,
"integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==",
"dev": true,
"optional": true,
"requires": {
@@ -2747,7 +2789,8 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true,
"optional": true
}
@@ -2755,7 +2798,8 @@
},
"readable-stream": {
"version": "2.3.6",
"bundled": true,
"resolved": false,
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"optional": true,
"requires": {
@@ -2770,7 +2814,8 @@
},
"rimraf": {
"version": "2.6.2",
"bundled": true,
"resolved": false,
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"dev": true,
"optional": true,
"requires": {
@@ -2779,45 +2824,50 @@
},
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
"bundled": true,
"resolved": false,
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"optional": true
},
"sax": {
"version": "1.2.4",
"bundled": true,
"resolved": false,
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true,
"optional": true
},
"semver": {
"version": "5.5.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
"dev": true,
"optional": true
},
"set-blocking": {
"version": "2.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true,
"optional": true
},
"signal-exit": {
"version": "3.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true,
"optional": true
},
"string-width": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -2826,7 +2876,8 @@
},
"string_decoder": {
"version": "1.1.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"optional": true,
"requires": {
@@ -2835,22 +2886,24 @@
},
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
},
"strip-json-comments": {
"version": "2.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true,
"optional": true
},
"tar": {
"version": "4.4.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==",
"dev": true,
"optional": true,
"requires": {
@@ -2865,13 +2918,15 @@
},
"util-deprecate": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true,
"optional": true
},
"wide-align": {
"version": "1.1.2",
"bundled": true,
"resolved": false,
"integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==",
"dev": true,
"optional": true,
"requires": {
@@ -2880,15 +2935,15 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
"dev": true,
"optional": true
"resolved": false,
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=",
"dev": true
}
}
},
@@ -2989,15 +3044,13 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true,
"optional": true
"dev": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
"optional": true,
"requires": {
"is-extglob": "^1.0.0"
}
@@ -3467,9 +3520,9 @@
}
},
"joplin-turndown-plugin-gfm": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.7.tgz",
"integrity": "sha512-z0SveNcchtWwglkO7SgvDzPnVHYk1WumD0QRcWvUchIihqXwDVlve3G8AHkIhM69LY1YdC0HCZJlSMp2spBe/g=="
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.8.tgz",
"integrity": "sha512-uXgq2zGvjiMl/sXG7946EGhh1pyGbZ0L/6z21LBi8D6BJgHQufmXdve/UP3zpgnhiFhfXvzGY10uNaTuDQ99iQ=="
},
"js-tokens": {
"version": "3.0.2",
@@ -4251,7 +4304,6 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
"dev": true,
"optional": true,
"requires": {
"remove-trailing-separator": "^1.0.1"
}
@@ -4615,8 +4667,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true,
"optional": true
"dev": true
},
"is-glob": {
"version": "2.0.1",
@@ -4899,14 +4950,36 @@
}
},
"react": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.4.0.tgz",
"integrity": "sha512-K0UrkLXSAekf5nJu89obKUM7o2vc6MMN9LYoKnCa+c+8MJRAT120xzPLENcWSRc7GYKIg0LlgJRDorrufdglQQ==",
"version": "16.8.1",
"resolved": "https://registry.npmjs.org/react/-/react-16.8.1.tgz",
"integrity": "sha512-wLw5CFGPdo7p/AgteFz7GblI2JPOos0+biSoxf1FPsGxWQZdN/pj6oToJs1crn61DL3Ln7mN86uZ4j74p31ELQ==",
"requires": {
"fbjs": "^0.8.16",
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.0"
"prop-types": "^15.6.2",
"scheduler": "^0.13.1"
},
"dependencies": {
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
},
"dependencies": {
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
}
}
}
}
},
"react-ace": {
@@ -4950,6 +5023,11 @@
"prop-types": "^15.6.0"
}
},
"react-is": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
},
"react-onclickoutside": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz",
@@ -4975,6 +5053,15 @@
}
}
},
"react-tooltip": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-3.10.0.tgz",
"integrity": "sha512-GGdxJvM1zSFztkTP7gCQbLTstWr1OOoMpJ5WZUGhimj0nhRY+MPz+92MpEnKmj0cftJ9Pd/M6FfSl0sfzmZWkg==",
"requires": {
"classnames": "^2.2.5",
"prop-types": "^15.6.0"
}
},
"read-chunk": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-2.1.0.tgz",
@@ -5149,15 +5236,13 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
"dev": true,
"optional": true
"dev": true
},
"repeat-element": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz",
"integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=",
"dev": true,
"optional": true
"dev": true
},
"repeat-string": {
"version": "1.6.1",
@@ -5303,6 +5388,15 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"scheduler": {
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
"integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.0.145",
"version": "1.0.153",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {
@@ -88,10 +88,12 @@
"chokidar": "^3.0.0",
"compare-versions": "^3.2.1",
"diacritics": "^1.3.0",
"diff-match-patch": "^1.0.4",
"electron-context-menu": "^0.9.1",
"electron-is-dev": "^0.3.0",
"electron-window-state": "^4.1.1",
"es6-promise-pool": "^2.5.0",
"file-uri-to-path": "^1.0.0",
"follow-redirects": "^1.5.0",
"form-data": "^2.3.2",
"formatcoords": "^1.1.3",
@@ -100,7 +102,7 @@
"html-entities": "^1.2.1",
"image-type": "^3.0.0",
"joplin-turndown": "^4.0.11",
"joplin-turndown-plugin-gfm": "^1.0.7",
"joplin-turndown-plugin-gfm": "^1.0.8",
"jssha": "^2.3.1",
"katex": "^0.10.0",
"levenshtein": "^1.0.5",
@@ -132,6 +134,7 @@
"react-datetime": "^2.14.0",
"react-dom": "^16.4.0",
"react-redux": "^5.0.7",
"react-tooltip": "^3.10.0",
"read-chunk": "^2.1.0",
"readability-node": "^0.1.0",
"redux": "^3.7.2",

View File

@@ -6,6 +6,7 @@ const SearchEngine = require('lib/services/SearchEngine');
const BaseModel = require('lib/BaseModel');
const Tag = require('lib/models/Tag');
const { ItemList } = require('../gui/ItemList.min');
const HelpButton = require('../gui/HelpButton.min');
const { substrWithEllipsis, surroundKeywords } = require('lib/string-utils.js');
const PLUGIN_NAME = 'gotoAnything';
@@ -61,8 +62,6 @@ class Dialog extends React.PureComponent {
row: {overflow: 'hidden', height:itemHeight, display: 'flex', justifyContent: 'center', flexDirection: 'column', paddingLeft: 10, paddingRight: 10},
help: Object.assign({}, theme.textStyle, { marginBottom: 10 }),
inputHelpWrapper: {display: 'flex', flexDirection: 'row', alignItems: 'center'},
helpIcon: {flex:0, width: 16, height: 16, marginLeft: 10},
helpButton: {color: theme.color, textDecoration: 'none'},
};
const rowTextStyle = {
@@ -194,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",
@@ -321,7 +332,7 @@ class Dialog extends React.PureComponent {
{helpComp}
<div style={style.inputHelpWrapper}>
<input autoFocus type="text" style={style.input} ref={this.inputRef} value={this.state.query} onChange={this.input_onChange} onKeyDown={this.input_onKeyDown}/>
<a href="#" style={style.helpButton} onClick={this.helpButton_onClick}><i style={style.helpIcon} className={"fa fa-question-circle"}></i></a>
<HelpButton onClick={this.helpButton_onClick}/>
</div>
{this.renderList()}
</div>

View File

@@ -113,3 +113,12 @@ table td, table th {
.note-property-box .rdt {
display: inline-block;
}
.help-tooltip {
font-family: sans-serif;
max-width: 200px;
}
:disabled {
opacity: 0.6;
}

View File

@@ -34,6 +34,9 @@ globalStyle.icon = {
globalStyle.lineInput = {
fontFamily: globalStyle.fontFamily,
maxHeight: 22,
height: 22,
paddingLeft: 5,
};
globalStyle.headerStyle = {
@@ -43,6 +46,7 @@ globalStyle.headerStyle = {
globalStyle.inputStyle = {
border: '1px solid',
height: 24,
maxHeight: 24,
paddingLeft: 5,
paddingRight: 5,
boxSizing: 'border-box',
@@ -54,13 +58,14 @@ globalStyle.containerStyle = {
};
globalStyle.buttonStyle = {
marginRight: 10,
// marginRight: 10,
border: '1px solid',
minHeight: 30,
minHeight: 26,
minWidth: 80,
maxWidth: 160,
paddingLeft: 12,
paddingRight: 12,
boxShadow: '0px 1px 1px rgba(0,0,0,0.3)',
};
const lightStyle = {
@@ -226,6 +231,8 @@ function addExtraStyles(style) {
style.dialogTitle = Object.assign({}, style.h1Style, { marginBottom: '1.2em' });
style.dropdownList = Object.assign({}, style.inputStyle);
return style;
}

View File

@@ -4,7 +4,7 @@ cd "$ROOT_DIR"
./build.sh || exit 1
cd "$ROOT_DIR/app"
./node_modules/.bin/electron . --env dev --log-level debug --open-dev-tools "$@"
./node_modules/.bin/electron . --env dev --log-level debug --no-welcome --open-dev-tools "$@"
# ./node_modules/.bin/electron . --profile ~/Temp/TestJoplin1 --env dev --log-level debug --open-dev-tools "$@"
# ./node_modules/.bin/electron . --profile ~/Temp/TestJoplin2 --env dev --log-level debug --open-dev-tools "$@"

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.143/Joplin-Setup-1.0.143.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.143/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.143/Joplin-1.0.143.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.143/Joplin-1.0.143-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.244/joplin-v1.0.244.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.252/joplin-v1.0.252.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
@@ -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 -->
@@ -299,7 +300,7 @@ It is generally recommended to enter the notes as Markdown as it makes the notes
## Custom CSS
Rendered markdown can be customized by placing a userstyle file in the profile directory `~/.config/joplin-desktop/userstyle.css` (This path might be different on your device - check at the top of the Config screen for the exact path). This file supports standard CSS syntax. Note that this file is used only when display the notes, **not when printing or exporting to PDF**. This is because printing has a lot more restrictions (for example, printing white text over a black background is usually not wanted), so special rules are applied to make it look good when printing, and a userstyle.css would interfer with that.
Rendered markdown can be customized by placing a userstyle file in the profile directory `~/.config/joplin-desktop/userstyle.css` (This path might be different on your device - check at the top of the Config screen for the exact path). This file supports standard CSS syntax. Joplin ***must*** be restarted for the new css to be applied, please ensure that Joplin is not closing to the tray, but is actually exiting. Note that this file is used only when display the notes, **not when printing or exporting to PDF**. This is because printing has a lot more restrictions (for example, printing white text over a black background is usually not wanted), so special rules are applied to make it look good when printing, and a userstyle.css would interfer with that.
# Searching
@@ -350,32 +351,32 @@ 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) | 95%
![](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 | 54%
![](https://joplinapp.org/images/flags/es/catalonia.png) | Catalan | [ca](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ca.po) | jmontane, 2018 | 78%
![](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) | 44%
![](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) | 69%
![](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. | 70%
![](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) | 100%
![](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/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/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) | 96%
![](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) | 69%
![](https://joplinapp.org/images/flags/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 86%
![](https://joplinapp.org/images/flags/country-4x3/be.png) | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 55%
![](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) | 83%
![](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) | 96%
![](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) | 90%
![](https://joplinapp.org/images/flags/country-4x3/ro.png) | Română | [ro](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ro.po) | | 54%
![](https://joplinapp.org/images/flags/country-4x3/si.png) | Slovenian | [sl_SI](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sl_SI.po) | | 68%
![](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) | 93%
![](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) | 90%
![](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) | 96%
![](https://joplinapp.org/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 96%
![](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) | 83%
![](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) | 90%
![](https://joplinapp.org/images/flags/country-4x3/kr.png) | 한국말 | [ko](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ko.po) | | 92%
![](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/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/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/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%
<!-- 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 2097480
versionName "1.0.244"
versionCode 2097488
versionName "1.0.252"
ndk {
abiFilters "armeabi-v7a", "x86"
}

View File

@@ -37,7 +37,9 @@
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
>
<!-- ============================= -->
<!-- START RN-push-notitication -->

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system"/>
<certificates src="user"/>
</trust-anchors>
</base-config>
</network-security-config>

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