mirror of
https://github.com/laurent22/joplin.git
synced 2025-03-20 20:55:18 +02:00
Merge branch 'master' of github.com:laurent22/joplin
This commit is contained in:
commit
68268cb35d
Assets/Screenshots/iOS
Screenshot_iPad_Paysage.pngScreenshot_iPad_Paysage.psdScreenshot_iPhone_Portrait.pngScreenshot_iPhone_Portrait.psdScreenshot_iPhone_Portrait_X.pngScreenshot_iPhone_Portrait_X.psd
BUILD.mdCONTRIBUTING.mdCliClient
ElectronClient
app
ElectronAppWrapper.jsapp.js
run.shgui
ConfigScreen.jsxHeader.jsxHelpButton.jsxMainScreen.jsxNotePropertiesDialog.jsxNoteRevisionViewer.jsxNoteText.jsxNoteTextViewer.jsxSideBar.jsx
main-html.jspackage-lock.jsonpackage.jsonplugins
style.csstheme.jsReactNativeClient
android/app
lib
BaseApplication.jsBaseModel.js
package-lock.jsonpackage.jsonroot.jscomponents/screens
import-enex-md-gen.jsjoplin-database.jsmodels
services
shim-init-node.jssynchronizer.jsTools
appveyor.ymldocs
readme
Binary file not shown.
Before ![]() (image error) Size: 335 KiB After ![]() (image error) Size: 336 KiB ![]() ![]() |
Binary file not shown.
Binary file not shown.
Before ![]() (image error) Size: 254 KiB After ![]() (image error) Size: 244 KiB ![]() ![]() |
Binary file not shown.
Binary file not shown.
Before ![]() (image error) Size: 249 KiB After ![]() (image error) Size: 249 KiB ![]() ![]() |
Binary file not shown.
3
BUILD.md
3
BUILD.md
@ -1,7 +1,8 @@
|
||||
[](https://travis-ci.org/laurent22/joplin) [](https://ci.appveyor.com/project/laurent22/joplin)
|
||||
|
||||
# General information
|
||||
|
||||
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when building each app.
|
||||
- The translations are built by running CliClient/build-translation.sh. You normally don't need to run this if you haven't updated the translation since the compiled files are on the repository.
|
||||
|
||||
## macOS dependencies
|
||||
|
||||
|
@ -1,7 +1,3 @@
|
||||
**IMPORTANT:** At the moment pull requests for new features are no longer being accepted. More info there: https://github.com/laurent22/joplin/issues/1112
|
||||
|
||||
* * *
|
||||
|
||||
# User support
|
||||
|
||||
For general discussion about Joplin, user support, software development questions, and to discuss new features, please go to the [Joplin Forum](https://discourse.joplin.cozic.net/). It is possible to login with your GitHub account.
|
||||
@ -18,11 +14,9 @@ Again, please check that it has not already been requested. If it has, simply **
|
||||
|
||||
# Creating a pull request
|
||||
|
||||
- If you want to add a new feature, consider asking about it before implementing it or checking existing discussions to make sure it is within the scope of the project. That scope, due to limited resources, might be narrower than you think. As a rule of thumb **if your change is likely to involve more than 50 lines of code, you should discuss it in the forum**, just so that you don't waste your time implementing something that might not be accepted.
|
||||
- If you want to add a new feature, consider asking about it before implementing it or checking existing discussions to make sure it is within the scope of the project. As a rule of thumb **if your change is likely to involve more than 50 lines of code, you should discuss it in the forum**, just so that you don't spend too much time implementing something that might not be accepted.
|
||||
|
||||
- Bug fixes have a very high chance of being accepted.
|
||||
|
||||
- A pull request that is relevant to the current roadmap has a very high chance of being accepted.
|
||||
- Bug fixes are always welcome.
|
||||
|
||||
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/master/BUILD.md) for more details.
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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');
|
||||
|
@ -15,6 +15,8 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.0.6\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
msgstr "Pro smazání tagu jej odeberte od přiřazených poznámek."
|
||||
@ -23,7 +25,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 +126,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 +249,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 +416,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 +454,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 +520,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 +577,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 +618,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 +632,7 @@ msgid "About Joplin"
|
||||
msgstr "O aplikaci Joplin"
|
||||
|
||||
msgid "Preferences..."
|
||||
msgstr ""
|
||||
msgstr "Nastavení..."
|
||||
|
||||
msgid "Check for updates..."
|
||||
msgstr "Zkontrolovat updaty..."
|
||||
@ -663,38 +666,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 +736,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 +756,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 ""
|
||||
msgstr "Procházet..."
|
||||
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Zkontrolujte nastavení synchronizace"
|
||||
|
||||
msgid "Apply"
|
||||
msgstr ""
|
||||
msgstr "Použít"
|
||||
|
||||
msgid "Submit"
|
||||
msgstr ""
|
||||
msgstr "Odeslat"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Uložit"
|
||||
@ -907,6 +915,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 +924,6 @@ msgstr "Status"
|
||||
msgid "Encryption is:"
|
||||
msgstr "Šifrování je:"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Usage"
|
||||
msgstr "Použití: %s"
|
||||
|
||||
@ -944,9 +953,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 +962,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 +985,32 @@ 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 ""
|
||||
msgstr "URL"
|
||||
|
||||
msgid "Note properties"
|
||||
msgstr ""
|
||||
msgstr "Nastavení poznámek"
|
||||
|
||||
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 +1024,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 +1052,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 +1099,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 +1121,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 +1137,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 +1151,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 +1173,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 +1200,7 @@ msgid "Unknown flag: %s"
|
||||
msgstr "Neznámý flag: %s"
|
||||
|
||||
msgid "Dropbox"
|
||||
msgstr ""
|
||||
msgstr "Dropbox"
|
||||
|
||||
msgid "File system"
|
||||
msgstr "Souborový systém"
|
||||
@ -1221,7 +1230,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"
|
||||
@ -1300,28 +1309,24 @@ 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 +1337,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 +1345,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 +1370,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 +1379,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 +1398,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 +1491,6 @@ msgstr "%d hodina"
|
||||
msgid "%d hours"
|
||||
msgstr "%d hodin"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Text editor command"
|
||||
msgstr "Textový editor"
|
||||
|
||||
@ -1533,7 +1537,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 +1545,39 @@ 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 ""
|
||||
msgstr "Ignorovat chyby TLS certifikátu"
|
||||
|
||||
#, 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 +1591,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"
|
||||
@ -1669,10 +1672,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 +1705,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 +1724,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 +1745,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 +1765,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 +1840,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 +1860,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 +1878,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"
|
||||
|
122
CliClient/package-lock.json
generated
122
CliClient/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "1.0.124",
|
||||
"version": "1.0.125",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@ -346,9 +346,9 @@
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||
},
|
||||
"color": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
|
||||
"integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.1.1.tgz",
|
||||
"integrity": "sha512-PvUltIXRjehRKPSy89VnDWFKY58xyhTLyxIg21vwQBI6qLwZNPmC8k3C1uytIgFKEpOIzN4y32iPm8231zFHIg==",
|
||||
"requires": {
|
||||
"color-convert": "^1.9.1",
|
||||
"color-string": "^1.5.2"
|
||||
@ -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",
|
||||
@ -697,9 +702,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"expand-template": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.1.1.tgz",
|
||||
"integrity": "sha512-cebqLtV8KOZfw0UI8TEFWxtczxxC1jvyUvx6H4fyp1K1FN7A4Q+uggVUlOsI1K8AGU0rwOGqP8nCapdrw8CYQg=="
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.1",
|
||||
@ -1853,9 +1858,14 @@
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz",
|
||||
"integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw=="
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
|
||||
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw=="
|
||||
},
|
||||
"napi-build-utils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz",
|
||||
"integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA=="
|
||||
},
|
||||
"ndarray": {
|
||||
"version": "1.0.18",
|
||||
@ -1899,9 +1909,9 @@
|
||||
}
|
||||
},
|
||||
"node-abi": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.4.4.tgz",
|
||||
"integrity": "sha512-DQ9Mo2mf/XectC+s6+grPPRQ1Z9gI3ZbrGv6nyXRkjwT3HrE0xvtvrfnH7YHYBLgC/KLadg+h3XHnhZw1sv88A==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.8.0.tgz",
|
||||
"integrity": "sha512-1/aa2clS0pue0HjckL62CsbhWWU35HARvBDXcJtYKbYR7LnIutmpxmXbuDMV9kEviD2lP/wACOgWmmwljghHyQ==",
|
||||
"requires": {
|
||||
"semver": "^5.4.1"
|
||||
}
|
||||
@ -2187,21 +2197,22 @@
|
||||
"integrity": "sha1-EdHhK5y2TWPjDBQ6Mw9MH1Z9qF8="
|
||||
},
|
||||
"prebuild-install": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-4.0.0.tgz",
|
||||
"integrity": "sha512-7tayxeYboJX0RbVzdnKyGl2vhQRWr6qfClEXDhOkXjuaOKCw2q8aiuFhONRYVsG/czia7KhpykIlI2S2VaPunA==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.0.tgz",
|
||||
"integrity": "sha512-aaLVANlj4HgZweKttFNUVNRxDukytuIuxeK2boIMHjagNJCiVKWFsKF4tCE3ql3GbrD2tExPQ7/pwtEJcHNZeg==",
|
||||
"requires": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"expand-template": "^1.0.2",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-abi": "^2.2.0",
|
||||
"napi-build-utils": "^1.0.1",
|
||||
"node-abi": "^2.7.0",
|
||||
"noop-logger": "^0.1.1",
|
||||
"npmlog": "^4.0.1",
|
||||
"os-homedir": "^1.0.1",
|
||||
"pump": "^2.0.1",
|
||||
"rc": "^1.1.6",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^2.7.0",
|
||||
"tar-fs": "^1.13.0",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
@ -2210,8 +2221,18 @@
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||
},
|
||||
"simple-get": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz",
|
||||
"integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==",
|
||||
"requires": {
|
||||
"decompress-response": "^3.3.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -2443,50 +2464,63 @@
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||
},
|
||||
"sharp": {
|
||||
"version": "0.20.8",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.20.8.tgz",
|
||||
"integrity": "sha512-A8NaPGWRDKpmHTi8sl2xzozYXhTQWBb/GaJ8ZPU7L/vKW8wVvd4Yq+isJ0c7p9sX5gnjPQcM3eOfHuvvnZ2fOQ==",
|
||||
"version": "0.22.1",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.22.1.tgz",
|
||||
"integrity": "sha512-lXzSk/FL5b/MpWrT1pQZneKe25stVjEbl6uhhJcTULm7PhmJgKKRbTDM/vtjyUuC/RLqL2PRyC4rpKwbv3soEw==",
|
||||
"requires": {
|
||||
"color": "^3.0.0",
|
||||
"color": "^3.1.1",
|
||||
"detect-libc": "^1.0.3",
|
||||
"fs-copy-file-sync": "^1.1.1",
|
||||
"nan": "^2.11.0",
|
||||
"nan": "^2.13.2",
|
||||
"npmlog": "^4.1.2",
|
||||
"prebuild-install": "^4.0.0",
|
||||
"semver": "^5.5.1",
|
||||
"simple-get": "^2.8.1",
|
||||
"tar": "^4.4.6",
|
||||
"prebuild-install": "^5.3.0",
|
||||
"semver": "^6.0.0",
|
||||
"simple-get": "^3.0.3",
|
||||
"tar": "^4.4.8",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chownr": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
|
||||
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz",
|
||||
"integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==",
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
|
||||
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
|
||||
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
|
||||
"integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw=="
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz",
|
||||
"integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ=="
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz",
|
||||
"integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==",
|
||||
"version": "4.4.8",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz",
|
||||
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
|
||||
"requires": {
|
||||
"chownr": "^1.0.1",
|
||||
"chownr": "^1.1.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.3.3",
|
||||
"minizlib": "^1.1.0",
|
||||
"minipass": "^2.3.4",
|
||||
"minizlib": "^1.1.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.2"
|
||||
@ -2505,9 +2539,9 @@
|
||||
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
|
||||
},
|
||||
"simple-get": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz",
|
||||
"integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.0.3.tgz",
|
||||
"integrity": "sha512-Wvre/Jq5vgoz31Z9stYWPLn0PqRqmBDpFSdypAnHu5AvRVCYPRYGnvryNLiXu8GOBNDH82J2FRHUGMjjHUpXFw==",
|
||||
"requires": {
|
||||
"decompress-response": "^3.3.0",
|
||||
"once": "^1.3.1",
|
||||
|
@ -20,7 +20,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "1.0.124",
|
||||
"version": "1.0.125",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@ -33,6 +33,7 @@
|
||||
"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",
|
||||
"follow-redirects": "^1.2.4",
|
||||
"form-data": "^2.1.4",
|
||||
@ -60,7 +61,7 @@
|
||||
"redux": "^3.7.2",
|
||||
"sax": "^1.2.2",
|
||||
"server-destroy": "^1.0.1",
|
||||
"sharp": "^0.20.8",
|
||||
"sharp": "^0.22.1",
|
||||
"sprintf-js": "^1.1.1",
|
||||
"sqlite3": "^4.0.1",
|
||||
"string-padding": "^1.0.2",
|
||||
|
@ -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
|
@ -35,7 +35,7 @@ describe('EnexToMd', function() {
|
||||
const htmlPath = basePath + '/' + htmlFilename;
|
||||
const mdPath = basePath + '/' + filename(htmlFilename) + '.md';
|
||||
|
||||
// if (htmlFilename !== 'text2.html') continue;
|
||||
// if (htmlFilename !== 'list5.html') continue;
|
||||
|
||||
const html = await shim.fsDriver().readFile(htmlPath);
|
||||
let expectedMd = await shim.fsDriver().readFile(mdPath);
|
||||
|
@ -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);
|
||||
|
16
CliClient/tests/enex_to_md/list5.html
Normal file
16
CliClient/tests/enex_to_md/list5.html
Normal file
@ -0,0 +1,16 @@
|
||||
<ul>
|
||||
<li lang="en-US">
|
||||
<div>Protocols</div>
|
||||
</li>
|
||||
<ul type="circle">
|
||||
<li lang="en-US">
|
||||
<div>two common network protocols used to send data packets over a network</div>
|
||||
</li>
|
||||
<li lang="en-US">
|
||||
<div>TCP Transmission control protocol</div>
|
||||
</li>
|
||||
</ul>
|
||||
<li lang="en-US">
|
||||
<div>Network port - a network port is a process-specific or an application-specific software construct serving as a communication endpoint, which is used by the Transport Layer protocols of Internet Protocol suite, such as UDP and TCP</div>
|
||||
</li>
|
||||
</ul>
|
7
CliClient/tests/enex_to_md/list5.md
Normal file
7
CliClient/tests/enex_to_md/list5.md
Normal file
@ -0,0 +1,7 @@
|
||||
- Protocols
|
||||
|
||||
- two common network protocols used to send data packets over a network
|
||||
|
||||
- TCP Transmission control protocol
|
||||
|
||||
- Network port - a network port is a process-specific or an application-specific software construct serving as a communication endpoint, which is used by the Transport Layer protocols of Internet Protocol suite, such as UDP and TCP
|
@ -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);
|
||||
}));
|
||||
|
||||
});
|
@ -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);
|
||||
}));
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}));
|
||||
|
||||
});
|
71
CliClient/tests/models_Revision.js
Normal file
71
CliClient/tests/models_Revision.js
Normal 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');
|
||||
}));
|
||||
|
||||
});
|
420
CliClient/tests/services_Revision.js
Normal file
420
CliClient/tests/services_Revision.js
Normal 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);
|
||||
}));
|
||||
|
||||
});
|
@ -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,40 @@ 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 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 +67,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 +98,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 +112,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 +127,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 +154,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 +248,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 +293,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 +316,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 +334,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 +352,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 +374,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 +407,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 +476,7 @@ describe('Synchronizer', function() {
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
let items = await allItems();
|
||||
let items = await allNotesFolders();
|
||||
|
||||
expect(items.length).toBe(1);
|
||||
}));
|
||||
@ -680,7 +694,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 +847,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);
|
||||
|
||||
@ -901,11 +915,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);
|
||||
|
||||
@ -1036,11 +1049,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 +1071,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 +1123,133 @@ 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');
|
||||
}));
|
||||
|
||||
});
|
||||
|
@ -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;
|
||||
@ -82,6 +85,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');
|
||||
@ -118,6 +122,7 @@ async function switchClient(id) {
|
||||
|
||||
BaseItem.encryptionService_ = encryptionServices_[id];
|
||||
Resource.encryptionService_ = encryptionServices_[id];
|
||||
BaseItem.revisionService_ = revisionServices_[id];
|
||||
|
||||
Setting.setConstant('resourceDir', resourceDir(id));
|
||||
|
||||
@ -129,21 +134,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 +180,7 @@ async function setupDatabase(id = null) {
|
||||
};
|
||||
|
||||
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
||||
databases_[id].setLogger(logger);
|
||||
await databases_[id].open({ name: filePath });
|
||||
|
||||
BaseModel.db_ = databases_[id];
|
||||
@ -200,6 +213,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 +236,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 +373,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 };
|
||||
|
@ -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)
|
||||
|
@ -29,6 +29,7 @@ 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 pluginClasses = [
|
||||
require('./plugins/GotoAnything.min'),
|
||||
@ -588,6 +589,11 @@ class Application extends BaseApplication {
|
||||
newNoteItem,
|
||||
newTodoItem,
|
||||
newNotebookItem, {
|
||||
label: _('Close Window'),
|
||||
platforms: ['darwin'],
|
||||
accelerator: 'Command+W',
|
||||
selector: 'performClose:',
|
||||
}, {
|
||||
type: 'separator',
|
||||
}, {
|
||||
label: _('Import'),
|
||||
@ -1026,6 +1032,11 @@ 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -102,11 +102,14 @@ class HeaderComponent extends React.Component {
|
||||
let icon = null;
|
||||
if (options.iconName) {
|
||||
const iconStyle = {
|
||||
fontSize: Math.round(style.fontSize * 1.4),
|
||||
fontSize: Math.round(style.fontSize * 1.1),
|
||||
color: style.color,
|
||||
};
|
||||
if (options.title) iconStyle.marginRight = 5;
|
||||
if (options.iconRotation) iconStyle.transform = 'rotate(' + options.iconRotation + 'deg)';
|
||||
if("undefined" != typeof(options.iconRotation)) {
|
||||
iconStyle.transition = "transform 0.15s ease-in-out";
|
||||
iconStyle.transform = 'rotate(' + options.iconRotation + 'deg)';
|
||||
}
|
||||
icon = <i style={iconStyle} className={"fa " + options.iconName}></i>
|
||||
}
|
||||
|
||||
|
39
ElectronClient/app/gui/HelpButton.jsx
Normal file
39
ElectronClient/app/gui/HelpButton.jsx
Normal 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;
|
@ -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
|
||||
|
@ -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';
|
||||
}
|
||||
|
175
ElectronClient/app/gui/NoteRevisionViewer.jsx
Normal file
175
ElectronClient/app/gui/NoteRevisionViewer.jsx
Normal 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;
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -152,7 +152,7 @@ class SideBarComponent extends React.Component {
|
||||
header: {
|
||||
height: itemHeight * 1.8,
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: theme.fontSize * 1.3,
|
||||
fontSize: theme.fontSize * 1.16,
|
||||
textDecoration: "none",
|
||||
boxSizing: "border-box",
|
||||
color: theme.color2,
|
||||
@ -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;
|
||||
}
|
||||
@ -509,7 +509,7 @@ class SideBarComponent extends React.Component {
|
||||
|
||||
makeHeader(key, label, iconName, extraProps = {}) {
|
||||
const style = this.style().header;
|
||||
const icon = <i style={{ fontSize: style.fontSize * 1.2, marginRight: 5 }} className={"fa " + iconName} />;
|
||||
const icon = <i style={{ fontSize: style.fontSize, marginRight: 5 }} className={"fa " + iconName} />;
|
||||
|
||||
if (extraProps.toggleblock || extraProps.onClick) {
|
||||
style.cursor = "pointer";
|
||||
@ -642,9 +642,15 @@ class SideBarComponent extends React.Component {
|
||||
|
||||
synchronizeButton(type) {
|
||||
const style = Object.assign({}, this.style().button, { marginBottom: 5 });
|
||||
const iconName = type === "sync" ? "fa-refresh" : "fa-times";
|
||||
const iconName = "fa-refresh";
|
||||
const label = type === "sync" ? _("Synchronise") : _("Cancel");
|
||||
const icon = <i style={{ fontSize: style.fontSize, marginRight: 5 }} className={"fa " + iconName} />;
|
||||
let iconStyle = { fontSize: style.fontSize, marginRight: 5 };
|
||||
|
||||
if(type !== 'sync'){
|
||||
iconStyle.animation = 'icon-infinite-rotation 1s linear infinite';
|
||||
}
|
||||
|
||||
const icon = <i style={iconStyle} className={"fa " + iconName} />;
|
||||
return (
|
||||
<a
|
||||
className="synchronize-button"
|
||||
@ -738,8 +744,8 @@ class SideBarComponent extends React.Component {
|
||||
{items}
|
||||
</div>
|
||||
<div style={{flex:0}}>
|
||||
{syncReportComp}
|
||||
{syncButton}
|
||||
{syncReportComp}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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');
|
||||
|
1785
ElectronClient/app/package-lock.json
generated
1785
ElectronClient/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.0.143",
|
||||
"version": "1.0.148",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@ -85,9 +85,10 @@
|
||||
"app-module-path": "^2.2.0",
|
||||
"async-mutex": "^0.1.3",
|
||||
"base-64": "^0.1.0",
|
||||
"chokidar": "^2.1.5",
|
||||
"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",
|
||||
@ -132,6 +133,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",
|
||||
|
@ -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 = {
|
||||
@ -321,7 +320,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>
|
||||
|
@ -100,6 +100,12 @@ table td, table th {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@keyframes icon-infinite-rotation{
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.smalltalk {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
@ -107,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;
|
||||
}
|
@ -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 = {
|
||||
@ -77,7 +82,7 @@ const lightStyle = {
|
||||
urlColor: '#155BDA',
|
||||
|
||||
backgroundColor2: "#162B3D",
|
||||
color2: "#ffffff",
|
||||
color2: "#f5f5f5",
|
||||
selectedColor2: "#0269C2",
|
||||
colorError2: "#ff6c6c",
|
||||
|
||||
@ -226,6 +231,8 @@ function addExtraStyles(style) {
|
||||
|
||||
style.dialogTitle = Object.assign({}, style.h1Style, { marginBottom: '1.2em' });
|
||||
|
||||
style.dropdownList = Object.assign({}, style.inputStyle);
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
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 . --profile c:\\Users\\Laurent\\.config\\joplin-desktop --log-level debug --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 "$@"
|
@ -70,7 +70,7 @@ if [[ ! -e ~/.joplin/VERSION ]] || [[ $(< ~/.joplin/VERSION) != "$version" ]]; t
|
||||
|
||||
# Create icon for Gnome
|
||||
echo 'Create Desktop icon.'
|
||||
if [[ $desktop =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.* ]]
|
||||
if [[ $desktop =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.* ]]
|
||||
then
|
||||
: "${TMPDIR:=/tmp}"
|
||||
# This command extracts to squashfs-root by default and can't be changed...
|
||||
@ -79,6 +79,7 @@ if [[ ! -e ~/.joplin/VERSION ]] || [[ $(< ~/.joplin/VERSION) != "$version" ]]; t
|
||||
APPIMAGE_VERSION=$(grep "^X-AppImage-BuildId=" $TMPDIR/squashfs-root/joplin.desktop | head -n 1 | cut -d " " -f 1)
|
||||
rm -rf $TMPDIR/squashfs-root
|
||||
|
||||
mkdir -p ~/.local/share/applications
|
||||
echo -e "[Desktop Entry]\nEncoding=UTF-8\nName=Joplin\nComment=Joplin for Desktop\nExec=/home/$USER/.joplin/Joplin.AppImage\nIcon=/home/$USER/.joplin/Icon512.png\nStartupWMClass=Joplin\nType=Application\nCategories=Application;\n$APPIMAGE_VERSION" >> ~/.local/share/applications/appimagekit-joplin.desktop
|
||||
echo "${COLOR_GREEN}OK${COLOR_RESET}"
|
||||
else
|
||||
|
20
README.md
20
README.md
@ -1,6 +1,6 @@
|
||||
# Joplin
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=E8JMYD2LQ8MMA&lc=GB&item_name=Joplin+Development¤cy_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) [](https://www.patreon.com/joplin) [](https://travis-ci.org/laurent22/joplin) [](https://ci.appveyor.com/project/laurent22/joplin)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=E8JMYD2LQ8MMA&lc=GB&item_name=Joplin+Development¤cy_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) [](https://www.patreon.com/joplin)
|
||||
|
||||
Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](#markdown).
|
||||
|
||||
@ -20,23 +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' height="40px" 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>(to run from a USB key, etc.)
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.143/Joplin-1.0.143.dmg'><img alt='Get it on macOS' height="40px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> |
|
||||
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' height="40px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | An Arch Linux package<br>[is also available](#terminal-application).
|
||||
|
||||
The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
|
||||
|
||||
On Linux, 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:
|
||||
|
||||
``` sh
|
||||
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.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`
|
||||
|
||||
## 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.243/joplin-v1.0.243.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.246/joplin-v1.0.246.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
|
||||
@ -307,7 +299,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
|
||||
|
||||
|
@ -90,8 +90,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097479
|
||||
versionName "1.0.243"
|
||||
versionCode 2097482
|
||||
versionName "1.0.246"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||
const SearchEngineUtils = require('lib/services/SearchEngineUtils');
|
||||
const RevisionService = require('lib/services/RevisionService');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
const BaseService = require('lib/services/BaseService');
|
||||
const SearchEngine = require('lib/services/SearchEngine');
|
||||
@ -577,6 +578,8 @@ class BaseApplication {
|
||||
time.setDateFormat(Setting.value('dateFormat'));
|
||||
time.setTimeFormat(Setting.value('timeFormat'));
|
||||
|
||||
BaseItem.revisionService_ = RevisionService.instance();
|
||||
|
||||
BaseService.logger_ = this.logger_;
|
||||
EncryptionService.instance().setLogger(this.logger_);
|
||||
BaseItem.encryptionService_ = EncryptionService.instance();
|
||||
|
@ -539,6 +539,7 @@ BaseModel.typeEnum_ = [
|
||||
['TYPE_ITEM_CHANGE', 10],
|
||||
['TYPE_NOTE_RESOURCE', 11],
|
||||
['TYPE_RESOURCE_LOCAL_STATE', 12],
|
||||
['TYPE_REVISION', 13],
|
||||
];
|
||||
|
||||
for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
|
||||
|
@ -10,7 +10,7 @@ const Setting = require('lib/models/Setting.js');
|
||||
const shared = require('lib/components/shared/config-shared.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const { reg } = require('lib/registry.js');
|
||||
import VersionInfo from 'react-native-version-info';
|
||||
const VersionInfo = require('react-native-version-info').default;
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent {
|
||||
|
||||
@ -163,10 +163,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
</View>
|
||||
);
|
||||
} else if (md.type == Setting.TYPE_INT) {
|
||||
const unitLabel = md.unitLabel ? md.unitLabel(value) : value;
|
||||
return (
|
||||
<View key={key} style={this.styles().settingContainer}>
|
||||
<Text key="label" style={this.styles().settingText}>{md.label()}</Text>
|
||||
<Slider key="control" style={this.styles().settingControl} value={value} onValueChange={(value) => updateSettingValue(key, value)} />
|
||||
<View style={{display:'flex', flexDirection: 'column', alignItems: 'center', flex:1}}>
|
||||
<Slider key="control" style={{width:'100%'}} step={md.step} minimumValue={md.minimum} maximumValue={md.maximum} value={value} onValueChange={(value) => updateSettingValue(key, value)} />
|
||||
<Text>{unitLabel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
} else if (md.type == Setting.TYPE_STRING) {
|
||||
|
@ -497,10 +497,12 @@ function enexXmlToMdArray(stream, resources) {
|
||||
|
||||
let container = state.lists[state.lists.length - 1];
|
||||
container.startedText = false;
|
||||
|
||||
const indent = ' '.repeat(state.lists.length - 1);
|
||||
if (container.tag == "ul") {
|
||||
section.lines.push("- ");
|
||||
section.lines.push(indent + "- ");
|
||||
} else {
|
||||
section.lines.push(container.counter + '. ');
|
||||
section.lines.push(indent + container.counter + '. ');
|
||||
container.counter++;
|
||||
}
|
||||
} else if (isStrongTag(n)) {
|
||||
|
@ -263,7 +263,7 @@ class JoplinDatabase extends Database {
|
||||
// must be set in the synchronizer too.
|
||||
|
||||
// Note: v16 and v17 don't do anything. They were used to debug an issue.
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18];
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
|
||||
|
||||
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
|
||||
|
||||
@ -515,6 +515,35 @@ class JoplinDatabase extends Database {
|
||||
END;`);
|
||||
}
|
||||
|
||||
if (targetVersion == 19) {
|
||||
const newTableSql = `
|
||||
CREATE TABLE revisions (
|
||||
id TEXT PRIMARY KEY,
|
||||
parent_id TEXT NOT NULL DEFAULT "",
|
||||
item_type INT NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
item_updated_time INT NOT NULL,
|
||||
title_diff TEXT NOT NULL DEFAULT "",
|
||||
body_diff TEXT NOT NULL DEFAULT "",
|
||||
metadata_diff TEXT NOT NULL DEFAULT "",
|
||||
encryption_cipher_text TEXT NOT NULL DEFAULT "",
|
||||
encryption_applied INT NOT NULL DEFAULT 0,
|
||||
updated_time INT NOT NULL,
|
||||
created_time INT NOT NULL
|
||||
);
|
||||
`;
|
||||
queries.push(this.sqlStringToLines(newTableSql)[0]);
|
||||
|
||||
queries.push('CREATE INDEX revisions_parent_id ON revisions (parent_id)');
|
||||
queries.push('CREATE INDEX revisions_item_type ON revisions (item_type)');
|
||||
queries.push('CREATE INDEX revisions_item_id ON revisions (item_id)');
|
||||
queries.push('CREATE INDEX revisions_item_updated_time ON revisions (item_updated_time)');
|
||||
queries.push('CREATE INDEX revisions_updated_time ON revisions (updated_time)');
|
||||
|
||||
queries.push('ALTER TABLE item_changes ADD COLUMN source INT NOT NULL DEFAULT 1');
|
||||
queries.push('ALTER TABLE item_changes ADD COLUMN before_change_item TEXT NOT NULL DEFAULT ""');
|
||||
}
|
||||
|
||||
queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] });
|
||||
|
||||
try {
|
||||
|
@ -106,7 +106,7 @@ class BaseItem extends BaseModel {
|
||||
let d = BaseItem.syncItemDefinitions_[i];
|
||||
if (Number(item) == d.type) return this.getClass(d.className);
|
||||
}
|
||||
throw new Error('Unknown type: ' + item);
|
||||
throw new JoplinError('Unknown type: ' + item, 'unknownItemType');
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,6 +160,7 @@ class BaseItem extends BaseModel {
|
||||
}
|
||||
|
||||
static async batchDelete(ids, options = null) {
|
||||
if (!options) options = {};
|
||||
let trackDeleted = true;
|
||||
if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted;
|
||||
|
||||
@ -219,6 +220,9 @@ class BaseItem extends BaseModel {
|
||||
if (['created_time', 'updated_time', 'sync_time', 'user_updated_time', 'user_created_time'].indexOf(propName) >= 0) {
|
||||
if (!propValue) return '';
|
||||
propValue = moment.unix(propValue / 1000).utc().format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
|
||||
} else if (['title_diff', 'body_diff'].indexOf(propName) >= 0) {
|
||||
if (!propValue) return '';
|
||||
propValue = JSON.stringify(propValue);
|
||||
} else if (propValue === null || propValue === undefined) {
|
||||
propValue = '';
|
||||
}
|
||||
@ -234,6 +238,9 @@ class BaseItem extends BaseModel {
|
||||
if (['created_time', 'updated_time', 'user_created_time', 'user_updated_time'].indexOf(propName) >= 0) {
|
||||
if (!propValue) return 0;
|
||||
propValue = moment(propValue, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x');
|
||||
} else if (['title_diff', 'body_diff'].indexOf(propName) >= 0) {
|
||||
if (!propValue) return '';
|
||||
propValue = JSON.parse(propValue);
|
||||
} else {
|
||||
propValue = Database.formatValue(ItemClass.fieldType(propName), propValue);
|
||||
}
|
||||
@ -279,7 +286,7 @@ class BaseItem extends BaseModel {
|
||||
|
||||
let temp = [];
|
||||
|
||||
if (output.title) temp.push(output.title);
|
||||
if (typeof output.title === "string") temp.push(output.title);
|
||||
if (output.body) temp.push(output.body);
|
||||
if (output.props.length) temp.push(output.props.join("\n"));
|
||||
|
||||
@ -291,19 +298,16 @@ class BaseItem extends BaseModel {
|
||||
return this.encryptionService_;
|
||||
}
|
||||
|
||||
static revisionService() {
|
||||
if (!this.revisionService_) throw new Error('BaseItem.revisionService_ is not set!!');
|
||||
return this.revisionService_;
|
||||
}
|
||||
|
||||
static async serializeForSync(item) {
|
||||
const ItemClass = this.itemClass(item);
|
||||
let shownKeys = ItemClass.fieldNames();
|
||||
shownKeys.push('type_');
|
||||
|
||||
// if (ItemClass.syncExcludedKeys) {
|
||||
// const keys = ItemClass.syncExcludedKeys();
|
||||
// for (let i = 0; i < keys.length; i++) {
|
||||
// const idx = shownKeys.indexOf(keys[i]);
|
||||
// shownKeys.splice(idx, 1);
|
||||
// }
|
||||
// }
|
||||
|
||||
const serialized = await ItemClass.serialize(item, shownKeys);
|
||||
|
||||
if (!Setting.value('encryption.enabled') || !ItemClass.encryptionSupported()) {
|
||||
@ -372,7 +376,7 @@ class BaseItem extends BaseModel {
|
||||
body.splice(0, 0, line);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!output.type_) throw new Error('Missing required property: type_: ' + content);
|
||||
output.type_ = Number(output.type_);
|
||||
|
||||
@ -597,7 +601,7 @@ class BaseItem extends BaseModel {
|
||||
static updateSyncTimeQueries(syncTarget, item, syncTime, syncDisabled = false, syncDisabledReason = '') {
|
||||
const itemType = item.type_;
|
||||
const itemId = item.id;
|
||||
if (!itemType || !itemId || syncTime === undefined) throw new Error('Invalid parameters in updateSyncTimeQueries()');
|
||||
if (!itemType || !itemId || syncTime === undefined) throw new Error(sprintf('Invalid parameters in updateSyncTimeQueries(): %d, %s, %d', syncTarget, JSON.stringify(item), syncTime));
|
||||
|
||||
return [
|
||||
{
|
||||
@ -644,7 +648,7 @@ class BaseItem extends BaseModel {
|
||||
|
||||
static displayTitle(item) {
|
||||
if (!item) return '';
|
||||
return !!item.encryption_applied ? '🔑 ' + _('Encrypted') : item.title + '';
|
||||
return !!item.encryption_applied ? '🔑 ' + _('Encrypted') : (!!item.title)? item.title + '' : Note.defaultTitle(item);
|
||||
}
|
||||
|
||||
static async markAllNonEncryptedForSync() {
|
||||
@ -704,6 +708,7 @@ class BaseItem extends BaseModel {
|
||||
}
|
||||
|
||||
BaseItem.encryptionService_ = null;
|
||||
BaseItem.revisionService_ = null;
|
||||
|
||||
// Also update:
|
||||
// - itemsThatNeedSync()
|
||||
@ -716,6 +721,7 @@ BaseItem.syncItemDefinitions_ = [
|
||||
{ type: BaseModel.TYPE_TAG, className: 'Tag' },
|
||||
{ type: BaseModel.TYPE_NOTE_TAG, className: 'NoteTag' },
|
||||
{ type: BaseModel.TYPE_MASTER_KEY, className: 'MasterKey' },
|
||||
{ type: BaseModel.TYPE_REVISION, className: 'Revision' },
|
||||
];
|
||||
|
||||
module.exports = BaseItem;
|
@ -11,7 +11,10 @@ class ItemChange extends BaseModel {
|
||||
return BaseModel.TYPE_ITEM_CHANGE;
|
||||
}
|
||||
|
||||
static async add(itemType, itemId, type) {
|
||||
static async add(itemType, itemId, type, changeSource = null, beforeChangeItemJson = null) {
|
||||
if (changeSource === null) changeSource = ItemChange.SOURCE_UNSPECIFIED;
|
||||
if (!beforeChangeItemJson) beforeChangeItemJson = '';
|
||||
|
||||
ItemChange.saveCalls_.push(true);
|
||||
|
||||
// Using a mutex so that records can be added to the database in the
|
||||
@ -21,7 +24,7 @@ class ItemChange extends BaseModel {
|
||||
try {
|
||||
await this.db().transactionExecBatch([
|
||||
{ sql: 'DELETE FROM item_changes WHERE item_id = ?', params: [itemId] },
|
||||
{ sql: 'INSERT INTO item_changes (item_type, item_id, type, created_time) VALUES (?, ?, ?, ?)', params: [itemType, itemId, type, Date.now()] },
|
||||
{ sql: 'INSERT INTO item_changes (item_type, item_id, type, source, created_time, before_change_item) VALUES (?, ?, ?, ?, ?, ?)', params: [itemType, itemId, type, changeSource, Date.now(), beforeChangeItemJson] },
|
||||
]);
|
||||
} finally {
|
||||
release();
|
||||
@ -61,4 +64,7 @@ ItemChange.TYPE_CREATE = 1;
|
||||
ItemChange.TYPE_UPDATE = 2;
|
||||
ItemChange.TYPE_DELETE = 3;
|
||||
|
||||
ItemChange.SOURCE_UNSPECIFIED = 1;
|
||||
ItemChange.SOURCE_SYNC = 2;
|
||||
|
||||
module.exports = ItemChange;
|
@ -525,14 +525,32 @@ class Note extends BaseItem {
|
||||
return this.save(newNote);
|
||||
}
|
||||
|
||||
static async noteIsOlderThan(noteId, date) {
|
||||
const n = await this.db().selectOne('SELECT updated_time FROM notes WHERE id = ?', [noteId]);
|
||||
if (!n) throw new Error('No such note: ' + noteId);
|
||||
return n.updated_time < date;
|
||||
}
|
||||
|
||||
static async save(o, options = null) {
|
||||
let isNew = this.isNew(o, options);
|
||||
if (isNew && !o.source) o.source = Setting.value('appName');
|
||||
if (isNew && !o.source_application) o.source_application = Setting.value('appId');
|
||||
|
||||
// We only keep the previous note content for "old notes" (see Revision Service for more info)
|
||||
// In theory, we could simply save all the previous note contents, and let the revision service
|
||||
// decide what to keep and what to ignore, but in practice keeping the previous content is a bit
|
||||
// heavy - the note needs to be reloaded here, the JSON blob needs to be saved, etc.
|
||||
// So the check for old note here is basically an optimisation.
|
||||
let beforeNoteJson = null;
|
||||
if (!isNew && this.revisionService().isOldNote(o.id)) {
|
||||
beforeNoteJson = await Note.load(o.id);
|
||||
if (beforeNoteJson) beforeNoteJson = JSON.stringify(beforeNoteJson);
|
||||
}
|
||||
|
||||
const note = await super.save(o, options);
|
||||
|
||||
ItemChange.add(BaseModel.TYPE_NOTE, note.id, isNew ? ItemChange.TYPE_CREATE : ItemChange.TYPE_UPDATE);
|
||||
const changeSource = options && options.changeSource ? options.changeSource : null;
|
||||
ItemChange.add(BaseModel.TYPE_NOTE, note.id, isNew ? ItemChange.TYPE_CREATE : ItemChange.TYPE_UPDATE, changeSource, beforeNoteJson);
|
||||
|
||||
this.dispatch({
|
||||
type: 'NOTE_UPDATE_ONE',
|
||||
@ -550,16 +568,29 @@ class Note extends BaseItem {
|
||||
}
|
||||
|
||||
static async batchDelete(ids, options = null) {
|
||||
const result = await super.batchDelete(ids, options);
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
ItemChange.add(BaseModel.TYPE_NOTE, ids[i], ItemChange.TYPE_DELETE);
|
||||
ids = ids.slice();
|
||||
|
||||
this.dispatch({
|
||||
type: 'NOTE_DELETE',
|
||||
id: ids[i],
|
||||
});
|
||||
while (ids.length) {
|
||||
const processIds = ids.splice(0, 50);
|
||||
|
||||
const notes = await Note.byIds(processIds);
|
||||
const beforeChangeItems = {};
|
||||
for (const note of notes) {
|
||||
beforeChangeItems[note.id] = JSON.stringify(note);
|
||||
}
|
||||
|
||||
const result = await super.batchDelete(processIds, options);
|
||||
const changeSource = options && options.changeSource ? options.changeSource : null;
|
||||
for (let i = 0; i < processIds.length; i++) {
|
||||
const id = processIds[i];
|
||||
ItemChange.add(BaseModel.TYPE_NOTE, id, ItemChange.TYPE_DELETE, changeSource, beforeChangeItems[id]);
|
||||
|
||||
this.dispatch({
|
||||
type: 'NOTE_DELETE',
|
||||
id: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static dueNotes() {
|
||||
|
248
ReactNativeClient/lib/models/Revision.js
Normal file
248
ReactNativeClient/lib/models/Revision.js
Normal file
@ -0,0 +1,248 @@
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const NoteTag = require('lib/models/NoteTag.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { _ } = require('lib/locale');
|
||||
const DiffMatchPatch = require('diff-match-patch');
|
||||
const ArrayUtils = require('lib/ArrayUtils.js');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
|
||||
const dmp = new DiffMatchPatch();
|
||||
|
||||
class Revision extends BaseItem {
|
||||
|
||||
static tableName() {
|
||||
return 'revisions';
|
||||
}
|
||||
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_REVISION;
|
||||
}
|
||||
|
||||
static createTextPatch(oldText, newText) {
|
||||
return dmp.patch_toText(dmp.patch_make(oldText, newText));
|
||||
}
|
||||
|
||||
static applyTextPatch(text, patch) {
|
||||
patch = dmp.patch_fromText(patch);
|
||||
const result = dmp.patch_apply(patch, text);
|
||||
if (!result || !result.length) throw new Error('Could not apply patch');
|
||||
return result[0];
|
||||
}
|
||||
|
||||
static createObjectPatch(oldObject, newObject) {
|
||||
if (!oldObject) oldObject = {};
|
||||
|
||||
const output = {
|
||||
new: {},
|
||||
deleted: [],
|
||||
};
|
||||
|
||||
for (let k in newObject) {
|
||||
if (!newObject.hasOwnProperty(k)) continue;
|
||||
if (oldObject[k] === newObject[k]) continue;
|
||||
output.new[k] = newObject[k];
|
||||
}
|
||||
|
||||
for (let k in oldObject) {
|
||||
if (!oldObject.hasOwnProperty(k)) continue;
|
||||
if (!(k in newObject)) output.deleted.push(k);
|
||||
}
|
||||
|
||||
return JSON.stringify(output);
|
||||
}
|
||||
|
||||
static applyObjectPatch(object, patch) {
|
||||
patch = JSON.parse(patch);
|
||||
const output = Object.assign({}, object);
|
||||
|
||||
for (let k in patch.new) {
|
||||
output[k] = patch.new[k];
|
||||
}
|
||||
|
||||
for (let i = 0; i < patch.deleted.length; i++) {
|
||||
delete output[patch.deleted[i]];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static async countRevisions(itemType, itemId) {
|
||||
const r = await this.db().selectOne('SELECT count(*) as total FROM revisions WHERE item_type = ? AND item_id = ?', [
|
||||
itemType,
|
||||
itemId,
|
||||
]);
|
||||
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static latestRevision(itemType, itemId) {
|
||||
return this.modelSelectOne('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time DESC LIMIT 1', [
|
||||
itemType,
|
||||
itemId,
|
||||
]);
|
||||
}
|
||||
|
||||
static allByType(itemType, itemId) {
|
||||
return this.modelSelectAll('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? ORDER BY item_updated_time ASC', [
|
||||
itemType,
|
||||
itemId,
|
||||
]);
|
||||
}
|
||||
|
||||
static async itemsWithRevisions(itemType, itemIds) {
|
||||
if (!itemIds.length) return [];
|
||||
const rows = await this.db().selectAll('SELECT distinct item_id FROM revisions WHERE item_type = ? AND item_id IN ("' + itemIds.join('","') + '")', [
|
||||
itemType,
|
||||
]);
|
||||
|
||||
return rows.map(r => r.item_id);
|
||||
}
|
||||
|
||||
static async itemsWithNoRevisions(itemType, itemIds) {
|
||||
const withRevs = await this.itemsWithRevisions(itemType, itemIds);
|
||||
const output = [];
|
||||
for (let i = 0; i < itemIds.length; i++) {
|
||||
if (withRevs.indexOf(itemIds[i]) < 0) output.push(itemIds[i]);
|
||||
}
|
||||
return ArrayUtils.unique(output);
|
||||
}
|
||||
|
||||
static moveRevisionToTop(revision, revs) {
|
||||
let targetIndex = -1;
|
||||
for (let i = revs.length - 1; i >= 0; i--) {
|
||||
const rev = revs[i];
|
||||
if (rev.id === revision.id) {
|
||||
targetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetIndex < 0) throw new Error('Could not find revision: ' + revision.id);
|
||||
|
||||
if (targetIndex !== revs.length - 1) {
|
||||
revs = revs.slice();
|
||||
const toTop = revs[targetIndex];
|
||||
revs.splice(targetIndex, 1);
|
||||
revs.push(toTop);
|
||||
}
|
||||
|
||||
return revs;
|
||||
}
|
||||
|
||||
// Note: revs must be sorted by update_time ASC (as returned by allByType)
|
||||
static async mergeDiffs(revision, revs = null) {
|
||||
if (!('encryption_applied' in revision) || !!revision.encryption_applied) throw new JoplinError('Target revision is encrypted', 'revision_encrypted');
|
||||
|
||||
if (!revs) {
|
||||
revs = await this.modelSelectAll('SELECT * FROM revisions WHERE item_type = ? AND item_id = ? AND item_updated_time <= ? ORDER BY item_updated_time ASC', [
|
||||
revision.item_type,
|
||||
revision.item_id,
|
||||
revision.item_updated_time,
|
||||
]);
|
||||
} else {
|
||||
revs = revs.slice();
|
||||
}
|
||||
|
||||
// Handle rare case where two revisions have been created at exactly the same millisecond
|
||||
// Also handle even rarer case where a rev and its parent have been created at the
|
||||
// same milliseconds. All code below expects target revision to be on top.
|
||||
revs = this.moveRevisionToTop(revision, revs);
|
||||
|
||||
const output = {
|
||||
title: '',
|
||||
body: '',
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
// Build up the list of revisions that are parents of the target revision.
|
||||
const revIndexes = [revs.length - 1];
|
||||
let parentId = revision.parent_id;
|
||||
for (let i = revs.length - 2; i >= 0; i--) {
|
||||
const rev = revs[i];
|
||||
if (rev.id !== parentId) continue;
|
||||
parentId = rev.parent_id;
|
||||
revIndexes.push(i);
|
||||
}
|
||||
revIndexes.reverse();
|
||||
|
||||
for (const revIndex of revIndexes) {
|
||||
const rev = revs[revIndex];
|
||||
if (!!rev.encryption_applied) throw new JoplinError(sprintf('Revision "%s" is encrypted', rev.id), 'revision_encrypted');
|
||||
output.title = this.applyTextPatch(output.title, rev.title_diff);
|
||||
output.body = this.applyTextPatch(output.body, rev.body_diff);
|
||||
output.metadata = this.applyObjectPatch(output.metadata, rev.metadata_diff);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static async deleteOldRevisions(ttl) {
|
||||
// When deleting old revisions, we need to make sure that the oldest surviving revision
|
||||
// is a "merged" one (as opposed to a diff from a now deleted revision). So every time
|
||||
// we deleted a revision, we need to find if there's a corresponding surviving revision
|
||||
// and modify that revision into a "merged" one.
|
||||
|
||||
const cutOffDate = Date.now() - ttl;
|
||||
const revisions = await this.modelSelectAll('SELECT * FROM revisions WHERE item_updated_time < ? ORDER BY item_updated_time DESC', [cutOffDate]);
|
||||
const doneItems = {};
|
||||
|
||||
for (const rev of revisions) {
|
||||
const doneKey = rev.item_type + '_' + rev.item_id;
|
||||
if (doneItems[doneKey]) continue;
|
||||
|
||||
const keptRev = await this.modelSelectOne('SELECT * FROM revisions WHERE item_updated_time >= ? AND item_type = ? AND item_id = ? ORDER BY item_updated_time ASC LIMIT 1', [
|
||||
cutOffDate,
|
||||
rev.item_type,
|
||||
rev.item_id,
|
||||
]);
|
||||
|
||||
try {
|
||||
const deleteQueryCondition = 'item_updated_time < ? AND item_id = ?';
|
||||
const deleteQueryParams = [cutOffDate, rev.item_id];
|
||||
const deleteQuery = { sql: 'DELETE FROM revisions WHERE ' + deleteQueryCondition, params: deleteQueryParams };
|
||||
|
||||
if (!keptRev) {
|
||||
const hasEncrypted = await this.modelSelectOne('SELECT * FROM revisions WHERE encryption_applied = 1 AND ' + deleteQueryCondition, deleteQueryParams);
|
||||
if (!!hasEncrypted) throw new JoplinError('One of the revision to be deleted is encrypted', 'revision_encrypted');
|
||||
await this.db().transactionExecBatch([deleteQuery]);
|
||||
} else {
|
||||
// Note: we don't need to check for encrypted rev here because
|
||||
// mergeDiff will already throw the revision_encrypted exception
|
||||
// if a rev is encrypted.
|
||||
const merged = await this.mergeDiffs(keptRev);
|
||||
|
||||
const queries = [
|
||||
deleteQuery,
|
||||
{ sql: 'UPDATE revisions SET title_diff = ?, body_diff = ?, metadata_diff = ? WHERE id = ?', params: [
|
||||
this.createTextPatch('', merged.title),
|
||||
this.createTextPatch('', merged.body),
|
||||
this.createObjectPatch({}, merged.metadata),
|
||||
keptRev.id,
|
||||
] },
|
||||
];
|
||||
|
||||
await this.db().transactionExecBatch(queries);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'revision_encrypted') {
|
||||
this.logger().info('Aborted deletion of old revisions for item ' + rev.item_id + ' because one of the revisions is still encrypted', error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
doneItems[doneKey] = true;
|
||||
}
|
||||
}
|
||||
|
||||
static async revisionExists(itemType, itemId, updatedTime) {
|
||||
const existingRev = await Revision.latestRevision(itemType, itemId);
|
||||
return existingRev && existingRev.item_updated_time === updatedTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Revision;
|
@ -191,7 +191,15 @@ class Setting extends BaseModel {
|
||||
|
||||
'resourceService.lastProcessedChangeId': { value: 0, type: Setting.TYPE_INT, public: false },
|
||||
'searchEngine.lastProcessedChangeId': { value: 0, type: Setting.TYPE_INT, public: false },
|
||||
'revisionService.lastProcessedChangeId': { value: 0, type: Setting.TYPE_INT, public: false },
|
||||
|
||||
'searchEngine.initialIndexingDone': { value: false, type: Setting.TYPE_BOOL, public: false },
|
||||
|
||||
'revisionService.enabled': { section: 'revisionService', value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Enable note history') },
|
||||
'revisionService.ttlDays': { section: 'revisionService', value: 90, type: Setting.TYPE_INT, public: true, minimum: 1, maximum: 365 * 2, step: 1, unitLabel: (value = null) => { return value === null ? _('days') : _('%d days', value) }, label: () => _('Keep note history for') },
|
||||
'revisionService.intervalBetweenRevisions': { section: 'revisionService', value: 1000 * 60 * 10, type: Setting.TYPE_INT, public: false },
|
||||
'revisionService.oldNoteInterval': { section: 'revisionService', value: 1000 * 60 * 60 * 24 * 7, type: Setting.TYPE_INT, public: false },
|
||||
|
||||
'welcome.wasBuilt': { value: false, type: Setting.TYPE_BOOL, public: false },
|
||||
};
|
||||
|
||||
@ -586,6 +594,7 @@ class Setting extends BaseModel {
|
||||
if (name === 'note') return _('Note');
|
||||
if (name === 'plugins') return _('Plugins');
|
||||
if (name === 'application') return _('Application');
|
||||
if (name === 'revisionService') return _('Note History');
|
||||
return name;
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ const { splitCommandString } = require('lib/string-utils');
|
||||
const { fileExtension } = require('lib/path-utils');
|
||||
const spawn = require('child_process').spawn;
|
||||
const chokidar = require('chokidar');
|
||||
// const chokidar = null;
|
||||
|
||||
class ExternalEditWatcher {
|
||||
|
||||
|
@ -7,6 +7,7 @@ class ItemChangeUtils {
|
||||
const lastProcessedChangeIds = [
|
||||
Setting.value('resourceService.lastProcessedChangeId'),
|
||||
Setting.value('searchEngine.lastProcessedChangeId'),
|
||||
Setting.value('revisionService.lastProcessedChangeId'),
|
||||
];
|
||||
|
||||
const lowestChangeId = Math.min(...lastProcessedChangeIds);
|
||||
|
@ -93,6 +93,7 @@ class ResourceService extends BaseService {
|
||||
}
|
||||
|
||||
async deleteOrphanResources(expiryDelay = null) {
|
||||
if (expiryDelay === null) expiryDelay = Setting.value('revisionService.ttlDays') * 24 * 60 * 60 * 1000;
|
||||
const resourceIds = await NoteResource.orphanResources(expiryDelay);
|
||||
this.logger().info('ResourceService::deleteOrphanResources:', resourceIds);
|
||||
for (let i = 0; i < resourceIds.length; i++) {
|
||||
|
265
ReactNativeClient/lib/services/RevisionService.js
Normal file
265
ReactNativeClient/lib/services/RevisionService.js
Normal file
@ -0,0 +1,265 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const ItemChange = require('lib/models/ItemChange');
|
||||
const Note = require('lib/models/Note');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const Revision = require('lib/models/Revision');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const ItemChangeUtils = require('lib/services/ItemChangeUtils');
|
||||
const { shim } = require('lib/shim');
|
||||
const BaseService = require('lib/services/BaseService');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const ArrayUtils = require('lib/ArrayUtils.js');
|
||||
|
||||
class RevisionService extends BaseService {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// An "old note" is one that has been created before the revision service existed. These
|
||||
// notes never benefited from revisions so the first time they are modified, a copy of
|
||||
// the original note is saved. The goal is to have at least one revision in case the note
|
||||
// is deleted or modified as a result of a bug or user mistake.
|
||||
this.isOldNotesCache_ = {};
|
||||
}
|
||||
|
||||
static instance() {
|
||||
if (this.instance_) return this.instance_;
|
||||
this.instance_ = new RevisionService();
|
||||
return this.instance_;
|
||||
}
|
||||
|
||||
oldNoteCutOffDate_() {
|
||||
return Date.now() - Setting.value('revisionService.oldNoteInterval');
|
||||
}
|
||||
|
||||
async isOldNote(noteId) {
|
||||
if (noteId in this.isOldNotesCache_) return this.isOldNotesCache_[noteId];
|
||||
|
||||
const isOld = await Note.noteIsOlderThan(noteId, this.oldNoteCutOffDate_());
|
||||
this.isOldNotesCache_[noteId] = isOld;
|
||||
return isOld;
|
||||
}
|
||||
|
||||
noteMetadata_(note) {
|
||||
const excludedFields = ['type_', 'title', 'body', 'created_time', 'updated_time', 'encryption_applied', 'encryption_cipher_text', 'is_conflict'];
|
||||
const md = {};
|
||||
for (let k in note) {
|
||||
if (excludedFields.indexOf(k) >= 0) continue;
|
||||
md[k] = note[k];
|
||||
}
|
||||
|
||||
if (note.user_updated_time === note.updated_time) delete md.user_updated_time;
|
||||
if (note.user_created_time === note.created_time) delete md.user_created_time;
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
isEmptyRevision_(rev) {
|
||||
if (!!rev.title_diff) return false;
|
||||
if (!!rev.body_diff) return false;
|
||||
|
||||
const md = JSON.parse(rev.metadata_diff);
|
||||
if (md.new && Object.keys(md.new).length) return false;
|
||||
if (md.deleted && Object.keys(md.deleted).length) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async createNoteRevision_(note, parentRevId = null) {
|
||||
const parentRev = parentRevId ? await Revision.load(parentRevId) : await Revision.latestRevision(BaseModel.TYPE_NOTE, note.id);
|
||||
|
||||
const output = {
|
||||
parent_id: '',
|
||||
item_type: BaseModel.TYPE_NOTE,
|
||||
item_id: note.id,
|
||||
item_updated_time: note.updated_time,
|
||||
};
|
||||
|
||||
const noteMd = this.noteMetadata_(note);
|
||||
const noteTitle = note.title ? note.title : '';
|
||||
const noteBody = note.body ? note.body : '';
|
||||
|
||||
if (!parentRev) {
|
||||
output.title_diff = Revision.createTextPatch('', noteTitle);
|
||||
output.body_diff = Revision.createTextPatch('', noteBody);
|
||||
output.metadata_diff = Revision.createObjectPatch({}, noteMd);
|
||||
} else {
|
||||
if (Date.now() - parentRev.updated_time < Setting.value('revisionService.intervalBetweenRevisions')) return null;
|
||||
|
||||
const merged = await Revision.mergeDiffs(parentRev);
|
||||
output.parent_id = parentRev.id;
|
||||
output.title_diff = Revision.createTextPatch(merged.title, noteTitle);
|
||||
output.body_diff = Revision.createTextPatch(merged.body, noteBody);
|
||||
output.metadata_diff = Revision.createObjectPatch(merged.metadata, noteMd);
|
||||
}
|
||||
|
||||
if (this.isEmptyRevision_(output)) return null;
|
||||
|
||||
return Revision.save(output);
|
||||
}
|
||||
|
||||
async collectRevisions() {
|
||||
if (this.isCollecting_) return;
|
||||
|
||||
this.isCollecting_ = true;
|
||||
|
||||
await ItemChange.waitForAllSaved();
|
||||
|
||||
const doneNoteIds = [];
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
// See synchronizer test units to see why changes coming
|
||||
// from sync are skipped.
|
||||
const changes = await ItemChange.modelSelectAll(`
|
||||
SELECT id, item_id, type, before_change_item
|
||||
FROM item_changes
|
||||
WHERE item_type = ?
|
||||
AND source != ?
|
||||
AND id > ?
|
||||
ORDER BY id ASC
|
||||
LIMIT 10
|
||||
`, [BaseModel.TYPE_NOTE, ItemChange.SOURCE_SYNC, Setting.value('revisionService.lastProcessedChangeId')]);
|
||||
|
||||
if (!changes.length) break;
|
||||
|
||||
const noteIds = changes.map(a => a.item_id);
|
||||
const notes = await Note.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0 AND encryption_applied = 0 AND id IN ("' + noteIds.join('","') + '")');
|
||||
|
||||
for (let i = 0; i < changes.length; i++) {
|
||||
const change = changes[i];
|
||||
const noteId = change.item_id;
|
||||
|
||||
if (change.type === ItemChange.TYPE_UPDATE && doneNoteIds.indexOf(noteId) < 0) {
|
||||
const note = BaseModel.byId(notes, noteId);
|
||||
const oldNote = change.before_change_item ? JSON.parse(change.before_change_item) : null;
|
||||
|
||||
if (note) {
|
||||
if (oldNote && oldNote.updated_time < this.oldNoteCutOffDate_()) {
|
||||
// This is where we save the original version of this old note
|
||||
await this.createNoteRevision_(oldNote);
|
||||
}
|
||||
|
||||
await this.createNoteRevision_(note);
|
||||
doneNoteIds.push(noteId);
|
||||
this.isOldNotesCache_[noteId] = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (change.type === ItemChange.TYPE_DELETE && !!change.before_change_item) {
|
||||
const note = JSON.parse(change.before_change_item);
|
||||
const revExists = await Revision.revisionExists(BaseModel.TYPE_NOTE, note.id, note.updated_time);
|
||||
if (!revExists) await this.createNoteRevision_(note);
|
||||
doneNoteIds.push(noteId);
|
||||
}
|
||||
|
||||
Setting.setValue('revisionService.lastProcessedChangeId', change.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'revision_encrypted') {
|
||||
// One or more revisions are encrypted - stop processing for now
|
||||
// and these revisions will be processed next time the revision
|
||||
// collector runs.
|
||||
this.logger().info('RevisionService::collectRevisions: One or more revision was encrypted. Processing was stopped but will resume later when the revision is decrypted.', error);
|
||||
} else {
|
||||
this.logger().error('RevisionService::collectRevisions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
await Setting.saveAll();
|
||||
await ItemChangeUtils.deleteProcessedChanges();
|
||||
|
||||
this.isCollecting_ = false;
|
||||
|
||||
this.logger().info('RevisionService::collectRevisions: Created revisions for ' + doneNoteIds.length + ' notes');
|
||||
}
|
||||
|
||||
async deleteOldRevisions(ttl) {
|
||||
return Revision.deleteOldRevisions(ttl);
|
||||
}
|
||||
|
||||
async revisionNote(revisions, index) {
|
||||
if (index < 0 || index >= revisions.length) throw new Error('Invalid revision index: ' + index);
|
||||
|
||||
const rev = revisions[index];
|
||||
const merged = await Revision.mergeDiffs(rev, revisions);
|
||||
|
||||
const output = Object.assign({
|
||||
title: merged.title,
|
||||
body: merged.body,
|
||||
}, merged.metadata);
|
||||
output.updated_time = output.user_updated_time;
|
||||
output.created_time = output.user_created_time;
|
||||
output.type_ = BaseModel.TYPE_NOTE;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
restoreFolderTitle() {
|
||||
return _('Restored Notes');
|
||||
}
|
||||
|
||||
async restoreFolder() {
|
||||
let folder = await Folder.loadByTitle(this.restoreFolderTitle());
|
||||
if (!folder) {
|
||||
folder = await Folder.save({ title: this.restoreFolderTitle() });
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
async importRevisionNote(note) {
|
||||
const toImport = Object.assign({}, note);
|
||||
delete toImport.id;
|
||||
delete toImport.updated_time;
|
||||
delete toImport.created_time;
|
||||
delete toImport.encryption_applied;
|
||||
delete toImport.encryption_cipher_text;
|
||||
|
||||
const folder = await this.restoreFolder();
|
||||
|
||||
toImport.parent_id = folder.id;
|
||||
|
||||
await Note.save(toImport);
|
||||
}
|
||||
|
||||
async maintenance() {
|
||||
const startTime = Date.now();
|
||||
this.logger().info('RevisionService::maintenance: Starting...');
|
||||
|
||||
if (!Setting.value('revisionService.enabled')) {
|
||||
this.logger().info('RevisionService::maintenance: Service is disabled');
|
||||
// We do as if we had processed all the latest changes so that they can be cleaned up
|
||||
// later on by ItemChangeUtils.deleteProcessedChanges().
|
||||
Setting.setValue('revisionService.lastProcessedChangeId', await ItemChange.lastChangeId());
|
||||
await this.deleteOldRevisions(Setting.value('revisionService.ttlDays') * 24 * 60 * 60 * 1000);
|
||||
} else {
|
||||
this.logger().info('RevisionService::maintenance: Service is enabled');
|
||||
await this.collectRevisions();
|
||||
await this.deleteOldRevisions(Setting.value('revisionService.ttlDays') * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
this.logger().info('RevisionService::maintenance: Done in ' + (Date.now() - startTime) + 'ms');
|
||||
}
|
||||
|
||||
runInBackground(collectRevisionInterval = null) {
|
||||
if (this.isRunningInBackground_) return;
|
||||
this.isRunningInBackground_ = true;
|
||||
|
||||
if (collectRevisionInterval === null) collectRevisionInterval = 1000 * 60 * 10;
|
||||
|
||||
this.logger().info('RevisionService::runInBackground: Starting background service with revision collection interval ' + collectRevisionInterval);
|
||||
|
||||
setTimeout(() => {
|
||||
this.maintenance();
|
||||
}, 1000 * 4);
|
||||
|
||||
shim.setInterval(() => {
|
||||
this.maintenance();
|
||||
}, collectRevisionInterval);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = RevisionService;
|
@ -87,9 +87,10 @@ function shimInit() {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
sharp(filePath)
|
||||
.resize(Resource.IMAGE_MAX_DIMENSION, Resource.IMAGE_MAX_DIMENSION)
|
||||
.max()
|
||||
.withoutEnlargement()
|
||||
.resize(Resource.IMAGE_MAX_DIMENSION, Resource.IMAGE_MAX_DIMENSION, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.toFile(targetPath, (err, info) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
|
@ -2,6 +2,7 @@ const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const ItemChange = require('lib/models/ItemChange.js');
|
||||
const ResourceLocalState = require('lib/models/ResourceLocalState.js');
|
||||
const MasterKey = require('lib/models/MasterKey.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
@ -239,7 +240,7 @@ class Synchronizer {
|
||||
// the local sync_time will be updated to Date.now() but on the next loop it will see that the remote item still has a date ahead
|
||||
// and will see a conflict. There's currently no automatic fix for this - the remote item on the sync target must be fixed manually
|
||||
// (by setting an updated_time less than current time).
|
||||
if (donePaths.indexOf(path) >= 0) throw new Error(sprintf("Processing a path that has already been done: %s. sync_time was not updated? Remote item has an updated_time in the future?", path));
|
||||
if (donePaths.indexOf(path) >= 0) throw new JoplinError(sprintf("Processing a path that has already been done: %s. sync_time was not updated? Remote item has an updated_time in the future?", path), 'processingPathTwice');
|
||||
|
||||
let remote = await this.api().stat(path);
|
||||
let action = null;
|
||||
@ -325,7 +326,7 @@ class Synchronizer {
|
||||
if (action == "createRemote" || action == "updateRemote") {
|
||||
let canSync = true;
|
||||
try {
|
||||
if (this.testingHooks_.indexOf("rejectedByTarget") >= 0) throw new JoplinError("Testing rejectedByTarget", "rejectedByTarget");
|
||||
if (this.testingHooks_.indexOf("notesRejectedByTarget") >= 0 && local.type_ === BaseModel.TYPE_NOTE) throw new JoplinError("Testing rejectedByTarget", "rejectedByTarget");
|
||||
const content = await ItemClass.serializeForSync(local);
|
||||
await this.api().put(path, content);
|
||||
} catch (error) {
|
||||
@ -370,9 +371,9 @@ class Synchronizer {
|
||||
local = remoteContent;
|
||||
|
||||
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
|
||||
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries });
|
||||
await ItemClass.save(local, { autoTimestamp: false, changeSource: ItemChange.SOURCE_SYNC, nextQueries: syncTimeQueries });
|
||||
} else {
|
||||
await ItemClass.delete(local.id);
|
||||
await ItemClass.delete(local.id, { changeSource: ItemChange.SOURCE_SYNC });
|
||||
}
|
||||
} else if (action == "noteConflict") {
|
||||
// ------------------------------------------------------------------------------
|
||||
@ -395,7 +396,7 @@ class Synchronizer {
|
||||
let conflictedNote = Object.assign({}, local);
|
||||
delete conflictedNote.id;
|
||||
conflictedNote.is_conflict = 1;
|
||||
await Note.save(conflictedNote, { autoTimestamp: false });
|
||||
await Note.save(conflictedNote, { autoTimestamp: false, changeSource: ItemChange.SOURCE_SYNC });
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
@ -406,12 +407,12 @@ class Synchronizer {
|
||||
if (remote) {
|
||||
local = remoteContent;
|
||||
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
|
||||
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries });
|
||||
await ItemClass.save(local, { autoTimestamp: false, changeSource: ItemChange.SOURCE_SYNC, nextQueries: syncTimeQueries });
|
||||
|
||||
if (!!local.encryption_applied) this.dispatch({ type: "SYNC_GOT_ENCRYPTED_ITEM" });
|
||||
} else {
|
||||
// Remote no longer exists (note deleted) so delete local one too
|
||||
await ItemClass.delete(local.id);
|
||||
await ItemClass.delete(local.id, { changeSource: ItemChange.SOURCE_SYNC });
|
||||
}
|
||||
}
|
||||
|
||||
@ -535,6 +536,8 @@ class Synchronizer {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.testingHooks_.indexOf('skipRevisions') >= 0 && content && content.type_ === BaseModel.TYPE_REVISION) action = null;
|
||||
|
||||
if (!action) continue;
|
||||
|
||||
this.logSyncOperation(action, local, remote, reason);
|
||||
@ -557,30 +560,13 @@ class Synchronizer {
|
||||
let options = {
|
||||
autoTimestamp: false,
|
||||
nextQueries: BaseItem.updateSyncTimeQueries(syncTargetId, content, time.unixMs()),
|
||||
changeSource: ItemChange.SOURCE_SYNC,
|
||||
};
|
||||
if (action == "createLocal") options.isNew = true;
|
||||
if (action == "updateLocal") options.oldItem = local;
|
||||
|
||||
const creatingNewResource = content.type_ == BaseModel.TYPE_RESOURCE && action == "createLocal";
|
||||
|
||||
// if (content.type_ == BaseModel.TYPE_RESOURCE && action == "createLocal") {
|
||||
// let localResourceContentPath = Resource.fullPath(content);
|
||||
// let remoteResourceContentPath = this.resourceDirName_ + "/" + content.id;
|
||||
// try {
|
||||
// await this.api().get(remoteResourceContentPath, { path: localResourceContentPath, target: "file" });
|
||||
// } catch (error) {
|
||||
// if (error.code === 'rejectedByTarget') {
|
||||
// this.progressReport_.errors.push(error);
|
||||
// this.logger().warn('Rejected by target: ' + path + ': ' + error.message);
|
||||
// continue;
|
||||
// } else {
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (creatingNewResource) content.fetch_status = Resource.FETCH_STATUS_IDLE;
|
||||
|
||||
if (creatingNewResource) {
|
||||
await ResourceLocalState.save({ resource_id: content.id, fetch_status: Resource.FETCH_STATUS_IDLE });
|
||||
}
|
||||
@ -608,7 +594,7 @@ class Synchronizer {
|
||||
}
|
||||
|
||||
let ItemClass = BaseItem.itemClass(local.type_);
|
||||
await ItemClass.delete(local.id, { trackDeleted: false });
|
||||
await ItemClass.delete(local.id, { trackDeleted: false, changeSource: ItemChange.SOURCE_SYNC });
|
||||
}
|
||||
}
|
||||
|
||||
@ -653,7 +639,7 @@ class Synchronizer {
|
||||
// CONFLICT
|
||||
await Folder.markNotesAsConflict(item.id);
|
||||
}
|
||||
await Folder.delete(item.id, { deleteChildren: false, trackDeleted: false });
|
||||
await Folder.delete(item.id, { deleteChildren: false, changeSource: ItemChange.SOURCE_SYNC, trackDeleted: false });
|
||||
}
|
||||
}
|
||||
|
||||
@ -662,10 +648,14 @@ class Synchronizer {
|
||||
}
|
||||
} // DELTA STEP
|
||||
} catch (error) {
|
||||
if (error && ["cannotEncryptEncrypted", "noActiveMasterKey"].indexOf(error.code) >= 0) {
|
||||
if (error && ["cannotEncryptEncrypted", "noActiveMasterKey", "processingPathTwice"].indexOf(error.code) >= 0) {
|
||||
// Only log an info statement for this since this is a common condition that is reported
|
||||
// in the application, and needs to be resolved by the user
|
||||
// in the application, and needs to be resolved by the user.
|
||||
// Or it's a temporary issue that will be resolved on next sync.
|
||||
this.logger().info(error.message);
|
||||
} else if (error.code === 'unknownItemType') {
|
||||
this.progressReport_.errors.push(_('Unknown item type downloaded - please upgrade Joplin to the latest version'));
|
||||
this.logger().error(error);
|
||||
} else {
|
||||
this.logger().error(error);
|
||||
|
||||
|
52
ReactNativeClient/package-lock.json
generated
52
ReactNativeClient/package-lock.json
generated
@ -2325,6 +2325,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=="
|
||||
},
|
||||
"dom-walk": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz",
|
||||
@ -2680,7 +2685,8 @@
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@ -2698,11 +2704,13 @@
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -2715,15 +2723,18 @@
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -2826,7 +2837,8 @@
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -2836,6 +2848,7 @@
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -2848,17 +2861,20 @@
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.2.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.0"
|
||||
@ -2875,6 +2891,7 @@
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@ -2947,7 +2964,8 @@
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -2957,6 +2975,7 @@
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -3032,7 +3051,8 @@
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@ -3062,6 +3082,7 @@
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@ -3079,6 +3100,7 @@
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@ -3117,11 +3139,13 @@
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.2",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -4123,7 +4147,7 @@
|
||||
},
|
||||
"load-json-file": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
||||
"integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
@ -5278,7 +5302,7 @@
|
||||
},
|
||||
"load-json-file": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
||||
"integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
@ -7372,7 +7396,7 @@
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz",
|
||||
"integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE="
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"base-64": "^0.1.0",
|
||||
"buffer": "^5.0.8",
|
||||
"diacritics": "^1.3.0",
|
||||
"diff-match-patch": "^1.0.4",
|
||||
"events": "^1.1.1",
|
||||
"form-data": "^2.1.4",
|
||||
"highlight.js": "^9.15.6",
|
||||
|
@ -22,9 +22,11 @@ const Tag = require('lib/models/Tag.js');
|
||||
const NoteTag = require('lib/models/NoteTag.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const MasterKey = require('lib/models/MasterKey.js');
|
||||
const Revision = require('lib/models/Revision.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const BaseService = require('lib/services/BaseService.js');
|
||||
const ResourceService = require('lib/services/ResourceService');
|
||||
const RevisionService = require('lib/services/RevisionService');
|
||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { NotesScreen } = require('lib/components/screens/notes.js');
|
||||
@ -396,6 +398,7 @@ async function initialize(dispatch) {
|
||||
BaseItem.loadClass('Tag', Tag);
|
||||
BaseItem.loadClass('NoteTag', NoteTag);
|
||||
BaseItem.loadClass('MasterKey', MasterKey);
|
||||
BaseItem.loadClass('Revision', Revision);
|
||||
|
||||
const fsDriver = new FsDriverRN();
|
||||
|
||||
@ -430,6 +433,8 @@ async function initialize(dispatch) {
|
||||
reg.logger().info('db.ftsEnabled = ', Setting.value('db.ftsEnabled'));
|
||||
}
|
||||
|
||||
BaseItem.revisionService_ = RevisionService.instance();
|
||||
|
||||
// Note: for now we hard-code the folder sort order as we need to
|
||||
// create a UI to allow customisation (started in branch mobile_add_sidebar_buttons)
|
||||
Setting.setValue('folders.sortOrder.field', 'title');
|
||||
@ -526,6 +531,10 @@ async function initialize(dispatch) {
|
||||
|
||||
await WelcomeUtils.install(dispatch);
|
||||
|
||||
// Collect revisions more frequently on mobile because it doesn't auto-save
|
||||
// and it cannot collect anything when the app is not active.
|
||||
RevisionService.instance().runInBackground(1000 * 30);
|
||||
|
||||
reg.logger().info('Application initialized');
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
const fs = require('fs-extra');
|
||||
const dirname = require('path').dirname;
|
||||
const marked = require('marked');
|
||||
const Mustache = require('mustache');
|
||||
|
||||
const headerHtml = `<!doctype html>
|
||||
@ -185,6 +184,24 @@ const headerHtml = `<!doctype html>
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -305,15 +322,103 @@ const scriptHtml = `
|
||||
const rootDir = dirname(__dirname);
|
||||
|
||||
function markdownToHtml(md) {
|
||||
const renderer = new marked.Renderer();
|
||||
const MarkdownIt = require('markdown-it');
|
||||
|
||||
let output = marked(md, {
|
||||
gfm: true,
|
||||
break: true,
|
||||
renderer: renderer,
|
||||
const markdownIt = new MarkdownIt({
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
html: true,
|
||||
});
|
||||
|
||||
return headerHtml + output + scriptHtml + footerHtml;
|
||||
markdownIt.core.ruler.push('checkbox', state => {
|
||||
const tokens = state.tokens;
|
||||
const Token = state.Token;
|
||||
const doneNames = [];
|
||||
|
||||
const headingTextToAnchorName = (text, doneNames) => {
|
||||
const allowed = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let lastWasDash = true;
|
||||
let output = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (allowed.indexOf(c) < 0) {
|
||||
if (lastWasDash) continue;
|
||||
lastWasDash = true;
|
||||
output += '-';
|
||||
} else {
|
||||
lastWasDash = false;
|
||||
output += c;
|
||||
}
|
||||
}
|
||||
|
||||
output = output.toLowerCase();
|
||||
|
||||
let temp = output;
|
||||
let index = 1;
|
||||
while (doneNames.indexOf(temp) >= 0) {
|
||||
temp = output + '-' + index;
|
||||
index++;
|
||||
}
|
||||
output = temp;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
const createAnchorTokens = anchorName => {
|
||||
const output = [];
|
||||
|
||||
{
|
||||
const token = new Token('heading_anchor_open', 'a', 1);
|
||||
token.attrs = [
|
||||
['name', anchorName],
|
||||
['href', '#' + anchorName],
|
||||
['class', 'heading-anchor'],
|
||||
];
|
||||
output.push(token);
|
||||
}
|
||||
|
||||
{
|
||||
const token = new Token('text', '', 0);
|
||||
token.content = '🔗';
|
||||
output.push(token);
|
||||
}
|
||||
|
||||
{
|
||||
const token = new Token('heading_anchor_close', 'a', -1);
|
||||
output.push(token);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
let insideHeading = false;
|
||||
let processedFirstInline = false;
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type === 'heading_open') {
|
||||
insideHeading = true;
|
||||
processedFirstInline = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.type === 'heading_close') {
|
||||
insideHeading = false;
|
||||
processedFirstInline = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (insideHeading && token.type === 'inline') {
|
||||
processedFirstInline = true;
|
||||
const anchorName = headingTextToAnchorName(token.content, doneNames);
|
||||
doneNames.push(anchorName);
|
||||
const anchorTokens = createAnchorTokens(anchorName);
|
||||
token.children = anchorTokens.concat(token.children);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return headerHtml + markdownIt.render(md) + scriptHtml + footerHtml;
|
||||
}
|
||||
|
||||
let tocMd_ = null;
|
||||
|
@ -1,6 +1,8 @@
|
||||
"use strict"
|
||||
|
||||
// Requires git2json: https://github.com/tarmstrong/git2json
|
||||
// Supported commit formats:
|
||||
|
||||
// (Desktop|Mobile|Android|iOS[CLI): (New|Improved|Fixed): Some message..... (#ISSUE)
|
||||
|
||||
require('app-module-path').addPath(__dirname + '/../ReactNativeClient');
|
||||
|
||||
@ -8,6 +10,7 @@ const rootDir = __dirname + '/..';
|
||||
const { basename, dirname, filename, fileExtension } = require(rootDir + '/ReactNativeClient/lib/path-utils.js');
|
||||
const fs = require('fs-extra');
|
||||
const { execCommand } = require('./tool-utils.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
|
||||
async function gitTags() {
|
||||
const r = await execCommand('git tag --format="%(objectname) %(refname:short)" --sort=-creatordate');
|
||||
@ -26,11 +29,22 @@ async function gitTags() {
|
||||
return output;
|
||||
}
|
||||
|
||||
async function gitLog() {
|
||||
await execCommand('git2json > gitlog.json');
|
||||
const output = await fs.readJson(rootDir + '/gitlog.json');
|
||||
if (!output || !output.length) throw new Error('Could not read git log or could not generate gitlog.json');
|
||||
await fs.remove('gitlog.json');
|
||||
async function gitLog(sinceTag) {
|
||||
let lines = await execCommand('git log --pretty=format:"%H:%s" ' + sinceTag + '..HEAD');
|
||||
lines = lines.split('\n');
|
||||
|
||||
const output = [];
|
||||
for (const line of lines) {
|
||||
const splitted = line.split(':');
|
||||
const commit = splitted[0];
|
||||
const message = line.substr(commit.length + 1).trim();;
|
||||
|
||||
output.push({
|
||||
commit: commit,
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@ -62,19 +76,33 @@ function platformFromTag(tagName) {
|
||||
|
||||
function filterLogs(logs, platform) {
|
||||
const output = [];
|
||||
const revertedLogs = [];
|
||||
|
||||
for (const log of logs) {
|
||||
const msg = log.message.trim().toLowerCase();
|
||||
|
||||
// Save to an array any commit that has been reverted, and exclude
|
||||
// these from the final output array.
|
||||
const revertMatches = log.message.split('\n')[0].trim().match(/^Revert "(.*?)"$/);
|
||||
if (revertMatches && revertMatches.length >= 2) {
|
||||
revertedLogs.push(revertMatches[1]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (revertedLogs.indexOf(log.message) >= 0) continue;
|
||||
|
||||
let prefix = log.message.trim().toLowerCase().split(':');
|
||||
if (prefix.length <= 1) continue;
|
||||
prefix = prefix[0].split(',').map(s => s.trim());
|
||||
|
||||
let addIt = false;
|
||||
|
||||
if (msg.indexOf('all:') === 0 && platform !== 'clipper') addIt = true;
|
||||
if ((platform === 'android' || platform === 'ios') && msg.indexOf('mobile:') === 0) addIt = true;
|
||||
if (platform === 'android' && msg.indexOf('android:') === 0) addIt = true;
|
||||
if (platform === 'ios' && msg.indexOf('ios:') === 0) addIt = true;
|
||||
if (platform === 'desktop' && msg.indexOf('desktop:') === 0) addIt = true;
|
||||
if (platform === 'cli' && msg.indexOf('cli:') === 0) addIt = true;
|
||||
if (platform === 'clipper' && msg.indexOf('clipper:') === 0) addIt = true;
|
||||
if (prefix.indexOf('all') >= 0 && platform !== 'clipper') addIt = true;
|
||||
if ((platform === 'android' || platform === 'ios') && prefix.indexOf('mobile') >= 0) addIt = true;
|
||||
if (platform === 'android' && prefix.indexOf('android') >= 0) addIt = true;
|
||||
if (platform === 'ios' && prefix.indexOf('ios') >= 0) addIt = true;
|
||||
if (platform === 'desktop' && prefix.indexOf('desktop') >= 0) addIt = true;
|
||||
if (platform === 'cli' && prefix.indexOf('cli') >= 0) addIt = true;
|
||||
if (platform === 'clipper' && prefix.indexOf('clipper') >= 0) addIt = true;
|
||||
|
||||
if (addIt) output.push(log);
|
||||
}
|
||||
@ -87,9 +115,17 @@ function formatCommitMessage(msg) {
|
||||
|
||||
const splitted = msg.split(':');
|
||||
|
||||
const isPlatformPrefix = prefix => {
|
||||
prefix = prefix.split(',').map(p => p.trim().toLowerCase());
|
||||
for (const p of prefix) {
|
||||
if (['android', 'mobile', 'ios', 'desktop', 'cli', 'clipper', 'all'].indexOf(p) >= 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (splitted.length) {
|
||||
const platform = splitted[0].trim().toLowerCase();
|
||||
if (['android', 'mobile', 'ios', 'desktop', 'cli', 'clipper', 'all'].indexOf(platform) >= 0) {
|
||||
if (isPlatformPrefix(platform)) {
|
||||
splitted.splice(0, 1);
|
||||
}
|
||||
|
||||
@ -97,9 +133,69 @@ function formatCommitMessage(msg) {
|
||||
}
|
||||
|
||||
output = output.split('\n')[0].trim();
|
||||
output = capitalizeFirstLetter(output);
|
||||
|
||||
output = output.replace(/Fixes #(\d+)(.*)/, "Fix #$1$2");
|
||||
const detectType = msg => {
|
||||
msg = msg.trim().toLowerCase();
|
||||
|
||||
if (msg.indexOf('fix') === 0) return 'fixed';
|
||||
if (msg.indexOf('add') === 0) return 'new';
|
||||
if (msg.indexOf('change') === 0) return 'improved';
|
||||
if (msg.indexOf('update') === 0) return 'improved';
|
||||
if (msg.indexOf('improve') === 0) return 'improved';
|
||||
|
||||
return 'improved';
|
||||
}
|
||||
|
||||
const parseCommitMessage = (msg) => {
|
||||
const parts = msg.split(':');
|
||||
const defaultType = 'improved';
|
||||
|
||||
if (parts.length === 1) {
|
||||
return {
|
||||
type: detectType(msg),
|
||||
message: msg.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
let t = parts[0].trim().toLowerCase();
|
||||
|
||||
parts.splice(0, 1);
|
||||
let message = parts.join(':').trim();
|
||||
|
||||
let type = null;
|
||||
|
||||
// eg. "All: Resolves #712: New: Support for note history (#1415)"
|
||||
// "Resolves" doesn't tell us if it's new or improved so check the
|
||||
// third token (which in this case is "new").
|
||||
if (t.indexOf('resolves') === 0 && ['new', 'improved', 'fixed'].indexOf(parts[0].trim().toLowerCase()) >= 0) {
|
||||
t = parts[0].trim().toLowerCase();
|
||||
parts.splice(0, 1);
|
||||
message = parts.join(':').trim();
|
||||
}
|
||||
|
||||
if (t.indexOf('fix') === 0) type = 'fixed';
|
||||
if (t.indexOf('new') === 0) type = 'new';
|
||||
if (t.indexOf('improved') === 0) type = 'improved';
|
||||
|
||||
if (!type) type = detectType(message);
|
||||
|
||||
let issueNumber = output.match(/#(\d+)/);
|
||||
issueNumber = issueNumber && issueNumber.length >= 2 ? issueNumber[1] : null;
|
||||
|
||||
return {
|
||||
type: type,
|
||||
message: message,
|
||||
issueNumber: issueNumber,
|
||||
};
|
||||
}
|
||||
|
||||
const commitMessage = parseCommitMessage(output);
|
||||
|
||||
output = capitalizeFirstLetter(commitMessage.type) + ': ' + capitalizeFirstLetter(commitMessage.message);
|
||||
if (commitMessage.issueNumber) {
|
||||
const formattedIssueNum = '(#' + commitMessage.issueNumber + ')'
|
||||
if (output.indexOf(formattedIssueNum) < 0) output += ' ' + formattedIssueNum;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
@ -125,34 +221,28 @@ async function main() {
|
||||
const sinceTagName = argv._[0];
|
||||
const platform = platformFromTag(sinceTagName);
|
||||
|
||||
const logs = await gitLog();
|
||||
const tags = await gitTags();
|
||||
const logsSinceTags = await gitLog(sinceTagName);
|
||||
const filteredLogs = filterLogs(logsSinceTags, platform);
|
||||
|
||||
let sinceTagHash = null;
|
||||
for (const tag of tags) {
|
||||
if (tag.name === sinceTagName) {
|
||||
sinceTagHash = tag.hash;
|
||||
break;
|
||||
let changelog = createChangeLog(filteredLogs);
|
||||
|
||||
const changelogFixes = [];
|
||||
const changelogImproves = [];
|
||||
const changelogNews = [];
|
||||
|
||||
for (const l of changelog) {
|
||||
if (l.indexOf('Fix') === 0) {
|
||||
changelogFixes.push(l);
|
||||
} else if (l.indexOf('Improve') === 0) {
|
||||
changelogImproves.push(l);
|
||||
} else if (l.indexOf('New') === 0) {
|
||||
changelogNews.push(l);
|
||||
} else {
|
||||
throw new Error('Invalid changelog line: ' + l);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sinceTagHash) throw new Error('Could not find tag: ' + sinceTagName);
|
||||
|
||||
const logsSinceTags = await gitLogSinceTag(logs, sinceTagHash);
|
||||
const filteredLogs = filterLogs(logsSinceTags, platform);
|
||||
|
||||
const changelog = createChangeLog(filteredLogs);
|
||||
changelog.sort((a, b) => {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
|
||||
const aIsFix = a.indexOf('fix') === 0;
|
||||
const bIsFix = b.indexOf('fix') === 0;
|
||||
|
||||
if (aIsFix && bIsFix) return 0;
|
||||
if (aIsFix) return +1;
|
||||
return -1;
|
||||
});
|
||||
changelog = [].concat(changelogNews).concat(changelogImproves).concat(changelogFixes);
|
||||
|
||||
const changelogString = changelog.map(l => '- ' + l);
|
||||
console.info(changelogString.join('\n'));
|
||||
|
368
Tools/package-lock.json
generated
368
Tools/package-lock.json
generated
@ -113,11 +113,6 @@
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
|
||||
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
@ -153,288 +148,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"d3": {
|
||||
"version": "5.9.1",
|
||||
"resolved": "https://registry.npmjs.org/d3/-/d3-5.9.1.tgz",
|
||||
"integrity": "sha512-JceuBn5VVWySPQc9EA0gfq0xQVgEQXGokHhe+359bmgGeUITLK2r2b9idMzquQne9DKxb7JDCE1gDRXe9OIF2Q==",
|
||||
"requires": {
|
||||
"d3-array": "1",
|
||||
"d3-axis": "1",
|
||||
"d3-brush": "1",
|
||||
"d3-chord": "1",
|
||||
"d3-collection": "1",
|
||||
"d3-color": "1",
|
||||
"d3-contour": "1",
|
||||
"d3-dispatch": "1",
|
||||
"d3-drag": "1",
|
||||
"d3-dsv": "1",
|
||||
"d3-ease": "1",
|
||||
"d3-fetch": "1",
|
||||
"d3-force": "1",
|
||||
"d3-format": "1",
|
||||
"d3-geo": "1",
|
||||
"d3-hierarchy": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-path": "1",
|
||||
"d3-polygon": "1",
|
||||
"d3-quadtree": "1",
|
||||
"d3-random": "1",
|
||||
"d3-scale": "2",
|
||||
"d3-scale-chromatic": "1",
|
||||
"d3-selection": "1",
|
||||
"d3-shape": "1",
|
||||
"d3-time": "1",
|
||||
"d3-time-format": "2",
|
||||
"d3-timer": "1",
|
||||
"d3-transition": "1",
|
||||
"d3-voronoi": "1",
|
||||
"d3-zoom": "1"
|
||||
}
|
||||
},
|
||||
"d3-array": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
|
||||
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
|
||||
},
|
||||
"d3-axis": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
|
||||
"integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ=="
|
||||
},
|
||||
"d3-brush": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.6.tgz",
|
||||
"integrity": "sha512-lGSiF5SoSqO5/mYGD5FAeGKKS62JdA1EV7HPrU2b5rTX4qEJJtpjaGLJngjnkewQy7UnGstnFd3168wpf5z76w==",
|
||||
"requires": {
|
||||
"d3-dispatch": "1",
|
||||
"d3-drag": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-selection": "1",
|
||||
"d3-transition": "1"
|
||||
}
|
||||
},
|
||||
"d3-chord": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz",
|
||||
"integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==",
|
||||
"requires": {
|
||||
"d3-array": "1",
|
||||
"d3-path": "1"
|
||||
}
|
||||
},
|
||||
"d3-collection": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
|
||||
"integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
|
||||
},
|
||||
"d3-color": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.2.3.tgz",
|
||||
"integrity": "sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw=="
|
||||
},
|
||||
"d3-contour": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz",
|
||||
"integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==",
|
||||
"requires": {
|
||||
"d3-array": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"d3-dispatch": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz",
|
||||
"integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g=="
|
||||
},
|
||||
"d3-drag": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.3.tgz",
|
||||
"integrity": "sha512-8S3HWCAg+ilzjJsNtWW1Mutl74Nmzhb9yU6igspilaJzeZVFktmY6oO9xOh5TDk+BM2KrNFjttZNoJJmDnkjkg==",
|
||||
"requires": {
|
||||
"d3-dispatch": "1",
|
||||
"d3-selection": "1"
|
||||
}
|
||||
},
|
||||
"d3-dsv": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz",
|
||||
"integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==",
|
||||
"requires": {
|
||||
"commander": "2",
|
||||
"iconv-lite": "0.4",
|
||||
"rw": "1"
|
||||
}
|
||||
},
|
||||
"d3-ease": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz",
|
||||
"integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ=="
|
||||
},
|
||||
"d3-fetch": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz",
|
||||
"integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==",
|
||||
"requires": {
|
||||
"d3-dsv": "1"
|
||||
}
|
||||
},
|
||||
"d3-force": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.0.tgz",
|
||||
"integrity": "sha512-PFLcDnRVANHMudbQlIB87gcfQorEsDIAvRpZ2bNddfM/WxdsEkyrEaOIPoydhH1I1V4HPjNLGOMLXCA0AuGQ9w==",
|
||||
"requires": {
|
||||
"d3-collection": "1",
|
||||
"d3-dispatch": "1",
|
||||
"d3-quadtree": "1",
|
||||
"d3-timer": "1"
|
||||
}
|
||||
},
|
||||
"d3-format": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz",
|
||||
"integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ=="
|
||||
},
|
||||
"d3-geo": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.3.tgz",
|
||||
"integrity": "sha512-n30yN9qSKREvV2fxcrhmHUdXP9TNH7ZZj3C/qnaoU0cVf/Ea85+yT7HY7i8ySPwkwjCNYtmKqQFTvLFngfkItQ==",
|
||||
"requires": {
|
||||
"d3-array": "1"
|
||||
}
|
||||
},
|
||||
"d3-hierarchy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz",
|
||||
"integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w=="
|
||||
},
|
||||
"d3-interpolate": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz",
|
||||
"integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==",
|
||||
"requires": {
|
||||
"d3-color": "1"
|
||||
}
|
||||
},
|
||||
"d3-path": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.7.tgz",
|
||||
"integrity": "sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA=="
|
||||
},
|
||||
"d3-polygon": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz",
|
||||
"integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w=="
|
||||
},
|
||||
"d3-quadtree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz",
|
||||
"integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA=="
|
||||
},
|
||||
"d3-random": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz",
|
||||
"integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ=="
|
||||
},
|
||||
"d3-scale": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
|
||||
"integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
|
||||
"requires": {
|
||||
"d3-array": "^1.2.0",
|
||||
"d3-collection": "1",
|
||||
"d3-format": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-time": "1",
|
||||
"d3-time-format": "2"
|
||||
}
|
||||
},
|
||||
"d3-scale-chromatic": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz",
|
||||
"integrity": "sha512-BWTipif1CimXcYfT02LKjAyItX5gKiwxuPRgr4xM58JwlLocWbjPLI7aMEjkcoOQXMkYsmNsvv3d2yl/OKuHHw==",
|
||||
"requires": {
|
||||
"d3-color": "1",
|
||||
"d3-interpolate": "1"
|
||||
}
|
||||
},
|
||||
"d3-selection": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz",
|
||||
"integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg=="
|
||||
},
|
||||
"d3-shape": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.4.tgz",
|
||||
"integrity": "sha512-izaz4fOpOnY3CD17hkZWNxbaN70sIGagLR/5jb6RS96Y+6VqX+q1BQf1av6QSBRdfULi3Gb8Js4CzG4+KAPjMg==",
|
||||
"requires": {
|
||||
"d3-path": "1"
|
||||
}
|
||||
},
|
||||
"d3-time": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz",
|
||||
"integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw=="
|
||||
},
|
||||
"d3-time-format": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz",
|
||||
"integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==",
|
||||
"requires": {
|
||||
"d3-time": "1"
|
||||
}
|
||||
},
|
||||
"d3-timer": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz",
|
||||
"integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg=="
|
||||
},
|
||||
"d3-transition": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz",
|
||||
"integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==",
|
||||
"requires": {
|
||||
"d3-color": "1",
|
||||
"d3-dispatch": "1",
|
||||
"d3-ease": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-selection": "^1.1.0",
|
||||
"d3-timer": "1"
|
||||
}
|
||||
},
|
||||
"d3-voronoi": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
|
||||
"integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
|
||||
},
|
||||
"d3-zoom": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.7.3.tgz",
|
||||
"integrity": "sha512-xEBSwFx5Z9T3/VrwDkMt+mr0HCzv7XjpGURJ8lWmIC8wxe32L39eWHIasEe/e7Ox8MPU4p1hvH8PKN2olLzIBg==",
|
||||
"requires": {
|
||||
"d3-dispatch": "1",
|
||||
"d3-drag": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-selection": "1",
|
||||
"d3-transition": "1"
|
||||
}
|
||||
},
|
||||
"dagre-d3-renderer": {
|
||||
"version": "0.5.8",
|
||||
"resolved": "https://registry.npmjs.org/dagre-d3-renderer/-/dagre-d3-renderer-0.5.8.tgz",
|
||||
"integrity": "sha512-XH2a86isUHRxzIYbjQVEuZtJnWEufb64H5DuXIUmn8esuB40jgLEbUUclulWOW62/ZoXlj2ZDyL8SJ+YRxs+jQ==",
|
||||
"requires": {
|
||||
"dagre-layout": "^0.8.8",
|
||||
"lodash": "^4.17.5"
|
||||
}
|
||||
},
|
||||
"dagre-layout": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/dagre-layout/-/dagre-layout-0.8.8.tgz",
|
||||
"integrity": "sha512-ZNV15T9za7X+fV8Z07IZquUKugCxm5owoiPPxfEx6OJRD331nkiIaF3vSt0JEY5FkrY0KfRQxcpQ3SpXB7pLPQ==",
|
||||
"requires": {
|
||||
"graphlibrary": "^2.2.0",
|
||||
"lodash": "^4.17.5"
|
||||
}
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
@ -483,11 +196,6 @@
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
|
||||
"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
|
||||
},
|
||||
"escaper": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/escaper/-/escaper-2.5.3.tgz",
|
||||
"integrity": "sha512-QGb9sFxBVpbzMggrKTX0ry1oiI4CSDAl9vIL702hzl1jGW8VZs7qfqTRX7WDOjoNDoEVGcEtu1ZOQgReSfT2kQ=="
|
||||
},
|
||||
"execa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||
@ -590,14 +298,6 @@
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
|
||||
},
|
||||
"graphlibrary": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/graphlibrary/-/graphlibrary-2.2.0.tgz",
|
||||
"integrity": "sha512-XTcvT55L8u4MBZrM37zXoUxsgxs/7sow7YSygd9CIwfWTVO8RVu7AYXhhCiTuFEf+APKgx6Jk4SuQbYR0vYKmQ==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.5"
|
||||
}
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
@ -623,11 +323,6 @@
|
||||
"sntp": "2.x.x"
|
||||
}
|
||||
},
|
||||
"he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||
},
|
||||
"hoek": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
|
||||
@ -658,11 +353,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
|
||||
},
|
||||
"is-regexp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
|
||||
"integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk="
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
@ -748,11 +438,6 @@
|
||||
"path-exists": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
||||
},
|
||||
"map-age-cleaner": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
|
||||
@ -773,11 +458,6 @@
|
||||
"uc.micro": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.7.tgz",
|
||||
"integrity": "sha512-zBEP4qO1YQp5aXHt8S5wTiOv9i2X74V/LQL0zhUNvVaklt6Ywa6lChxIvS+ibYlCGgADwKwZFhjC3+XfpsvQvQ=="
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
@ -793,21 +473,6 @@
|
||||
"p-is-promise": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"mermaid": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.0.0.tgz",
|
||||
"integrity": "sha512-vUQRykev0A6RtxIVqQT3a9TDxcSbdZbQF5JDyKgidnYuJy8BE8jp6LM+HKDSQuroKm6buu4NlpMO+qhxIP/cTg==",
|
||||
"requires": {
|
||||
"d3": "^5.7.0",
|
||||
"dagre-d3-renderer": "^0.5.8",
|
||||
"dagre-layout": "^0.8.8",
|
||||
"graphlibrary": "^2.2.0",
|
||||
"he": "^1.2.0",
|
||||
"lodash": "^4.17.11",
|
||||
"moment": "^2.23.0",
|
||||
"scope-css": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
||||
@ -826,11 +491,6 @@
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
|
||||
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
|
||||
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
|
||||
},
|
||||
"mustache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.0.tgz",
|
||||
@ -1000,26 +660,11 @@
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
|
||||
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
|
||||
},
|
||||
"rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
||||
},
|
||||
"scope-css": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/scope-css/-/scope-css-1.2.1.tgz",
|
||||
"integrity": "sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q==",
|
||||
"requires": {
|
||||
"escaper": "^2.5.3",
|
||||
"slugify": "^1.3.1",
|
||||
"strip-css-comments": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
|
||||
@ -1048,11 +693,6 @@
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"slugify": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.4.tgz",
|
||||
"integrity": "sha512-KP0ZYk5hJNBS8/eIjGkFDCzGQIoZ1mnfQRYS5WM3273z+fxGWXeN0fkwf2ebEweydv9tioZIHGZKoF21U07/nw=="
|
||||
},
|
||||
"sntp": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz",
|
||||
@ -1108,14 +748,6 @@
|
||||
"ansi-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"strip-css-comments": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-css-comments/-/strip-css-comments-3.0.0.tgz",
|
||||
"integrity": "sha1-elYl7/iisibPiUehElTaluE9rok=",
|
||||
"requires": {
|
||||
"is-regexp": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"strip-eof": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||
|
@ -13,7 +13,6 @@
|
||||
"fs-extra": "^4.0.3",
|
||||
"gettext-parser": "^1.3.0",
|
||||
"markdown-it": "^8.4.1",
|
||||
"marked": "^0.3.7",
|
||||
"mustache": "^2.3.0",
|
||||
"node-fetch": "^1.7.3",
|
||||
"request": "^2.85.0",
|
||||
|
@ -21,3 +21,6 @@ build_script:
|
||||
- yarn dist
|
||||
|
||||
test: off
|
||||
|
||||
# We only want to build tags
|
||||
skip_non_tags: true
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,55 +268,60 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="joplin-api">Joplin API</h1>
|
||||
<p>When the Web Clipper service is enabled, Joplin exposes a <a href="https://en.wikipedia.org/wiki/Representational_state_transfer">REST API</a> which allows third-party applications to access Joplin's data and to create, modify or delete notes, notebooks, resources or tags.</p>
|
||||
<p>In order to use it, you'll first need to find on which port the service is running. To do so, open the Web Clipper Options in Joplin and if the service is running it should tell you on which port. Normally it runs on port <strong>41184</strong>. If you want to find it programmatically, you may follow this kind of algorithm:</p>
|
||||
<pre><code class="lang-javascript">let port = null;
|
||||
<h1><a name="joplin-api" href="#joplin-api" class="heading-anchor">🔗</a>Joplin API</h1>
|
||||
<p>When the Web Clipper service is enabled, Joplin exposes a <a href="https://en.wikipedia.org/wiki/Representational_state_transfer">REST API</a> which allows third-party applications to access Joplin's data and to create, modify or delete notes, notebooks, resources or tags.</p>
|
||||
<p>In order to use it, you'll first need to find on which port the service is running. To do so, open the Web Clipper Options in Joplin and if the service is running it should tell you on which port. Normally it runs on port <strong>41184</strong>. If you want to find it programmatically, you may follow this kind of algorithm:</p>
|
||||
<pre><code class="language-javascript">let port = null;
|
||||
for (let portToTest = 41184; portToTest <= 41194; portToTest++) {
|
||||
const result = pingPort(portToTest); // Call GET /ping
|
||||
if (result == 'JoplinClipperServer') {
|
||||
if (result == 'JoplinClipperServer') {
|
||||
port = portToTest; // Found the port
|
||||
break;
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<h1 id="authorisation">Authorisation</h1>
|
||||
<h1><a name="authorisation" href="#authorisation" class="heading-anchor">🔗</a>Authorisation</h1>
|
||||
<p>To prevent unauthorised applications from accessing the API, the calls must be authentified. To do so, you must provide a token as a query parameter for each API call. You can get this token from the Joplin desktop application, on the Web Clipper Options screen.</p>
|
||||
<p>This would be an example of valid cURL call using a token:</p>
|
||||
<pre><code>curl http://localhost:41184/notes?token=ABCD123ABCD123ABCD123ABCD123ABCD123
|
||||
</code></pre><p>In the documentation below, the token will not be specified every time however you will need to include it.</p>
|
||||
<h1 id="using-the-api">Using the API</h1>
|
||||
</code></pre>
|
||||
<p>In the documentation below, the token will not be specified every time however you will need to include it.</p>
|
||||
<h1><a name="using-the-api" href="#using-the-api" class="heading-anchor">🔗</a>Using the API</h1>
|
||||
<p>All the calls, unless noted otherwise, receives and send <strong>JSON data</strong>. For example to create a new note:</p>
|
||||
<pre><code>curl --data '{ "title": "My note", "body": "Some note in **Markdown**"}' http://localhost:41184/notes
|
||||
</code></pre><p>In the documentation below, the calls may include special parameters such as :id or :note_id. You would replace this with the item ID or note ID.</p>
|
||||
<pre><code>curl --data '{ "title": "My note", "body": "Some note in **Markdown**"}' http://localhost:41184/notes
|
||||
</code></pre>
|
||||
<p>In the documentation below, the calls may include special parameters such as :id or :note_id. You would replace this with the item ID or note ID.</p>
|
||||
<p>For example, for the endpoint <code>DELETE /tags/:id/notes/:note_id</code>, to remove the tag with ID "ABCD1234" from the note with ID "EFGH789", you would run for example:</p>
|
||||
<pre><code>curl -X DELETE http://localhost:41184/tags/ABCD1234/notes/EFGH789
|
||||
</code></pre><p>The four verbs supported by the API are the following ones:</p>
|
||||
</code></pre>
|
||||
<p>The four verbs supported by the API are the following ones:</p>
|
||||
<ul>
|
||||
<li><strong>GET</strong>: To retrieve items (notes, notebooks, etc.).</li>
|
||||
<li><strong>POST</strong>: To create new items. In general most item properties are optional. If you omit any, a default value will be used.</li>
|
||||
<li><strong>PUT</strong>: To update an item. Note in a REST API, traditionally PUT is used to completely replace an item, however in this API it will only replace the properties that are provided. For example if you PUT {"title": "my new title"}, only the "title" property will be changed. The other properties will be left untouched (they won't be cleared nor changed).</li>
|
||||
<li><strong>PUT</strong>: To update an item. Note in a REST API, traditionally PUT is used to completely replace an item, however in this API it will only replace the properties that are provided. For example if you PUT {"title": "my new title"}, only the "title" property will be changed. The other properties will be left untouched (they won't be cleared nor changed).</li>
|
||||
<li><strong>DELETE</strong>: To delete items.</li>
|
||||
</ul>
|
||||
<h1 id="filtering-data">Filtering data</h1>
|
||||
<h1><a name="filtering-data" href="#filtering-data" class="heading-anchor">🔗</a>Filtering data</h1>
|
||||
<p>You can change the fields that will be returned by the API using the <code>fields=</code> query parameter, which takes a list of comma separated fields. For example, to get the longitude and latitude of a note, use this:</p>
|
||||
<pre><code>curl http://localhost:41184/notes/ABCD123?fields=longitude,latitude
|
||||
</code></pre><p>To get the IDs only of all the tags:</p>
|
||||
</code></pre>
|
||||
<p>To get the IDs only of all the tags:</p>
|
||||
<pre><code>curl http://localhost:41184/tags?fields=id
|
||||
</code></pre><h1 id="error-handling">Error handling</h1>
|
||||
</code></pre>
|
||||
<h1><a name="error-handling" href="#error-handling" class="heading-anchor">🔗</a>Error handling</h1>
|
||||
<p>In case of an error, an HTTP status code >= 400 will be returned along with a JSON object that provides more info about the error. The JSON object is in the format <code>{ "error": "description of error" }</code>.</p>
|
||||
<h1 id="about-the-property-types">About the property types</h1>
|
||||
<h1><a name="about-the-property-types" href="#about-the-property-types" class="heading-anchor">🔗</a>About the property types</h1>
|
||||
<ul>
|
||||
<li>Text is UTF-8.</li>
|
||||
<li>All date/time are Unix timestamps in milliseconds.</li>
|
||||
<li>Booleans are integer values 0 or 1.</li>
|
||||
</ul>
|
||||
<h1 id="testing-if-the-service-is-available">Testing if the service is available</h1>
|
||||
<h1><a name="testing-if-the-service-is-available" href="#testing-if-the-service-is-available" class="heading-anchor">🔗</a>Testing if the service is available</h1>
|
||||
<p>Call <strong>GET /ping</strong> to check if the service is available. It should return "JoplinClipperServer" if it works.</p>
|
||||
<h1 id="searching">Searching</h1>
|
||||
<h1><a name="searching" href="#searching" class="heading-anchor">🔗</a>Searching</h1>
|
||||
<p>Call <strong>GET /search?query=YOUR_QUERY</strong> to search for notes. This end-point supports the <code>field</code> parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: <a href="https://joplinapp.org/#searching">https://joplinapp.org/#searching</a></p>
|
||||
<h1 id="notes">Notes</h1>
|
||||
<h2 id="properties">Properties</h2>
|
||||
<h1><a name="notes" href="#notes" class="heading-anchor">🔗</a>Notes</h1>
|
||||
<h2><a name="properties" href="#properties" class="heading-anchor">🔗</a>Properties</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -431,7 +454,7 @@ for (let portToTest = 41184; portToTest <= 41194; portToTest++) {
|
||||
<tr>
|
||||
<td>base_url</td>
|
||||
<td>text</td>
|
||||
<td>If <code>body_html</code> is provided and contains relative URLs, provide the <code>base_url</code> parameter too so that all the URLs can be converted to absolute ones. The base URL is basically where the HTML was fetched from, minus the query (everything after the '?'). For example if the original page was <code>https://stackoverflow.com/search?q=%5Bjava%5D+test</code>, the base URL is <code>https://stackoverflow.com/search</code>.</td>
|
||||
<td>If <code>body_html</code> is provided and contains relative URLs, provide the <code>base_url</code> parameter too so that all the URLs can be converted to absolute ones. The base URL is basically where the HTML was fetched from, minus the query (everything after the '?'). For example if the original page was <code>https://stackoverflow.com/search?q=%5Bjava%5D+test</code>, the base URL is <code>https://stackoverflow.com/search</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>image_data_url</td>
|
||||
@ -445,37 +468,44 @@ for (let portToTest = 41184; portToTest <= 41194; portToTest++) {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="get-notes">GET /notes</h2>
|
||||
<h2><a name="get-notes" href="#get-notes" class="heading-anchor">🔗</a>GET /notes</h2>
|
||||
<p>Gets all notes</p>
|
||||
<h2 id="get-notes-id">GET /notes/:id</h2>
|
||||
<h2><a name="get-notes-id" href="#get-notes-id" class="heading-anchor">🔗</a>GET /notes/:id</h2>
|
||||
<p>Gets note with ID :id</p>
|
||||
<h2 id="get-notes-id-tags">GET /notes/:id/tags</h2>
|
||||
<h2><a name="get-notes-id-tags" href="#get-notes-id-tags" class="heading-anchor">🔗</a>GET /notes/:id/tags</h2>
|
||||
<p>Gets all the tags attached to this note.</p>
|
||||
<h2 id="post-notes">POST /notes</h2>
|
||||
<h2><a name="post-notes" href="#post-notes" class="heading-anchor">🔗</a>POST /notes</h2>
|
||||
<p>Creates a new note</p>
|
||||
<p>You can either specify the note body as Markdown by setting the <code>body</code> parameter, or in HTML by setting the <code>body_html</code>.</p>
|
||||
<p>Examples:</p>
|
||||
<ul>
|
||||
<li><p>Create a note from some Markdown text</p>
|
||||
<pre><code>curl --data '{ "title": "My note", "body": "Some note in **Markdown**"}' http://127.0.0.1:41184/notes
|
||||
</code></pre></li>
|
||||
<li><p>Create a note from some HTML</p>
|
||||
<pre><code>curl --data '{ "title": "My note", "body_html": "Some note in <b>HTML</b>"}' http://127.0.0.1:41184/notes
|
||||
</code></pre></li>
|
||||
<li><p>Create a note and attach an image to it:</p>
|
||||
<pre><code>curl --data '{ "title": "Image test", "body": "Here is Joplin icon:", "image_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII="}' http://127.0.0.1:41184/notes
|
||||
</code></pre></li>
|
||||
<li>
|
||||
<p>Create a note from some Markdown text</p>
|
||||
<pre><code>curl --data '{ "title": "My note", "body": "Some note in **Markdown**"}' http://127.0.0.1:41184/notes
|
||||
</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Create a note from some HTML</p>
|
||||
<pre><code>curl --data '{ "title": "My note", "body_html": "Some note in <b>HTML</b>"}' http://127.0.0.1:41184/notes
|
||||
</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Create a note and attach an image to it:</p>
|
||||
<pre><code>curl --data '{ "title": "Image test", "body": "Here is Joplin icon:", "image_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII="}' http://127.0.0.1:41184/notes
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 id="creating-a-note-with-a-specific-id">Creating a note with a specific ID</h3>
|
||||
<h3><a name="creating-a-note-with-a-specific-id" href="#creating-a-note-with-a-specific-id" class="heading-anchor">🔗</a>Creating a note with a specific ID</h3>
|
||||
<p>When a new note is created, it is automatically assigned a new unique ID so <strong>normally you do not need to set the ID</strong>. However, if for some reason you want to set it, you can supply it as the <code>id</code> property. It needs to be a 32 characters long hexadecimal string. <strong>Make sure it is unique</strong>, for example by generating it using whatever GUID function is available in your programming language.</p>
|
||||
<pre><code> curl --data '{ "id": "00a87474082744c1a8515da6aa5792d2", "title": "My note with custom ID"}' http://127.0.0.1:41184/notes
|
||||
</code></pre><h2 id="put-notes-id">PUT /notes/:id</h2>
|
||||
<pre><code> curl --data '{ "id": "00a87474082744c1a8515da6aa5792d2", "title": "My note with custom ID"}' http://127.0.0.1:41184/notes
|
||||
</code></pre>
|
||||
<h2><a name="put-notes-id" href="#put-notes-id" class="heading-anchor">🔗</a>PUT /notes/:id</h2>
|
||||
<p>Sets the properties of the note with ID :id</p>
|
||||
<h2 id="delete-notes-id">DELETE /notes/:id</h2>
|
||||
<h2><a name="delete-notes-id" href="#delete-notes-id" class="heading-anchor">🔗</a>DELETE /notes/:id</h2>
|
||||
<p>Deletes the note with ID :id</p>
|
||||
<h1 id="folders">Folders</h1>
|
||||
<h1><a name="folders" href="#folders" class="heading-anchor">🔗</a>Folders</h1>
|
||||
<p>This is actually a notebook. Internally notebooks are called "folders".</p>
|
||||
<h2 id="properties">Properties</h2>
|
||||
<h2><a name="properties-1" href="#properties-1" class="heading-anchor">🔗</a>Properties</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -532,21 +562,21 @@ for (let portToTest = 41184; portToTest <= 41194; portToTest++) {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="get-folders">GET /folders</h2>
|
||||
<h2><a name="get-folders" href="#get-folders" class="heading-anchor">🔗</a>GET /folders</h2>
|
||||
<p>Gets all folders</p>
|
||||
<p>The folders are returned as a tree. The sub-notebooks of a notebook, if any, are under the <code>children</code> key.</p>
|
||||
<h2 id="get-folders-id">GET /folders/:id</h2>
|
||||
<h2><a name="get-folders-id" href="#get-folders-id" class="heading-anchor">🔗</a>GET /folders/:id</h2>
|
||||
<p>Gets folder with ID :id</p>
|
||||
<h2 id="get-folders-id-notes">GET /folders/:id/notes</h2>
|
||||
<h2><a name="get-folders-id-notes" href="#get-folders-id-notes" class="heading-anchor">🔗</a>GET /folders/:id/notes</h2>
|
||||
<p>Gets all the notes inside this folder.</p>
|
||||
<h2 id="post-folders">POST /folders</h2>
|
||||
<h2><a name="post-folders" href="#post-folders" class="heading-anchor">🔗</a>POST /folders</h2>
|
||||
<p>Creates a new folder</p>
|
||||
<h2 id="put-folders-id">PUT /folders/:id</h2>
|
||||
<h2><a name="put-folders-id" href="#put-folders-id" class="heading-anchor">🔗</a>PUT /folders/:id</h2>
|
||||
<p>Sets the properties of the folder with ID :id</p>
|
||||
<h2 id="delete-folders-id">DELETE /folders/:id</h2>
|
||||
<h2><a name="delete-folders-id" href="#delete-folders-id" class="heading-anchor">🔗</a>DELETE /folders/:id</h2>
|
||||
<p>Deletes the folder with ID :id</p>
|
||||
<h1 id="resources">Resources</h1>
|
||||
<h2 id="properties">Properties</h2>
|
||||
<h1><a name="resources" href="#resources" class="heading-anchor">🔗</a>Resources</h1>
|
||||
<h2><a name="properties-2" href="#properties-2" class="heading-anchor">🔗</a>Properties</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -618,23 +648,24 @@ for (let portToTest = 41184; portToTest <= 41194; portToTest++) {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="get-resources">GET /resources</h2>
|
||||
<h2><a name="get-resources" href="#get-resources" class="heading-anchor">🔗</a>GET /resources</h2>
|
||||
<p>Gets all resources</p>
|
||||
<h2 id="get-resources-id">GET /resources/:id</h2>
|
||||
<h2><a name="get-resources-id" href="#get-resources-id" class="heading-anchor">🔗</a>GET /resources/:id</h2>
|
||||
<p>Gets resource with ID :id</p>
|
||||
<h2 id="get-resources-id-file">GET /resources/:id/file</h2>
|
||||
<h2><a name="get-resources-id-file" href="#get-resources-id-file" class="heading-anchor">🔗</a>GET /resources/:id/file</h2>
|
||||
<p>Gets the actual file associated with this resource.</p>
|
||||
<h2 id="post-resources">POST /resources</h2>
|
||||
<h2><a name="post-resources" href="#post-resources" class="heading-anchor">🔗</a>POST /resources</h2>
|
||||
<p>Creates a new resource</p>
|
||||
<p>Creating a new resource is special because you also need to upload the file. Unlike other API calls, this one must have the "multipart/form-data" Content-Type. The file data must be passed to the "data" form field, and the other properties to the "props" form field. An example of a valid call with cURL would be:</p>
|
||||
<pre><code>curl -F 'data=@/path/to/file.jpg' -F 'props={"title":"my resource title"}' http://localhost:41184/resources
|
||||
</code></pre><p>The "data" field is required, while the "props" one is not. If not specified, default values will be used.</p>
|
||||
<h2 id="put-resources-id">PUT /resources/:id</h2>
|
||||
<pre><code>curl -F 'data=@/path/to/file.jpg' -F 'props={"title":"my resource title"}' http://localhost:41184/resources
|
||||
</code></pre>
|
||||
<p>The "data" field is required, while the "props" one is not. If not specified, default values will be used.</p>
|
||||
<h2><a name="put-resources-id" href="#put-resources-id" class="heading-anchor">🔗</a>PUT /resources/:id</h2>
|
||||
<p>Sets the properties of the resource with ID :id</p>
|
||||
<h2 id="delete-resources-id">DELETE /resources/:id</h2>
|
||||
<h2><a name="delete-resources-id" href="#delete-resources-id" class="heading-anchor">🔗</a>DELETE /resources/:id</h2>
|
||||
<p>Deletes the resource with ID :id</p>
|
||||
<h1 id="tags">Tags</h1>
|
||||
<h2 id="properties">Properties</h2>
|
||||
<h1><a name="tags" href="#tags" class="heading-anchor">🔗</a>Tags</h1>
|
||||
<h2><a name="properties-3" href="#properties-3" class="heading-anchor">🔗</a>Properties</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -686,21 +717,21 @@ for (let portToTest = 41184; portToTest <= 41194; portToTest++) {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="get-tags">GET /tags</h2>
|
||||
<h2><a name="get-tags" href="#get-tags" class="heading-anchor">🔗</a>GET /tags</h2>
|
||||
<p>Gets all tags</p>
|
||||
<h2 id="get-tags-id">GET /tags/:id</h2>
|
||||
<h2><a name="get-tags-id" href="#get-tags-id" class="heading-anchor">🔗</a>GET /tags/:id</h2>
|
||||
<p>Gets tag with ID :id</p>
|
||||
<h2 id="get-tags-id-notes">GET /tags/:id/notes</h2>
|
||||
<h2><a name="get-tags-id-notes" href="#get-tags-id-notes" class="heading-anchor">🔗</a>GET /tags/:id/notes</h2>
|
||||
<p>Gets all the notes with this tag.</p>
|
||||
<h2 id="post-tags">POST /tags</h2>
|
||||
<h2><a name="post-tags" href="#post-tags" class="heading-anchor">🔗</a>POST /tags</h2>
|
||||
<p>Creates a new tag</p>
|
||||
<h2 id="post-tags-id-notes">POST /tags/:id/notes</h2>
|
||||
<h2><a name="post-tags-id-notes" href="#post-tags-id-notes" class="heading-anchor">🔗</a>POST /tags/:id/notes</h2>
|
||||
<p>Post a note to this endpoint to add the tag to the note. The note data must at least contain an ID property (all other properties will be ignored).</p>
|
||||
<h2 id="put-tags-id">PUT /tags/:id</h2>
|
||||
<h2><a name="put-tags-id" href="#put-tags-id" class="heading-anchor">🔗</a>PUT /tags/:id</h2>
|
||||
<p>Sets the properties of the tag with ID :id</p>
|
||||
<h2 id="delete-tags-id">DELETE /tags/:id</h2>
|
||||
<h2><a name="delete-tags-id" href="#delete-tags-id" class="heading-anchor">🔗</a>DELETE /tags/:id</h2>
|
||||
<p>Deletes the tag with ID :id</p>
|
||||
<h2 id="delete-tags-id-notes-note_id">DELETE /tags/:id/notes/:note_id</h2>
|
||||
<h2><a name="delete-tags-id-notes-note-id" href="#delete-tags-id-notes-note-id" class="heading-anchor">🔗</a>DELETE /tags/:id/notes/:note_id</h2>
|
||||
<p>Remove the tag from the note.</p>
|
||||
|
||||
<script>
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,8 +268,8 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="joplin-changelog">Joplin changelog</h1>
|
||||
<h2 id="-v1-0-143-https-github-com-laurent22-joplin-releases-tag-v1-0-143-2019-04-22t10-51-38z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.143">v1.0.143</a> - 2019-04-22T10:51:38Z</h2>
|
||||
<h1><a name="joplin-changelog" href="#joplin-changelog" class="heading-anchor">🔗</a>Joplin changelog</h1>
|
||||
<h2><a name="v1-0-143-https-github-com-laurent22-joplin-releases-tag-v1-0-143-2019-04-22t10-51-38z" href="#v1-0-143-https-github-com-laurent22-joplin-releases-tag-v1-0-143-2019-04-22t10-51-38z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.143">v1.0.143</a> - 2019-04-22T10:51:38Z</h2>
|
||||
<ul>
|
||||
<li>Improved support for Japanese, Chinese, Korean search queries (also applies to Goto Anything)</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1433">#1433</a>: Some resources could incorrectly be deleted even though they are still present in a note. Also added additional verifications to make sure resources that are still linked to a note are not accidentally deleted.</li>
|
||||
@ -263,10 +281,10 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1427">#1427</a>: Support checkoxes behind bullets</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1417">#1417</a>: Clipper: Sort the folders in the same order as the desktop app</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1425">#1425</a> (probably): Fix display of images when using VSCode as external editor</li>
|
||||
<li>Change shortcuts for 'Print' and 'Goto Anything' (<a href="https://github.com/laurent22/joplin/issues/1420">#1420</a>)</li>
|
||||
<li>Change shortcuts for 'Print' and 'Goto Anything' (<a href="https://github.com/laurent22/joplin/issues/1420">#1420</a>)</li>
|
||||
<li>Add option to use soft breaks for markdown rendering (<a href="https://github.com/laurent22/joplin/issues/1408">#1408</a>)</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-142-https-github-com-laurent22-joplin-releases-tag-v1-0-142-2019-04-02t16-44-51z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.142">v1.0.142</a> - 2019-04-02T16:44:51Z</h2>
|
||||
<h2><a name="v1-0-142-https-github-com-laurent22-joplin-releases-tag-v1-0-142-2019-04-02t16-44-51z" href="#v1-0-142-https-github-com-laurent22-joplin-releases-tag-v1-0-142-2019-04-02t16-44-51z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.142">v1.0.142</a> - 2019-04-02T16:44:51Z</h2>
|
||||
<ul>
|
||||
<li>New: Allow toggling markdown plugins and added several new plugins (<a href="https://github.com/laurent22/joplin/issues/1347">#1347</a>)</li>
|
||||
<li>New: Added Goto Anything dialog (Ctrl+P or Cmd+P)</li>
|
||||
@ -280,20 +298,20 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1325">#1325</a>: Fixed nested checkbox indentation</li>
|
||||
<li>fix sub pixel rendering for desktop (<a href="https://github.com/laurent22/joplin/issues/1378">#1378</a>)</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-140-https-github-com-laurent22-joplin-releases-tag-v1-0-140-2019-03-10t20-59-58z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.140">v1.0.140</a> - 2019-03-10T20:59:58Z</h2>
|
||||
<h2><a name="v1-0-140-https-github-com-laurent22-joplin-releases-tag-v1-0-140-2019-03-10t20-59-58z" href="#v1-0-140-https-github-com-laurent22-joplin-releases-tag-v1-0-140-2019-03-10t20-59-58z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.140">v1.0.140</a> - 2019-03-10T20:59:58Z</h2>
|
||||
<ul>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/1105">#1105</a>: Added support for macro persistence for Katex</li>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/206">#206</a>: Added support for sorting notebooks by title or last modified</li>
|
||||
<li>Fixed: Windows 32-bit version should now work again.</li>
|
||||
<li>Improved: Rewritten Markdown rendering system to make it easier to add new features. Fixed a few minor rendering bugs doing so.</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-139-https-github-com-laurent22-joplin-releases-tag-v1-0-139-2019-03-09t10-06-48z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.139">v1.0.139</a> - 2019-03-09T10:06:48Z</h2>
|
||||
<h2><a name="v1-0-139-https-github-com-laurent22-joplin-releases-tag-v1-0-139-2019-03-09t10-06-48z" href="#v1-0-139-https-github-com-laurent22-joplin-releases-tag-v1-0-139-2019-03-09t10-06-48z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.139">v1.0.139</a> - 2019-03-09T10:06:48Z</h2>
|
||||
<p>This pre-release is mainly for testing the new rendering engine.</p>
|
||||
<h2 id="-v1-0-138-https-github-com-laurent22-joplin-releases-tag-v1-0-138-2019-03-03t17-23-00z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.138">v1.0.138</a> - 2019-03-03T17:23:00Z</h2>
|
||||
<h2><a name="v1-0-138-https-github-com-laurent22-joplin-releases-tag-v1-0-138-2019-03-03t17-23-00z" href="#v1-0-138-https-github-com-laurent22-joplin-releases-tag-v1-0-138-2019-03-03t17-23-00z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.138">v1.0.138</a> - 2019-03-03T17:23:00Z</h2>
|
||||
<p>This is only for testing the Arabic translation.</p>
|
||||
<h2 id="-v1-0-137-https-github-com-laurent22-joplin-releases-tag-v1-0-137-2019-03-03t01-12-51z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.137">v1.0.137</a> - 2019-03-03T01:12:51Z</h2>
|
||||
<h2><a name="v1-0-137-https-github-com-laurent22-joplin-releases-tag-v1-0-137-2019-03-03t01-12-51z" href="#v1-0-137-https-github-com-laurent22-joplin-releases-tag-v1-0-137-2019-03-03t01-12-51z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.137">v1.0.137</a> - 2019-03-03T01:12:51Z</h2>
|
||||
<p>To test Windows 32-bit build.</p>
|
||||
<h2 id="-v1-0-135-https-github-com-laurent22-joplin-releases-tag-v1-0-135-2019-02-27t23-36-57z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.135">v1.0.135</a> - 2019-02-27T23:36:57Z</h2>
|
||||
<h2><a name="v1-0-135-https-github-com-laurent22-joplin-releases-tag-v1-0-135-2019-02-27t23-36-57z" href="#v1-0-135-https-github-com-laurent22-joplin-releases-tag-v1-0-135-2019-02-27t23-36-57z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.135">v1.0.135</a> - 2019-02-27T23:36:57Z</h2>
|
||||
<p>Note: this is the same as v132 but with a fix for the resizeable column bug, and for PDF export and printing.</p>
|
||||
<ul>
|
||||
<li>New: Experimental support for Mermaid graphs (This is <strong>not</strong> yet supported on mobile).</li>
|
||||
@ -308,7 +326,7 @@
|
||||
<li>Various bug fixes and improvement following previous release.</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1251">#1251</a>: Handle Show Uncompleted Tasks option when selecting a tag</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-134-https-github-com-laurent22-joplin-releases-tag-v1-0-134-2019-02-27t10-21-44z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.134">v1.0.134</a> - 2019-02-27T10:21:44Z</h2>
|
||||
<h2><a name="v1-0-134-https-github-com-laurent22-joplin-releases-tag-v1-0-134-2019-02-27t10-21-44z" href="#v1-0-134-https-github-com-laurent22-joplin-releases-tag-v1-0-134-2019-02-27t10-21-44z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.134">v1.0.134</a> - 2019-02-27T10:21:44Z</h2>
|
||||
<p>Note: this is the same as v132 but with a fix for the resizeable column bug.</p>
|
||||
<ul>
|
||||
<li>New: Experimental support for Mermaid graphs (This is <strong>not</strong> yet supported on mobile).</li>
|
||||
@ -323,7 +341,7 @@
|
||||
<li>Various bug fixes and improvement following previous release.</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1251">#1251</a>: Handle Show Uncompleted Tasks option when selecting a tag</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-132-https-github-com-laurent22-joplin-releases-tag-v1-0-132-2019-02-26t23-02-05z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.132">v1.0.132</a> - 2019-02-26T23:02:05Z</h2>
|
||||
<h2><a name="v1-0-132-https-github-com-laurent22-joplin-releases-tag-v1-0-132-2019-02-26t23-02-05z" href="#v1-0-132-https-github-com-laurent22-joplin-releases-tag-v1-0-132-2019-02-26t23-02-05z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.132">v1.0.132</a> - 2019-02-26T23:02:05Z</h2>
|
||||
<ul>
|
||||
<li>New: Experimental support for Mermaid graphs (This is <strong>not</strong> yet supported on mobile).</li>
|
||||
<li>New: Allow resizing sidebar columns.</li>
|
||||
@ -337,7 +355,7 @@
|
||||
<li>Various bug fixes and improvement following previous release.</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1251">#1251</a>: Handle Show Uncompleted Tasks option when selecting a tag</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-127-https-github-com-laurent22-joplin-releases-tag-v1-0-127-2019-02-14t23-12-48z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.127">v1.0.127</a> - 2019-02-14T23:12:48Z</h2>
|
||||
<h2><a name="v1-0-127-https-github-com-laurent22-joplin-releases-tag-v1-0-127-2019-02-14t23-12-48z" href="#v1-0-127-https-github-com-laurent22-joplin-releases-tag-v1-0-127-2019-02-14t23-12-48z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.127">v1.0.127</a> - 2019-02-14T23:12:48Z</h2>
|
||||
<p>This big release aims at improving the overall usability of the application and to make it more accessible to newcomers.</p>
|
||||
<ul>
|
||||
<li>New: Added Welcome notes the first time the app is launched to give an overview of Joplin and its features.</li>
|
||||
@ -351,7 +369,7 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1161">#1161</a>: Display highlighted text and other background colours and images when exporting to PDF or printing</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1200">#1200</a>: Note list was hidden when minimizing and maximizing window</li>
|
||||
<li>Fixed: Do not display tags that are not associated with any note</li>
|
||||
<li>Improved: Added 'Insert date time' option to menu</li>
|
||||
<li>Improved: Added 'Insert date time' option to menu</li>
|
||||
<li>Improved: Added a few more shortcuts for macOS and other platforms</li>
|
||||
<li>Improved: Added Usage link next to search box</li>
|
||||
<li>Improved: Allow using macOS App bundle as external editor, and improved error handling</li>
|
||||
@ -375,7 +393,7 @@
|
||||
<li>Updated translations and added Turkish language (thanks Zorbey Doğangüneş)</li>
|
||||
<li>API: Allow specifying item ID for any item</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-126-https-github-com-laurent22-joplin-releases-tag-v1-0-126-2019-02-09t19-46-16z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.126">v1.0.126</a> - 2019-02-09T19:46:16Z</h2>
|
||||
<h2><a name="v1-0-126-https-github-com-laurent22-joplin-releases-tag-v1-0-126-2019-02-09t19-46-16z" href="#v1-0-126-https-github-com-laurent22-joplin-releases-tag-v1-0-126-2019-02-09t19-46-16z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.126">v1.0.126</a> - 2019-02-09T19:46:16Z</h2>
|
||||
<ul>
|
||||
<li>New: Added Welcome notes the first time the app is launched to give an overview of Joplin and its features.</li>
|
||||
<li>New: Allow selecting editor path with dialog window</li>
|
||||
@ -386,7 +404,7 @@
|
||||
<li>Fixed importing ENEX file when note incorrectly contains a reminder tag</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1142">#1142</a>: Disallow dropping notes on sidebar Notebook header</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1161">#1161</a>: Display highlighted text and other background colours and images when exporting to PDF or printing</li>
|
||||
<li>Improved: Added 'Insert date time' option to menu</li>
|
||||
<li>Improved: Added 'Insert date time' option to menu</li>
|
||||
<li>Improved: Added a few more shortcuts for macOS and other platforms</li>
|
||||
<li>Improved: Added Usage link next to search box</li>
|
||||
<li>Improved: Allow using macOS App bundle as external editor, and improved error handling</li>
|
||||
@ -406,7 +424,7 @@
|
||||
<li>Improved: When deleting note, display title or number of notes</li>
|
||||
<li>API: Allow specifying item ID for any item</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-125-https-github-com-laurent22-joplin-releases-tag-v1-0-125-2019-01-26t18-14-33z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.125">v1.0.125</a> - 2019-01-26T18:14:33Z</h2>
|
||||
<h2><a name="v1-0-125-https-github-com-laurent22-joplin-releases-tag-v1-0-125-2019-01-26t18-14-33z" href="#v1-0-125-https-github-com-laurent22-joplin-releases-tag-v1-0-125-2019-01-26t18-14-33z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.125">v1.0.125</a> - 2019-01-26T18:14:33Z</h2>
|
||||
<ul>
|
||||
<li>New: Added support for pre-releases - in the options you can now choose to receive pre-releases too.</li>
|
||||
<li>New: Added version info to auto-update dialog</li>
|
||||
@ -419,7 +437,7 @@
|
||||
<li>Improved: Handle ESC key press to cancel the NotePropertiesDialog (<a href="https://github.com/laurent22/joplin/issues/1125">#1125</a>)</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1137">#1137</a>: Fixed regression on SeaFile sync</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-120-https-github-com-laurent22-joplin-releases-tag-v1-0-120-2019-01-10t21-42-53z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.120">v1.0.120</a> - 2019-01-10T21:42:53Z</h2>
|
||||
<h2><a name="v1-0-120-https-github-com-laurent22-joplin-releases-tag-v1-0-120-2019-01-10t21-42-53z" href="#v1-0-120-https-github-com-laurent22-joplin-releases-tag-v1-0-120-2019-01-10t21-42-53z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.120">v1.0.120</a> - 2019-01-10T21:42:53Z</h2>
|
||||
<ul>
|
||||
<li>New: Adds functionality to toggle the notebooks and tags on the sidebar. (<a href="https://github.com/laurent22/joplin/issues/1002">#1002</a>)</li>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/1059">#1059</a>: Fixed behaviour of export to PDF and print</li>
|
||||
@ -430,7 +448,7 @@
|
||||
<li>Apply zoom and editorfont updates without needing to restart (<a href="https://github.com/laurent22/joplin/issues/1109">#1109</a>)</li>
|
||||
<li>Updated many translations</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-119-https-github-com-laurent22-joplin-releases-tag-v1-0-119-2018-12-18t12-40-22z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.119">v1.0.119</a> - 2018-12-18T12:40:22Z</h2>
|
||||
<h2><a name="v1-0-119-https-github-com-laurent22-joplin-releases-tag-v1-0-119-2018-12-18t12-40-22z" href="#v1-0-119-https-github-com-laurent22-joplin-releases-tag-v1-0-119-2018-12-18t12-40-22z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.119">v1.0.119</a> - 2018-12-18T12:40:22Z</h2>
|
||||
<p>Important: This release might be slow on startup due to the need to index all the notes, especially if you have many of them with lots of content. The best is simply to wait for it even if it takes several minutes. This is just a one off and afterwards startup time will be the same as before.</p>
|
||||
<ul>
|
||||
<li>New: Fast full text search engine. Works with multiple terms, support for prefixes, can restrict search to either note title or body. See <a href="https://joplin.cozic.net/#searching">https://joplin.cozic.net/#searching</a> for more info.</li>
|
||||
@ -443,7 +461,7 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1039">#1039</a>: Always print or export to PDF using light theme</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1033">#1033</a>: Handle hard break when rendering Markdown to HTML</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-118-https-github-com-laurent22-joplin-releases-tag-v1-0-118-2019-01-11t08-34-13z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.118">v1.0.118</a> - 2019-01-11T08:34:13Z</h2>
|
||||
<h2><a name="v1-0-118-https-github-com-laurent22-joplin-releases-tag-v1-0-118-2019-01-11t08-34-13z" href="#v1-0-118-https-github-com-laurent22-joplin-releases-tag-v1-0-118-2019-01-11t08-34-13z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.118">v1.0.118</a> - 2019-01-11T08:34:13Z</h2>
|
||||
<p>Important: This release might be slow on startup due to the need to index all the notes, especially if you have many of them with lots of content. The best is simply to wait for it even if it takes several minutes. This is just a one off and afterwards startup time will be the same as before.</p>
|
||||
<ul>
|
||||
<li>New: Fast full text search engine. Works with multiple terms, support for prefixes, can restrict search to either note title or body. See <a href="https://joplin.cozic.net/#searching">https://joplin.cozic.net/#searching</a> for more info.</li>
|
||||
@ -456,7 +474,7 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1039">#1039</a>: Always print or export to PDF using light theme</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/1033">#1033</a>: Handle hard break when rendering Markdown to HTML</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-117-https-github-com-laurent22-joplin-releases-tag-v1-0-117-2018-11-24t12-05-24z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.117">v1.0.117</a> - 2018-11-24T12:05:24Z</h2>
|
||||
<h2><a name="v1-0-117-https-github-com-laurent22-joplin-releases-tag-v1-0-117-2018-11-24t12-05-24z" href="#v1-0-117-https-github-com-laurent22-joplin-releases-tag-v1-0-117-2018-11-24t12-05-24z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.117">v1.0.117</a> - 2018-11-24T12:05:24Z</h2>
|
||||
<ul>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/996">#996</a>: Allow editing multiple notes in external editor</li>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/846">#846</a>: Set resource path to correct relative path so that for example images show up in Markdown viewers.</li>
|
||||
@ -464,7 +482,7 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/968">#968</a>: Export resources specified with a title</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/995">#995</a>: Disabled tag bar for now until performance issues are resolved.</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-116-https-github-com-laurent22-joplin-releases-tag-v1-0-116-2018-11-20t19-09-24z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.116">v1.0.116</a> - 2018-11-20T19:09:24Z</h2>
|
||||
<h2><a name="v1-0-116-https-github-com-laurent22-joplin-releases-tag-v1-0-116-2018-11-20t19-09-24z" href="#v1-0-116-https-github-com-laurent22-joplin-releases-tag-v1-0-116-2018-11-20t19-09-24z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.116">v1.0.116</a> - 2018-11-20T19:09:24Z</h2>
|
||||
<p>This is mostly a bug fix release following the recent v115 release.</p>
|
||||
<ul>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/933">#933</a>: Handle internal links from HTML and from MD.</li>
|
||||
@ -473,7 +491,7 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/985">#985</a>: Add missing syntax highlighting for dark theme</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/991">#991</a>: Add dark theme to note properties dialog</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-115-https-github-com-laurent22-joplin-releases-tag-v1-0-115-2018-11-16t16-52-02z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.115">v1.0.115</a> - 2018-11-16T16:52:02Z</h2>
|
||||
<h2><a name="v1-0-115-https-github-com-laurent22-joplin-releases-tag-v1-0-115-2018-11-16t16-52-02z" href="#v1-0-115-https-github-com-laurent22-joplin-releases-tag-v1-0-115-2018-11-16t16-52-02z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.115">v1.0.115</a> - 2018-11-16T16:52:02Z</h2>
|
||||
<p>This is a rather large release which includes many of the pull requests that were submitted during Hacktoberfest, plus some extra improvements and bug fixes. Many thanks to all the contributors!</p>
|
||||
<ul>
|
||||
<li>New: Adds functionality to display tags under the open note. (<a href="https://github.com/laurent22/joplin/issues/893">#893</a>)</li>
|
||||
@ -490,9 +508,9 @@
|
||||
<li>API: Allow setting the ID of newly created notes.</li>
|
||||
<li>Renewed code signing certificate.</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-114-https-github-com-laurent22-joplin-releases-tag-v1-0-114-2018-10-24t20-14-10z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.114">v1.0.114</a> - 2018-10-24T20:14:10Z</h2>
|
||||
<h2><a name="v1-0-114-https-github-com-laurent22-joplin-releases-tag-v1-0-114-2018-10-24t20-14-10z" href="#v1-0-114-https-github-com-laurent22-joplin-releases-tag-v1-0-114-2018-10-24t20-14-10z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.114">v1.0.114</a> - 2018-10-24T20:14:10Z</h2>
|
||||
<ul>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/832">#832</a>: Enex import: Don't add extra line breaks at the beginning of list item when it contains a block element</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/832">#832</a>: Enex import: Don't add extra line breaks at the beginning of list item when it contains a block element</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/798">#798</a>: Enable Select All shortcut in macOS</li>
|
||||
<li>API: Fixed handling of PUT method and log errors to file</li>
|
||||
<li>Api: Fixes <a href="https://github.com/laurent22/joplin/issues/843">#843</a>: Fixed regression that was preventing resource metadata from being downloaded</li>
|
||||
@ -502,20 +520,20 @@
|
||||
<li>Prevent URLs added via A tag from being opened inside app</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/853">#853</a>: Replace characters to equivalent US-ASCII ones when exporting files</li>
|
||||
<li>Improved the way resources are loaded to prepare to allow making downloading resources optional, and to make sync faster</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/312">#312</a> (maybe): Removed power saving feature, which wasn\'t doing anything and added a possible fix to the UI freezing issue on Linux</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/312">#312</a> (maybe): Removed power saving feature, which wasn't doing anything and added a possible fix to the UI freezing issue on Linux</li>
|
||||
<li>Improved: Handle internal anchors</li>
|
||||
<li>Improved Linux install script</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-111-https-github-com-laurent22-joplin-releases-tag-v1-0-111-2018-09-30t20-15-09z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.111">v1.0.111</a> - 2018-09-30T20:15:09Z</h2>
|
||||
<h2><a name="v1-0-111-https-github-com-laurent22-joplin-releases-tag-v1-0-111-2018-09-30t20-15-09z" href="#v1-0-111-https-github-com-laurent22-joplin-releases-tag-v1-0-111-2018-09-30t20-15-09z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.111">v1.0.111</a> - 2018-09-30T20:15:09Z</h2>
|
||||
<p>This is mainly a release to fix a bug related to the new IMG tag support.</p>
|
||||
<ul>
|
||||
<li>Electron: Resolves <a href="https://github.com/laurent22/joplin/issues/820">#820</a>: Allow dragging and dropping a note in another note to create a link</li>
|
||||
<li>Electron: Fixes resources being incorrectly auto-deleted when inside an IMG tag</li>
|
||||
<li>API: Allow downloading a resource data via <code>/resources/:id/file</code></li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-110-https-github-com-laurent22-joplin-releases-tag-v1-0-110-2018-09-29t12-29-21z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.110">v1.0.110</a> - 2018-09-29T12:29:21Z</h2>
|
||||
<p>This is a release only to get the new API out. If you do not need the functionalities of this API or you don't know what it is, you can probably skip this version.</p>
|
||||
<h2 id="-v1-0-109-https-github-com-laurent22-joplin-releases-tag-v1-0-109-2018-09-27t18-01-41z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.109">v1.0.109</a> - 2018-09-27T18:01:41Z</h2>
|
||||
<h2><a name="v1-0-110-https-github-com-laurent22-joplin-releases-tag-v1-0-110-2018-09-29t12-29-21z" href="#v1-0-110-https-github-com-laurent22-joplin-releases-tag-v1-0-110-2018-09-29t12-29-21z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.110">v1.0.110</a> - 2018-09-29T12:29:21Z</h2>
|
||||
<p>This is a release only to get the new API out. If you do not need the functionalities of this API or you don't know what it is, you can probably skip this version.</p>
|
||||
<h2><a name="v1-0-109-https-github-com-laurent22-joplin-releases-tag-v1-0-109-2018-09-27t18-01-41z" href="#v1-0-109-https-github-com-laurent22-joplin-releases-tag-v1-0-109-2018-09-27t18-01-41z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.109">v1.0.109</a> - 2018-09-27T18:01:41Z</h2>
|
||||
<ul>
|
||||
<li>New: Allow loading image resources in IMG html tags. For example, this is now possible: <code><img src=":/a92ac34387ff467a8c839d201dcd39aa" width="50"/></code></li>
|
||||
<li>Security: Fixed security issue by enabling contextIsolation and proxying IPC messages via preload script. Thank you Yaroslav Lobachevski for discovering the issue.</li>
|
||||
@ -529,21 +547,21 @@
|
||||
<li>Clipper: Fixed importing certain images with sources that contain brackets</li>
|
||||
<li>Improved: Mostly an invisible change at this point, but the REST API has been refactored to allow adding more calls and to support third-party applications.</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-108-https-github-com-laurent22-joplin-releases-tag-v1-0-108-2018-09-29t18-49-29z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.108">v1.0.108</a> - 2018-09-29T18:49:29Z</h2>
|
||||
<p>To test the latest security fix only. Won't be released officially.</p>
|
||||
<h2 id="-v1-0-107-https-github-com-laurent22-joplin-releases-tag-v1-0-107-2018-09-16t19-51-07z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.107">v1.0.107</a> - 2018-09-16T19:51:07Z</h2>
|
||||
<h2><a name="v1-0-108-https-github-com-laurent22-joplin-releases-tag-v1-0-108-2018-09-29t18-49-29z" href="#v1-0-108-https-github-com-laurent22-joplin-releases-tag-v1-0-108-2018-09-29t18-49-29z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.108">v1.0.108</a> - 2018-09-29T18:49:29Z</h2>
|
||||
<p>To test the latest security fix only. Won't be released officially.</p>
|
||||
<h2><a name="v1-0-107-https-github-com-laurent22-joplin-releases-tag-v1-0-107-2018-09-16t19-51-07z" href="#v1-0-107-https-github-com-laurent22-joplin-releases-tag-v1-0-107-2018-09-16t19-51-07z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.107">v1.0.107</a> - 2018-09-16T19:51:07Z</h2>
|
||||
<ul>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/755">#755</a>: Added note properties dialog box to view and edit created time, updated time, source URL and geolocation</li>
|
||||
<li>Added Dutch (Netherlands) translation</li>
|
||||
<li>Added Romanian translation</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/718">#718</a>: Allow recursively importing Markdown folder</li>
|
||||
<li>Fix <a href="https://github.com/laurent22/joplin/issues/764">#764</a>: Fix equation tag positioning</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/710">#710</a>: Don't unwatch file when it is temporarily deleted</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/710">#710</a>: Don't unwatch file when it is temporarily deleted</li>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/781">#781</a>: Allow creating notebooks with duplicate titles to allow two notebooks with same name to exist under different parents</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/799">#799</a>: Handle restricted_content error for Dropbox (skip files that cannot be uploaded to copyright or other Dropbox t&c violation)</li>
|
||||
<li>Provided script to install on Ubuntu (with icon)</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-106-https-github-com-laurent22-joplin-releases-tag-v1-0-106-2018-09-08t15-23-40z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.106">v1.0.106</a> - 2018-09-08T15:23:40Z</h2>
|
||||
<h2><a name="v1-0-106-https-github-com-laurent22-joplin-releases-tag-v1-0-106-2018-09-08t15-23-40z" href="#v1-0-106-https-github-com-laurent22-joplin-releases-tag-v1-0-106-2018-09-08t15-23-40z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.106">v1.0.106</a> - 2018-09-08T15:23:40Z</h2>
|
||||
<p>Note: this release is no longer signed to avoid issues with renewing certificates. If you get a warning or the application cannot be installed, please report on the forum on GitHub.</p>
|
||||
<ul>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/761">#761</a>: Highlight single tick code segments</li>
|
||||
@ -552,7 +570,7 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/697">#697</a>: Focus search text input after clearing search</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/709">#709</a>: Now that HTML is supported in notes, remove BR tag replacement hack to fix newline issues.</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-105-https-github-com-laurent22-joplin-releases-tag-v1-0-105-2018-09-05t11-29-36z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.105">v1.0.105</a> - 2018-09-05T11:29:36Z</h2>
|
||||
<h2><a name="v1-0-105-https-github-com-laurent22-joplin-releases-tag-v1-0-105-2018-09-05t11-29-36z" href="#v1-0-105-https-github-com-laurent22-joplin-releases-tag-v1-0-105-2018-09-05t11-29-36z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.105">v1.0.105</a> - 2018-09-05T11:29:36Z</h2>
|
||||
<ul>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/679">#679</a>: Drag a note on a tag to associate the tag.</li>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/427">#427</a>: Import source-url from Enex files</li>
|
||||
@ -560,24 +578,24 @@
|
||||
<li>New: replace the resource icon (for internal links) with the Joplin icon (from ForkAwesome)</li>
|
||||
<li>Update: Upgraded Katex to support new features</li>
|
||||
<li>Update: Improve speed of loading notes that include many resources, and prevent UI from freezing</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/653">#653</a>: Don't detect horizontal rule as bullet list item</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/653">#653</a>: Don't detect horizontal rule as bullet list item</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/113">#113</a>: Upgraded Ace Editor to try to fix Korean input issue (to be confirmed)</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-104-https-github-com-laurent22-joplin-releases-tag-v1-0-104-2018-06-28t20-25-36z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.104">v1.0.104</a> - 2018-06-28T20:25:36Z</h2>
|
||||
<h2><a name="v1-0-104-https-github-com-laurent22-joplin-releases-tag-v1-0-104-2018-06-28t20-25-36z" href="#v1-0-104-https-github-com-laurent22-joplin-releases-tag-v1-0-104-2018-06-28t20-25-36z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.104">v1.0.104</a> - 2018-06-28T20:25:36Z</h2>
|
||||
<ul>
|
||||
<li>New: Allow HTML in Markdown documents in a secure way.</li>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/619">#619</a>: Context menu to cut, copy and paste. Also added menu to copy link in web view</li>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/612">#612</a>: Allow duplicating a note</li>
|
||||
<li>New: Web Clipper: Support 'author' property</li>
|
||||
<li>New: Web Clipper: Support 'author' property</li>
|
||||
<li>Improved: Resolves <a href="https://github.com/laurent22/joplin/issues/647">#647</a>: Allow specifying text editor path and arguments in setting</li>
|
||||
<li>Improved: Optimised encryption and decryption of items so that it doesn't freeze the UI, especially on mobile</li>
|
||||
<li>Improved: Optimised encryption and decryption of items so that it doesn't freeze the UI, especially on mobile</li>
|
||||
<li>Improved: Set PDF default file name</li>
|
||||
<li>Improved: Resolves <a href="https://github.com/laurent22/joplin/issues/644">#644</a>: Added support for .markdown extension when importing files</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/634">#634</a>: Press ESC to dismiss dialog in non-English languages</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/639">#639</a>: Make sure text wraps when printing or exporting as PDF</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/646">#646</a>: Mentioned that TLS settings must be saved before checking sync config</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-103-https-github-com-laurent22-joplin-releases-tag-v1-0-103-2018-06-21t19-38-13z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.103">v1.0.103</a> - 2018-06-21T19:38:13Z</h2>
|
||||
<h2><a name="v1-0-103-https-github-com-laurent22-joplin-releases-tag-v1-0-103-2018-06-21t19-38-13z" href="#v1-0-103-https-github-com-laurent22-joplin-releases-tag-v1-0-103-2018-06-21t19-38-13z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.103">v1.0.103</a> - 2018-06-21T19:38:13Z</h2>
|
||||
<ul>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/611">#611</a>: Allow opening and editing note in external editor</li>
|
||||
<li>New: <a href="https://github.com/laurent22/joplin/issues/628">#628</a>: Adds a shortcut to insert the date and time.</li>
|
||||
@ -587,53 +605,53 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/632">#632</a>: Handle restricted_content error in Dropbox</li>
|
||||
<li>Fix: Revert <a href="https://github.com/laurent22/joplin/issues/554">#554</a> to try to fix <a href="https://github.com/laurent22/joplin/issues/624">#624</a>: WebDAV error when syncing with SeaFile</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-101-https-github-com-laurent22-joplin-releases-tag-v1-0-101-2018-06-17t18-35-11z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.101">v1.0.101</a> - 2018-06-17T18:35:11Z</h2>
|
||||
<h2><a name="v1-0-101-https-github-com-laurent22-joplin-releases-tag-v1-0-101-2018-06-17t18-35-11z" href="#v1-0-101-https-github-com-laurent22-joplin-releases-tag-v1-0-101-2018-06-17t18-35-11z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.101">v1.0.101</a> - 2018-06-17T18:35:11Z</h2>
|
||||
<p>This is a bug-fix release following v100 with the following fixes:</p>
|
||||
<ul>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/623">#623</a>: Improved handling of text selection and fixed infinite loop (white screen bug)</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/593">#593</a>: Resource should not be auto-deleted if they've never been linked to any note</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/593">#593</a>: Resource should not be auto-deleted if they've never been linked to any note</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/630">#630</a>: PDF export in context menu</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-100-https-github-com-laurent22-joplin-releases-tag-v1-0-100-2018-06-14t17-41-43z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.100">v1.0.100</a> - 2018-06-14T17:41:43Z</h2>
|
||||
<h2><a name="v1-0-100-https-github-com-laurent22-joplin-releases-tag-v1-0-100-2018-06-14t17-41-43z" href="#v1-0-100-https-github-com-laurent22-joplin-releases-tag-v1-0-100-2018-06-14t17-41-43z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.100">v1.0.100</a> - 2018-06-14T17:41:43Z</h2>
|
||||
<ul>
|
||||
<li>New: Added toolbar buttons for formatting text.</li>
|
||||
<li>New: Added Traditional Chinese and Catalan translations</li>
|
||||
<li>Fixed: Handle Nginx DAV PROPFIND responses correctly</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/597">#597</a>: Also import sub-notebooks when importing JEX data</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/600">#600</a>: Improved resuming of long sync operations so that it doesn't needlessly re-download the items from the beginning</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/600">#600</a>: Improved resuming of long sync operations so that it doesn't needlessly re-download the items from the beginning</li>
|
||||
<li>Fix: Try to display more info when there is a Dropbox API error</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-99-https-github-com-laurent22-joplin-releases-tag-v1-0-99-2018-06-10t13-18-23z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.99">v1.0.99</a> - 2018-06-10T13:18:23Z</h2>
|
||||
<h2><a name="v1-0-99-https-github-com-laurent22-joplin-releases-tag-v1-0-99-2018-06-10t13-18-23z" href="#v1-0-99-https-github-com-laurent22-joplin-releases-tag-v1-0-99-2018-06-10t13-18-23z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.99">v1.0.99</a> - 2018-06-10T13:18:23Z</h2>
|
||||
<p>Note: This is the same as 1.0.97, but with a fix for the Linux version, which could not start anymore.</p>
|
||||
<p>If you're using the web clipper, make sure to also update it!</p>
|
||||
<p>If you're using the web clipper, make sure to also update it!</p>
|
||||
<ul>
|
||||
<li>Updated: Auto-delete resources only after 10 days to handle some edge cases</li>
|
||||
<li>Clipper: Cleaner and more consistent clipper REST API, to facilitate third-party access</li>
|
||||
<li>Clipper: Fixes <a href="https://github.com/laurent22/joplin/issues/569">#569</a>: Make clipper service available on localhost only</li>
|
||||
<li>Clipper: Fixes <a href="https://github.com/laurent22/joplin/issues/573">#573</a>: Better handling of certain code blocks</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-97-https-github-com-laurent22-joplin-releases-tag-v1-0-97-2018-06-09t19-23-34z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.97">v1.0.97</a> - 2018-06-09T19:23:34Z</h2>
|
||||
<p>If you're using the web clipper, make sure to also update it!</p>
|
||||
<h2><a name="v1-0-97-https-github-com-laurent22-joplin-releases-tag-v1-0-97-2018-06-09t19-23-34z" href="#v1-0-97-https-github-com-laurent22-joplin-releases-tag-v1-0-97-2018-06-09t19-23-34z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.97">v1.0.97</a> - 2018-06-09T19:23:34Z</h2>
|
||||
<p>If you're using the web clipper, make sure to also update it!</p>
|
||||
<ul>
|
||||
<li>Updated: Auto-delete resources only after 10 days to handle some edge cases</li>
|
||||
<li>Clipper: Cleaner and more consistent clipper REST API, to facilitate third-party access</li>
|
||||
<li>Clipper: Fixes <a href="https://github.com/laurent22/joplin/issues/569">#569</a>: Make clipper service available on localhost only</li>
|
||||
<li>Clipper: Fixes <a href="https://github.com/laurent22/joplin/issues/573">#573</a>: Better handling of certain code blocks</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-96-https-github-com-laurent22-joplin-releases-tag-v1-0-96-2018-05-26t16-36-39z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.96">v1.0.96</a> - 2018-05-26T16:36:39Z</h2>
|
||||
<h2><a name="v1-0-96-https-github-com-laurent22-joplin-releases-tag-v1-0-96-2018-05-26t16-36-39z" href="#v1-0-96-https-github-com-laurent22-joplin-releases-tag-v1-0-96-2018-05-26t16-36-39z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.96">v1.0.96</a> - 2018-05-26T16:36:39Z</h2>
|
||||
<p>This release is mainly to fix various issues with the recently released Web Clipper.</p>
|
||||
<ul>
|
||||
<li>Clipper: Allow selecting folder to add the note to</li>
|
||||
<li>Clipper: Fixed issue when taking screenshot</li>
|
||||
<li>Clipper: Added Firefox extension</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-95-https-github-com-laurent22-joplin-releases-tag-v1-0-95-2018-05-25t13-04-30z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.95">v1.0.95</a> - 2018-05-25T13:04:30Z</h2>
|
||||
<h2><a name="v1-0-95-https-github-com-laurent22-joplin-releases-tag-v1-0-95-2018-05-25t13-04-30z" href="#v1-0-95-https-github-com-laurent22-joplin-releases-tag-v1-0-95-2018-05-25t13-04-30z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.95">v1.0.95</a> - 2018-05-25T13:04:30Z</h2>
|
||||
<ul>
|
||||
<li>New: A web clipper is now available - it allows saving web pages and screenshots from your browser to Joplin. To start using it, go to Options > Web Clipper Options. Note that this feature is a beta release so there might still be some issues. Feedback is welcome.</li>
|
||||
<li>Fix: Identify another Dropbox missing auth error, to allow resetting the token</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/531">#531</a>: Get WebDAV to work with certain servers that require a trailing slash on directories</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-94-https-github-com-laurent22-joplin-releases-tag-v1-0-94-2018-05-21t20-52-59z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.94">v1.0.94</a> - 2018-05-21T20:52:59Z</h2>
|
||||
<h2><a name="v1-0-94-https-github-com-laurent22-joplin-releases-tag-v1-0-94-2018-05-21t20-52-59z" href="#v1-0-94-https-github-com-laurent22-joplin-releases-tag-v1-0-94-2018-05-21t20-52-59z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.94">v1.0.94</a> - 2018-05-21T20:52:59Z</h2>
|
||||
<ul>
|
||||
<li>New: Allow copying path of resources</li>
|
||||
<li>New: Adds functionality to allow for renaming of tags.</li>
|
||||
@ -645,15 +663,15 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/527">#527</a>: Remove empty section separators from menus</li>
|
||||
<li>Fix: Added styles to fix margin bottom for nested lists</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-93-https-github-com-laurent22-joplin-releases-tag-v1-0-93-2018-05-14t11-36-01z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.93">v1.0.93</a> - 2018-05-14T11:36:01Z</h2>
|
||||
<h2><a name="v1-0-93-https-github-com-laurent22-joplin-releases-tag-v1-0-93-2018-05-14t11-36-01z" href="#v1-0-93-https-github-com-laurent22-joplin-releases-tag-v1-0-93-2018-05-14t11-36-01z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.93">v1.0.93</a> - 2018-05-14T11:36:01Z</h2>
|
||||
<ul>
|
||||
<li>New: A portable version is now available. To install it simply copy the file "JoplinPortable.exe" to your USB device. See the documentation for more information - <a href="https://joplin.cozic.net/#desktop-applications">https://joplin.cozic.net/#desktop-applications</a></li>
|
||||
<li>Improved: Made import of ENEX files more robust and accurate</li>
|
||||
<li>Improved: Auto-update process should be more reliable.</li>
|
||||
<li>Fixed: Made sync-after-save interval longer to made synchronisations less frequent.</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-91-https-github-com-laurent22-joplin-releases-tag-v1-0-91-2018-05-10t14-48-04z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.91">v1.0.91</a> - 2018-05-10T14:48:04Z</h2>
|
||||
<p>Same as v1.0.90 but with a fix for <a href="https://github.com/laurent22/joplin/issues/510">#510</a> </p>
|
||||
<h2><a name="v1-0-91-https-github-com-laurent22-joplin-releases-tag-v1-0-91-2018-05-10t14-48-04z" href="#v1-0-91-https-github-com-laurent22-joplin-releases-tag-v1-0-91-2018-05-10t14-48-04z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.91">v1.0.91</a> - 2018-05-10T14:48:04Z</h2>
|
||||
<p>Same as v1.0.90 but with a fix for <a href="https://github.com/laurent22/joplin/issues/510">#510</a></p>
|
||||
<ul>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/345">#345</a>: Option to hide completed todos</li>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/200">#200</a>, Resolves <a href="https://github.com/laurent22/joplin/issues/416">#416</a>: Allow attaching images by pasting them in. Allow attaching files by drag and dropping them. Insert attachment at cursor position.</li>
|
||||
@ -662,14 +680,14 @@
|
||||
<li>Fixed: Tag display</li>
|
||||
<li>Security: Resolves <a href="https://github.com/laurent22/joplin/issues/500">#500</a>: Fixed XSS security vulnerability</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-89-https-github-com-laurent22-joplin-releases-tag-v1-0-89-2018-05-09t13-05-05z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.89">v1.0.89</a> - 2018-05-09T13:05:05Z</h2>
|
||||
<h2><a name="v1-0-89-https-github-com-laurent22-joplin-releases-tag-v1-0-89-2018-05-09t13-05-05z" href="#v1-0-89-https-github-com-laurent22-joplin-releases-tag-v1-0-89-2018-05-09t13-05-05z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.89">v1.0.89</a> - 2018-05-09T13:05:05Z</h2>
|
||||
<ul>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/122">#122</a>: Added support for sub-notebooks. Please see doc for more info: <a href="https://joplin.cozic.net/#sub-notebooks">https://joplin.cozic.net/#sub-notebooks</a></li>
|
||||
<li>Improved: Export/Import links to notes</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/480">#480</a>: Ignore invalid flag automatically passed by macOS</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/61">#61</a>: Handle path that ends with slash for file system sync</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-85-https-github-com-laurent22-joplin-releases-tag-v1-0-85-2018-05-01t21-08-24z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.85">v1.0.85</a> - 2018-05-01T21:08:24Z</h2>
|
||||
<h2><a name="v1-0-85-https-github-com-laurent22-joplin-releases-tag-v1-0-85-2018-05-01t21-08-24z" href="#v1-0-85-https-github-com-laurent22-joplin-releases-tag-v1-0-85-2018-05-01t21-08-24z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.85">v1.0.85</a> - 2018-05-01T21:08:24Z</h2>
|
||||
<p>Note: This is the same as v84 but with the note creation bug fixed.</p>
|
||||
<ul>
|
||||
<li>New: Windows 32-bit support</li>
|
||||
@ -683,22 +701,22 @@
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/470">#470</a>: Make it clear that spaces in URLs are invalid.</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/434">#434</a>: Handle Katex block mode</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-83-https-github-com-laurent22-joplin-releases-tag-v1-0-83-2018-04-04t19-43-58z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.83">v1.0.83</a> - 2018-04-04T19:43:58Z</h2>
|
||||
<h2><a name="v1-0-83-https-github-com-laurent22-joplin-releases-tag-v1-0-83-2018-04-04t19-43-58z" href="#v1-0-83-https-github-com-laurent22-joplin-releases-tag-v1-0-83-2018-04-04t19-43-58z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.83">v1.0.83</a> - 2018-04-04T19:43:58Z</h2>
|
||||
<ul>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/365">#365</a>: Cannot paste in Dropbox screen</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-82-https-github-com-laurent22-joplin-releases-tag-v1-0-82-2018-03-31t19-16-31z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.82">v1.0.82</a> - 2018-03-31T19:16:31Z</h2>
|
||||
<h2><a name="v1-0-82-https-github-com-laurent22-joplin-releases-tag-v1-0-82-2018-03-31t19-16-31z" href="#v1-0-82-https-github-com-laurent22-joplin-releases-tag-v1-0-82-2018-03-31t19-16-31z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.82">v1.0.82</a> - 2018-03-31T19:16:31Z</h2>
|
||||
<ul>
|
||||
<li>Updated translations</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-81-https-github-com-laurent22-joplin-releases-tag-v1-0-81-2018-03-28t08-13-58z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.81">v1.0.81</a> - 2018-03-28T08:13:58Z</h2>
|
||||
<h2><a name="v1-0-81-https-github-com-laurent22-joplin-releases-tag-v1-0-81-2018-03-28t08-13-58z" href="#v1-0-81-https-github-com-laurent22-joplin-releases-tag-v1-0-81-2018-03-28t08-13-58z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.81">v1.0.81</a> - 2018-03-28T08:13:58Z</h2>
|
||||
<ul>
|
||||
<li>New: Dropbox synchronisation</li>
|
||||
<li>New: Czech translation</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/318">#318</a>: Display full links in editor</li>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/329">#329</a>: Add link to E2EE doc</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-79-https-github-com-laurent22-joplin-releases-tag-v1-0-79-2018-03-23t18-00-11z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.79">v1.0.79</a> - 2018-03-23T18:00:11Z</h2>
|
||||
<h2><a name="v1-0-79-https-github-com-laurent22-joplin-releases-tag-v1-0-79-2018-03-23t18-00-11z" href="#v1-0-79-https-github-com-laurent22-joplin-releases-tag-v1-0-79-2018-03-23t18-00-11z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.79">v1.0.79</a> - 2018-03-23T18:00:11Z</h2>
|
||||
<ul>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/144">#144</a>, Resolves <a href="https://github.com/laurent22/joplin/issues/311">#311</a>: Highlight search results and search in real time. Associated Ctrl+F with searching.</li>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/73">#73</a>: Show modified date next to note in editor</li>
|
||||
@ -707,33 +725,33 @@
|
||||
<li>Updated: Resolves <a href="https://github.com/laurent22/joplin/issues/307">#307</a>: Use blue colour for sidebar, to be consistent with mobile app and logo</li>
|
||||
<li>Updated: Translations</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-78-https-github-com-laurent22-joplin-releases-tag-v1-0-78-2018-03-17t15-27-18z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.78">v1.0.78</a> - 2018-03-17T15:27:18Z</h2>
|
||||
<h2><a name="v1-0-78-https-github-com-laurent22-joplin-releases-tag-v1-0-78-2018-03-17t15-27-18z" href="#v1-0-78-https-github-com-laurent22-joplin-releases-tag-v1-0-78-2018-03-17t15-27-18z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.78">v1.0.78</a> - 2018-03-17T15:27:18Z</h2>
|
||||
<ul>
|
||||
<li>Improved: Handle deletion of resources that are not linked to any note</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-77-https-github-com-laurent22-joplin-releases-tag-v1-0-77-2018-03-16t15-12-35z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.77">v1.0.77</a> - 2018-03-16T15:12:35Z</h2>
|
||||
<h2><a name="v1-0-77-https-github-com-laurent22-joplin-releases-tag-v1-0-77-2018-03-16t15-12-35z" href="#v1-0-77-https-github-com-laurent22-joplin-releases-tag-v1-0-77-2018-03-16t15-12-35z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.77">v1.0.77</a> - 2018-03-16T15:12:35Z</h2>
|
||||
<p>Note: This fixes an invalid database upgrade in the previous version.</p>
|
||||
<ul>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/237">#237</a>: Export to PDF and print option</li>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/154">#154</a>: No longer used resources are automatically deleted after approximately 24h</li>
|
||||
<li>Improved: Resolves <a href="https://github.com/laurent22/joplin/issues/298">#298</a>: Removed extraneous first characters from auto-title</li>
|
||||
<li>Improved: Made WebDAV options dynamics so that changing username or password doesn't require restarting the app</li>
|
||||
<li>Improved: Made WebDAV options dynamics so that changing username or password doesn't require restarting the app</li>
|
||||
<li>Fix: Fixes <a href="https://github.com/laurent22/joplin/issues/291">#291</a>: Crash with empty backtick</li>
|
||||
<li>Fix: Fixes <a href="https://github.com/laurent22/joplin/issues/292">#292</a>: Improved auto-update feature and fixed incorrect notifications</li>
|
||||
<li>Fix: Signed executables on Windows</li>
|
||||
<li>Updated Russian, German, Portuguese, Spanish and French translations. Many thanks to the translators!</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-72-https-github-com-laurent22-joplin-releases-tag-v1-0-72-2018-03-14t09-44-35z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.72">v1.0.72</a> - 2018-03-14T09:44:35Z</h2>
|
||||
<h2><a name="v1-0-72-https-github-com-laurent22-joplin-releases-tag-v1-0-72-2018-03-14t09-44-35z" href="#v1-0-72-https-github-com-laurent22-joplin-releases-tag-v1-0-72-2018-03-14t09-44-35z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.72">v1.0.72</a> - 2018-03-14T09:44:35Z</h2>
|
||||
<ul>
|
||||
<li>New: Allow exporting only selected notes or notebook</li>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/266">#266</a>: Allow setting text editor font family</li>
|
||||
<li>New: Display icon next to resources and allow downloading them from Electron client</li>
|
||||
<li>Improved: Optimised sync when dealing with many items, in particular when using Nextcloud or WebDAV</li>
|
||||
<li>Improved: Display last sync error unless it's a timeout or network error</li>
|
||||
<li>Improved: Display last sync error unless it's a timeout or network error</li>
|
||||
<li>Improved: Fixes <a href="https://github.com/laurent22/joplin/issues/268">#268</a>: Improve error message for invalid flags</li>
|
||||
<li>Fix: Fixes <a href="https://github.com/laurent22/joplin/issues/271">#271</a>: Sort by created time was not respected</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-70-https-github-com-laurent22-joplin-releases-tag-v1-0-70-2018-02-28t20-04-30z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.70">v1.0.70</a> - 2018-02-28T20:04:30Z</h2>
|
||||
<h2><a name="v1-0-70-https-github-com-laurent22-joplin-releases-tag-v1-0-70-2018-02-28t20-04-30z" href="#v1-0-70-https-github-com-laurent22-joplin-releases-tag-v1-0-70-2018-02-28t20-04-30z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.70">v1.0.70</a> - 2018-02-28T20:04:30Z</h2>
|
||||
<ul>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/97">#97</a>: Export to JEX format or RAW format</li>
|
||||
<li>New: Import JEX and RAW format</li>
|
||||
@ -744,20 +762,20 @@
|
||||
<li>Fix: Fixed sync interval sorting order</li>
|
||||
<li>Fix: <a href="https://github.com/laurent22/joplin/issues/256">#256</a>: Check that no other instance of Joplin is running before launching a new one</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-67-https-github-com-laurent22-joplin-releases-tag-v1-0-67-2018-02-19t22-51-08z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.67">v1.0.67</a> - 2018-02-19T22:51:08Z</h2>
|
||||
<h2><a name="v1-0-67-https-github-com-laurent22-joplin-releases-tag-v1-0-67-2018-02-19t22-51-08z" href="#v1-0-67-https-github-com-laurent22-joplin-releases-tag-v1-0-67-2018-02-19t22-51-08z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.67">v1.0.67</a> - 2018-02-19T22:51:08Z</h2>
|
||||
<ul>
|
||||
<li>Fixed: <a href="https://github.com/laurent22/joplin/issues/217">#217</a>: Display a message when the note has no content and only the note viewer is visible</li>
|
||||
<li>Fixed: <a href="https://github.com/laurent22/joplin/issues/240">#240</a>: Tags should be handled in a case-insensitive way</li>
|
||||
<li>Fixed: <a href="https://github.com/laurent22/joplin/issues/241">#241</a>: Ignore response for certain WebDAV calls to improve compatibility with some services.</li>
|
||||
<li>Updated: French and Español translation</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-66-https-github-com-laurent22-joplin-releases-tag-v1-0-66-2018-02-18t23-09-09z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.66">v1.0.66</a> - 2018-02-18T23:09:09Z</h2>
|
||||
<h2><a name="v1-0-66-https-github-com-laurent22-joplin-releases-tag-v1-0-66-2018-02-18t23-09-09z" href="#v1-0-66-https-github-com-laurent22-joplin-releases-tag-v1-0-66-2018-02-18t23-09-09z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.66">v1.0.66</a> - 2018-02-18T23:09:09Z</h2>
|
||||
<ul>
|
||||
<li>Fixed: Local items were no longer being deleted via sync.</li>
|
||||
<li>Improved: More debug information when WebDAV sync target does not work.</li>
|
||||
<li>Improved: Compatibility with some WebDAV services (Seafile in particular)</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-65-https-github-com-laurent22-joplin-releases-tag-v1-0-65-2018-02-17t20-02-25z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.65">v1.0.65</a> - 2018-02-17T20:02:25Z</h2>
|
||||
<h2><a name="v1-0-65-https-github-com-laurent22-joplin-releases-tag-v1-0-65-2018-02-17t20-02-25z" href="#v1-0-65-https-github-com-laurent22-joplin-releases-tag-v1-0-65-2018-02-17t20-02-25z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.65">v1.0.65</a> - 2018-02-17T20:02:25Z</h2>
|
||||
<ul>
|
||||
<li>New: Added several keyboard shortcuts</li>
|
||||
<li>New: Convert new lines in tables to BR tags, and added support for HTML tags in Markdown viewers</li>
|
||||
@ -765,7 +783,7 @@
|
||||
<li>Fixed: Issue with items not being decrypted immediately when they are created due to a sync conflict.</li>
|
||||
<li>Updated: Translations</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-64-https-github-com-laurent22-joplin-releases-tag-v1-0-64-2018-02-16t00-58-20z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.64">v1.0.64</a> - 2018-02-16T00:58:20Z</h2>
|
||||
<h2><a name="v1-0-64-https-github-com-laurent22-joplin-releases-tag-v1-0-64-2018-02-16t00-58-20z" href="#v1-0-64-https-github-com-laurent22-joplin-releases-tag-v1-0-64-2018-02-16t00-58-20z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.64">v1.0.64</a> - 2018-02-16T00:58:20Z</h2>
|
||||
<p>Still more fixes and improvements to get v1 as stable as possible before adding new features.</p>
|
||||
<p>IMPORTANT: If you use Nextcloud it is recommended to sync all your notes before installing this release (see below).</p>
|
||||
<ul>
|
||||
@ -774,20 +792,20 @@
|
||||
<li>Fixed: Allow copy and paste from config and encryption screen on macOS</li>
|
||||
<li>Fixed: <a href="https://github.com/laurent22/joplin/issues/201">#201</a>, <a href="https://github.com/laurent22/joplin/issues/216">#216</a>: Make sure only one update check can run at a time, and improved modal dialog boxes</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-63-https-github-com-laurent22-joplin-releases-tag-v1-0-63-2018-02-14t19-40-36z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.63">v1.0.63</a> - 2018-02-14T19:40:36Z</h2>
|
||||
<h2><a name="v1-0-63-https-github-com-laurent22-joplin-releases-tag-v1-0-63-2018-02-14t19-40-36z" href="#v1-0-63-https-github-com-laurent22-joplin-releases-tag-v1-0-63-2018-02-14t19-40-36z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.63">v1.0.63</a> - 2018-02-14T19:40:36Z</h2>
|
||||
<ul>
|
||||
<li>Improved the way settings are changed. Should also fixed issue with sync context being accidentally broken.</li>
|
||||
<li>Improved WebDAV driver compatibility with some services (eg. Seafile)</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-62-https-github-com-laurent22-joplin-releases-tag-v1-0-62-2018-02-12t20-19-58z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.62">v1.0.62</a> - 2018-02-12T20:19:58Z</h2>
|
||||
<h2><a name="v1-0-62-https-github-com-laurent22-joplin-releases-tag-v1-0-62-2018-02-12t20-19-58z" href="#v1-0-62-https-github-com-laurent22-joplin-releases-tag-v1-0-62-2018-02-12t20-19-58z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.62">v1.0.62</a> - 2018-02-12T20:19:58Z</h2>
|
||||
<ul>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/205">#205</a>: Importing Evernote notes while on import page re-imports previous import</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/209">#209</a>: Items with non-ASCII characters end up truncated on Nextcloud</li>
|
||||
<li>Added Basque translation, fixed issue with handling invalid translations. Updated translation FR.</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-61-https-github-com-laurent22-joplin-releases-tag-v0-10-61-2018-02-08t18-27-39z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.61">v0.10.61</a> - 2018-02-08T18:27:39Z</h2>
|
||||
<h2><a name="v0-10-61-https-github-com-laurent22-joplin-releases-tag-v0-10-61-2018-02-08t18-27-39z" href="#v0-10-61-https-github-com-laurent22-joplin-releases-tag-v0-10-61-2018-02-08t18-27-39z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.61">v0.10.61</a> - 2018-02-08T18:27:39Z</h2>
|
||||
<ul>
|
||||
<li>New: Display message when creating new note or to-do so that it doesn't look like the previous note content got deleted.</li>
|
||||
<li>New: Display message when creating new note or to-do so that it doesn't look like the previous note content got deleted.</li>
|
||||
<li>New: Also support $ as delimiter for Katex expressions</li>
|
||||
<li>New: Added sync config check to config screens</li>
|
||||
<li>New: Allowing opening and saving resource images</li>
|
||||
@ -796,23 +814,23 @@
|
||||
<li>Fix: Make sure alarms and resources are attached to right note when creating new note</li>
|
||||
<li>Fix: Use mutex when saving model to avoid race conditions when decrypting and syncing at the same time</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-60-https-github-com-laurent22-joplin-releases-tag-v0-10-60-2018-02-06t13-09-56z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.60">v0.10.60</a> - 2018-02-06T13:09:56Z</h2>
|
||||
<h2><a name="v0-10-60-https-github-com-laurent22-joplin-releases-tag-v0-10-60-2018-02-06t13-09-56z" href="#v0-10-60-https-github-com-laurent22-joplin-releases-tag-v0-10-60-2018-02-06t13-09-56z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.60">v0.10.60</a> - 2018-02-06T13:09:56Z</h2>
|
||||
<ul>
|
||||
<li>New: WebDAV synchronisation target</li>
|
||||
<li>New: Support for math typesetting <a href="https://khan.github.io/KaTeX/">Katex</a></li>
|
||||
<li>New: Tray icon for Windows and macOS</li>
|
||||
<li>Fixed: Don't allow adding notes to conflict notebook</li>
|
||||
<li>Fixed: Don't allow adding notes to conflict notebook</li>
|
||||
<li>Updated: Russian translation</li>
|
||||
<li>Updated: French translation</li>
|
||||
<li>New: List missing master keys in encryption screen</li>
|
||||
<li>Fixed: Attaching images in Linux was no longer working</li>
|
||||
<li>Fixed crash in macOS</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-54-https-github-com-laurent22-joplin-releases-tag-v0-10-54-2018-01-31t20-21-30z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.54">v0.10.54</a> - 2018-01-31T20:21:30Z</h2>
|
||||
<h2><a name="v0-10-54-https-github-com-laurent22-joplin-releases-tag-v0-10-54-2018-01-31t20-21-30z" href="#v0-10-54-https-github-com-laurent22-joplin-releases-tag-v0-10-54-2018-01-31t20-21-30z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.54">v0.10.54</a> - 2018-01-31T20:21:30Z</h2>
|
||||
<ul>
|
||||
<li>Optimised Nextcloud functionality so that it is faster and consumes less resources</li>
|
||||
<li>Fixed Nextcloud sync issue when processing many items.</li>
|
||||
<li>Fixed: Handle case where file is left half-uploaded on Nextcloud instance (possibly an ocloud.de issue only)</li>
|
||||
<li>Fixed: Handle case where file is left half-uploaded on Nextcloud instance (possibly an <a href="http://ocloud.de">ocloud.de</a> issue only)</li>
|
||||
<li>Fixed: Allow decryption of other items to continue even if an item cannot be decrypted</li>
|
||||
<li>Add Content-Size header for WebDAV, which is required by some services</li>
|
||||
<li>Fixed auto-title when title is manually entered first</li>
|
||||
@ -820,104 +838,103 @@
|
||||
<li>New: Allow focusing either title or body when creating a new note or to-do</li>
|
||||
<li>Fixed crash when having invalid UTF-8 string in text editor</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-52-https-github-com-laurent22-joplin-releases-tag-v0-10-52-2018-01-31t19-25-18z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.52">v0.10.52</a> - 2018-01-31T19:25:18Z</h2>
|
||||
<h2><a name="v0-10-52-https-github-com-laurent22-joplin-releases-tag-v0-10-52-2018-01-31t19-25-18z" href="#v0-10-52-https-github-com-laurent22-joplin-releases-tag-v0-10-52-2018-01-31t19-25-18z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.52">v0.10.52</a> - 2018-01-31T19:25:18Z</h2>
|
||||
<ul>
|
||||
<li>Optimised Nextcloud functionality so that it is faster and consumes less resources</li>
|
||||
<li>Fixed Nextcloud sync issue when processing many items.</li>
|
||||
<li>Fixed: Handle case where file is left half-uploaded on Nextcloud instance (possibly an ocloud.de issue only)</li>
|
||||
<li>Fixed: Handle case where file is left half-uploaded on Nextcloud instance (possibly an <a href="http://ocloud.de">ocloud.de</a> issue only)</li>
|
||||
<li>Fixed: Allow decryption of other items to continue even if an item cannot be decrypted</li>
|
||||
<li>Add Content-Size header for WebDAV, which is required by some services</li>
|
||||
<li>Fixed auto-title when title is manually entered first</li>
|
||||
<li>Improved auto-update process to avoid random crashes</li>
|
||||
<li>New: Allow focusing either title or body when creating a new note or to-do</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-51-https-github-com-laurent22-joplin-releases-tag-v0-10-51-2018-01-28t18-47-02z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.51">v0.10.51</a> - 2018-01-28T18:47:02Z</h2>
|
||||
<h2><a name="v0-10-51-https-github-com-laurent22-joplin-releases-tag-v0-10-51-2018-01-28t18-47-02z" href="#v0-10-51-https-github-com-laurent22-joplin-releases-tag-v0-10-51-2018-01-28t18-47-02z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.51">v0.10.51</a> - 2018-01-28T18:47:02Z</h2>
|
||||
<ul>
|
||||
<li>Added Nextcloud support (Beta)</li>
|
||||
<li>Upgraded Electron to 1.7.11 to fix security vulnerability</li>
|
||||
<li>Fixed checkbox issue in config screen</li>
|
||||
<li>Fixed detection of encrypted item</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-48-https-github-com-laurent22-joplin-releases-tag-v0-10-48-2018-01-23t11-19-51z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.48">v0.10.48</a> - 2018-01-23T11:19:51Z</h2>
|
||||
<h2><a name="v0-10-48-https-github-com-laurent22-joplin-releases-tag-v0-10-48-2018-01-23t11-19-51z" href="#v0-10-48-https-github-com-laurent22-joplin-releases-tag-v0-10-48-2018-01-23t11-19-51z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.48">v0.10.48</a> - 2018-01-23T11:19:51Z</h2>
|
||||
<ul>
|
||||
<li>Improved and optimised file system sync target when many items are present.</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/155">#155</a>: Caret alignment issue with Russian text</li>
|
||||
<li>Dutch translation (Thanks @tcassaert)</li>
|
||||
<li>Removed certain log statements so that sensitive info doesn't end up in logs</li>
|
||||
<li>Removed certain log statements so that sensitive info doesn't end up in logs</li>
|
||||
<li>Fix: Handle case where resource blob is missing during sync</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-47-https-github-com-laurent22-joplin-releases-tag-v0-10-47-2018-01-16t17-27-17z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.47">v0.10.47</a> - 2018-01-16T17:27:17Z</h2>
|
||||
<h2><a name="v0-10-47-https-github-com-laurent22-joplin-releases-tag-v0-10-47-2018-01-16t17-27-17z" href="#v0-10-47-https-github-com-laurent22-joplin-releases-tag-v0-10-47-2018-01-16t17-27-17z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.47">v0.10.47</a> - 2018-01-16T17:27:17Z</h2>
|
||||
<ul>
|
||||
<li>Improved the way new note are created, and automatically add a title. Made saving and loading notes more reliable.</li>
|
||||
<li>Fix: race condition when a note is being uploaded while it's being modified in the text editor</li>
|
||||
<li>Fix: race condition when a note is being uploaded while it's being modified in the text editor</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/129">#129</a>: Tags are case insensitive</li>
|
||||
<li>Schedule sync only after 30 seconds</li>
|
||||
<li>Schedule sync after enabling or disabling encryption</li>
|
||||
<li>Display sync items being fetched</li>
|
||||
<li>Fixed logic of what note is used when right-clicking one or more notes</li>
|
||||
<li>Fix: Don't scroll back to top when note is reloaded via sync</li>
|
||||
<li>Fix: Don't scroll back to top when note is reloaded via sync</li>
|
||||
<li>Display URL for links</li>
|
||||
<li>Fix: Move prompt to top to avoid issue with date picker being hidden</li>
|
||||
<li>Fixed table font size and family</li>
|
||||
<li>Fixed logic to save, and make sure scheduled save always happen even when changing note</li>
|
||||
<li>Fixed OneDrive sync when resync is requested</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/85">#85</a>: Don't record deleted_items entries for folders deleted via sync</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/85">#85</a>: Don't record deleted_items entries for folders deleted via sync</li>
|
||||
<li>Updated translations</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-43-https-github-com-laurent22-joplin-releases-tag-v0-10-43-2018-01-08t10-12-10z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.43">v0.10.43</a> - 2018-01-08T10:12:10Z</h2>
|
||||
<h2><a name="v0-10-43-https-github-com-laurent22-joplin-releases-tag-v0-10-43-2018-01-08t10-12-10z" href="#v0-10-43-https-github-com-laurent22-joplin-releases-tag-v0-10-43-2018-01-08t10-12-10z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.43">v0.10.43</a> - 2018-01-08T10:12:10Z</h2>
|
||||
<ul>
|
||||
<li>Fixed saving and loading of settings, which could affect synchronisation</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-41-https-github-com-laurent22-joplin-releases-tag-v0-10-41-2018-01-05t20-38-12z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.41">v0.10.41</a> - 2018-01-05T20:38:12Z</h2>
|
||||
<h2><a name="v0-10-41-https-github-com-laurent22-joplin-releases-tag-v0-10-41-2018-01-05t20-38-12z" href="#v0-10-41-https-github-com-laurent22-joplin-releases-tag-v0-10-41-2018-01-05t20-38-12z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.41">v0.10.41</a> - 2018-01-05T20:38:12Z</h2>
|
||||
<ul>
|
||||
<li>Added End-To-End Encryption support (E2EE)</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-40-https-github-com-laurent22-joplin-releases-tag-v0-10-40-2018-01-02t23-16-57z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.40">v0.10.40</a> - 2018-01-02T23:16:57Z</h2>
|
||||
<h2><a name="v0-10-40-https-github-com-laurent22-joplin-releases-tag-v0-10-40-2018-01-02t23-16-57z" href="#v0-10-40-https-github-com-laurent22-joplin-releases-tag-v0-10-40-2018-01-02t23-16-57z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.40">v0.10.40</a> - 2018-01-02T23:16:57Z</h2>
|
||||
<ul>
|
||||
<li>Fixed undo in text editor</li>
|
||||
<li>Updated German translation</li>
|
||||
<li>Added Russian, Japanese and Chinese translations</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-39-https-github-com-laurent22-joplin-releases-tag-v0-10-39-2017-12-11t21-19-44z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.39">v0.10.39</a> - 2017-12-11T21:19:44Z</h2>
|
||||
<h2><a name="v0-10-39-https-github-com-laurent22-joplin-releases-tag-v0-10-39-2017-12-11t21-19-44z" href="#v0-10-39-https-github-com-laurent22-joplin-releases-tag-v0-10-39-2017-12-11t21-19-44z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.39">v0.10.39</a> - 2017-12-11T21:19:44Z</h2>
|
||||
<ul>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/55">#55</a>: Added support for HTML tags found in ENEX files: colgroup, col, ins, kbd, address, caption, var, area, map</li>
|
||||
<li>Resolve <a href="https://github.com/laurent22/joplin/issues/7">#7</a>: Show storage location in Options screen</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/84">#84</a>: Fields losing focus in Config screen</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/86">#86</a>: App icon missing on Linux</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/87">#87</a>: Show warningn when deleting notebook that contains notes.</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/3">#3</a>: Paths with '.' would cause JSX compilation to fail</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/3">#3</a>: Paths with '.' would cause JSX compilation to fail</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-38-https-github-com-laurent22-joplin-releases-tag-v0-10-38-2017-12-08t10-12-06z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.38">v0.10.38</a> - 2017-12-08T10:12:06Z</h2>
|
||||
<h2><a name="v0-10-38-https-github-com-laurent22-joplin-releases-tag-v0-10-38-2017-12-08t10-12-06z" href="#v0-10-38-https-github-com-laurent22-joplin-releases-tag-v0-10-38-2017-12-08t10-12-06z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.38">v0.10.38</a> - 2017-12-08T10:12:06Z</h2>
|
||||
<ul>
|
||||
<li>Dialog to export sync status</li>
|
||||
<li>Enabled support for filesystem sync</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-37-https-github-com-laurent22-joplin-releases-tag-v0-10-37-2017-12-07t19-38-05z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.37">v0.10.37</a> - 2017-12-07T19:38:05Z</h2>
|
||||
<h2><a name="v0-10-37-https-github-com-laurent22-joplin-releases-tag-v0-10-37-2017-12-07t19-38-05z" href="#v0-10-37-https-github-com-laurent22-joplin-releases-tag-v0-10-37-2017-12-07t19-38-05z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.37">v0.10.37</a> - 2017-12-07T19:38:05Z</h2>
|
||||
<ul>
|
||||
<li>Better handling of items that cannot be synchronised (for example, if they exceed the max file size supported by the target)</li>
|
||||
<li>Added Synchronisation Status screen</li>
|
||||
<li>Improved Enex support:<ul>
|
||||
<li>Improved Enex support:
|
||||
<ul>
|
||||
<li>Better handling of notes containing entire web pages (such as when imported via Web Clipper)</li>
|
||||
<li>Support for <code><img></code> tags</li>
|
||||
<li>Support for various other tags</li>
|
||||
<li>Improved importing web pages that contain tables within tables. In which case the outer tables (which are usually the website layout) are rendered as regular text block and only the inner tables are actually rendered as tables.<ul>
|
||||
<li>Improved importing web pages that contain tables within tables. In which case the outer tables (which are usually the website layout) are rendered as regular text block and only the inner tables are actually rendered as tables.</li>
|
||||
<li>Fixed many other minor warnings and errors</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Allow setting installation directory in Windows</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-36-https-github-com-laurent22-joplin-releases-tag-v0-10-36-2017-12-05t09-34-40z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.36">v0.10.36</a> - 2017-12-05T09:34:40Z</h2>
|
||||
<h2><a name="v0-10-36-https-github-com-laurent22-joplin-releases-tag-v0-10-36-2017-12-05t09-34-40z" href="#v0-10-36-https-github-com-laurent22-joplin-releases-tag-v0-10-36-2017-12-05t09-34-40z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.36">v0.10.36</a> - 2017-12-05T09:34:40Z</h2>
|
||||
<ul>
|
||||
<li>All: Improved synchronisation when sync target has unreliable timestamps</li>
|
||||
<li>All: Fixed display issue - when items were modified during sync it could result in blank rows being displayed in note lists.</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-35-https-github-com-laurent22-joplin-releases-tag-v0-10-35-2017-12-02t15-56-08z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.35">v0.10.35</a> - 2017-12-02T15:56:08Z</h2>
|
||||
<h2><a name="v0-10-35-https-github-com-laurent22-joplin-releases-tag-v0-10-35-2017-12-02t15-56-08z" href="#v0-10-35-https-github-com-laurent22-joplin-releases-tag-v0-10-35-2017-12-02t15-56-08z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.35">v0.10.35</a> - 2017-12-02T15:56:08Z</h2>
|
||||
<ul>
|
||||
<li>All: Fixed sync issue and database migration issue</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-34-https-github-com-laurent22-joplin-releases-tag-v0-10-34-2017-12-02t14-50-28z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.34">v0.10.34</a> - 2017-12-02T14:50:28Z</h2>
|
||||
<h2><a name="v0-10-34-https-github-com-laurent22-joplin-releases-tag-v0-10-34-2017-12-02t14-50-28z" href="#v0-10-34-https-github-com-laurent22-joplin-releases-tag-v0-10-34-2017-12-02t14-50-28z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.34">v0.10.34</a> - 2017-12-02T14:50:28Z</h2>
|
||||
<ul>
|
||||
<li>All: fixed database creation error</li>
|
||||
<li>All: Improved Evernote import for blockquotes and sup tags</li>
|
||||
@ -930,43 +947,43 @@
|
||||
<li>All: Allow attaching files of unknown mime type</li>
|
||||
<li>All: Added error for OneDrive for Business</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-33-https-github-com-laurent22-joplin-releases-tag-v0-10-33-2017-12-02t13-20-39z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.33">v0.10.33</a> - 2017-12-02T13:20:39Z</h2>
|
||||
<h2><a name="v0-10-33-https-github-com-laurent22-joplin-releases-tag-v0-10-33-2017-12-02t13-20-39z" href="#v0-10-33-https-github-com-laurent22-joplin-releases-tag-v0-10-33-2017-12-02t13-20-39z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.33">v0.10.33</a> - 2017-12-02T13:20:39Z</h2>
|
||||
<ul>
|
||||
<li>Improved Evernote import for blockquotes and sup tags</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-31-https-github-com-laurent22-joplin-releases-tag-v0-10-31-2017-12-01t09-56-44z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.31">v0.10.31</a> - 2017-12-01T09:56:44Z</h2>
|
||||
<h2><a name="v0-10-31-https-github-com-laurent22-joplin-releases-tag-v0-10-31-2017-12-01t09-56-44z" href="#v0-10-31-https-github-com-laurent22-joplin-releases-tag-v0-10-31-2017-12-01t09-56-44z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.31">v0.10.31</a> - 2017-12-01T09:56:44Z</h2>
|
||||
<ul>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/22">#22</a> - keyboard cursor jumps while typing.</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-30-https-github-com-laurent22-joplin-releases-tag-v0-10-30-2017-11-30t20-28-16z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.30">v0.10.30</a> - 2017-11-30T20:28:16Z</h2>
|
||||
<h2><a name="v0-10-30-https-github-com-laurent22-joplin-releases-tag-v0-10-30-2017-11-30t20-28-16z" href="#v0-10-30-https-github-com-laurent22-joplin-releases-tag-v0-10-30-2017-11-30t20-28-16z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.30">v0.10.30</a> - 2017-11-30T20:28:16Z</h2>
|
||||
<ul>
|
||||
<li>Added Spanish locale (thank you Erick Rodríguez Ponce)</li>
|
||||
<li>Fixed copy/cut/paste issue in macOS</li>
|
||||
<li>Fixed checkbox issue in Option screen.</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-28-https-github-com-laurent22-joplin-releases-tag-v0-10-28-2017-11-30t01-07-46z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.28">v0.10.28</a> - 2017-11-30T01:07:46Z</h2>
|
||||
<h2><a name="v0-10-28-https-github-com-laurent22-joplin-releases-tag-v0-10-28-2017-11-30t01-07-46z" href="#v0-10-28-https-github-com-laurent22-joplin-releases-tag-v0-10-28-2017-11-30t01-07-46z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.28">v0.10.28</a> - 2017-11-30T01:07:46Z</h2>
|
||||
<ul>
|
||||
<li>Added toolbar to set alarms and attach files</li>
|
||||
<li>Fixed Evernote import of certain images</li>
|
||||
<li>Fixed note update issue</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-26-https-github-com-laurent22-joplin-releases-tag-v0-10-26-2017-11-29t16-02-17z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.26">v0.10.26</a> - 2017-11-29T16:02:17Z</h2>
|
||||
<h2><a name="v0-10-26-https-github-com-laurent22-joplin-releases-tag-v0-10-26-2017-11-29t16-02-17z" href="#v0-10-26-https-github-com-laurent22-joplin-releases-tag-v0-10-26-2017-11-29t16-02-17z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.26">v0.10.26</a> - 2017-11-29T16:02:17Z</h2>
|
||||
<ul>
|
||||
<li>Added support for alarms (notifications)</li>
|
||||
<li>Fixed scrolling issue for long notes</li>
|
||||
<li>Improved OneDrive login and possibly fixed rare error</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-25-https-github-com-laurent22-joplin-releases-tag-v0-10-25-2017-11-24t14-27-49z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.25">v0.10.25</a> - 2017-11-24T14:27:49Z</h2>
|
||||
<h2><a name="v0-10-25-https-github-com-laurent22-joplin-releases-tag-v0-10-25-2017-11-24t14-27-49z" href="#v0-10-25-https-github-com-laurent22-joplin-releases-tag-v0-10-25-2017-11-24t14-27-49z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.25">v0.10.25</a> - 2017-11-24T14:27:49Z</h2>
|
||||
<ul>
|
||||
<li>Allow multi-selection on note lists</li>
|
||||
<li>Allow drag and drop of notes</li>
|
||||
<li>Hide invalid characters (non-breaking spaces) in editor</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-23-https-github-com-laurent22-joplin-releases-tag-v0-10-23-2017-11-21t19-38-41z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.23">v0.10.23</a> - 2017-11-21T19:38:41Z</h2>
|
||||
<h2 id="-v0-10-22-https-github-com-laurent22-joplin-releases-tag-v0-10-22-2017-11-20t21-45-57z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.22">v0.10.22</a> - 2017-11-20T21:45:57Z</h2>
|
||||
<h2 id="-v0-10-21-https-github-com-laurent22-joplin-releases-tag-v0-10-21-2017-11-18t00-53-15z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.21">v0.10.21</a> - 2017-11-18T00:53:15Z</h2>
|
||||
<h2 id="-v0-10-20-https-github-com-laurent22-joplin-releases-tag-v0-10-20-2017-11-17t17-18-25z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.20">v0.10.20</a> - 2017-11-17T17:18:25Z</h2>
|
||||
<h2 id="-v0-10-19-https-github-com-laurent22-joplin-releases-tag-v0-10-19-2017-11-20t18-59-48z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.19">v0.10.19</a> - 2017-11-20T18:59:48Z</h2>
|
||||
<h2><a name="v0-10-23-https-github-com-laurent22-joplin-releases-tag-v0-10-23-2017-11-21t19-38-41z" href="#v0-10-23-https-github-com-laurent22-joplin-releases-tag-v0-10-23-2017-11-21t19-38-41z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.23">v0.10.23</a> - 2017-11-21T19:38:41Z</h2>
|
||||
<h2><a name="v0-10-22-https-github-com-laurent22-joplin-releases-tag-v0-10-22-2017-11-20t21-45-57z" href="#v0-10-22-https-github-com-laurent22-joplin-releases-tag-v0-10-22-2017-11-20t21-45-57z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.22">v0.10.22</a> - 2017-11-20T21:45:57Z</h2>
|
||||
<h2><a name="v0-10-21-https-github-com-laurent22-joplin-releases-tag-v0-10-21-2017-11-18t00-53-15z" href="#v0-10-21-https-github-com-laurent22-joplin-releases-tag-v0-10-21-2017-11-18t00-53-15z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.21">v0.10.21</a> - 2017-11-18T00:53:15Z</h2>
|
||||
<h2><a name="v0-10-20-https-github-com-laurent22-joplin-releases-tag-v0-10-20-2017-11-17t17-18-25z" href="#v0-10-20-https-github-com-laurent22-joplin-releases-tag-v0-10-20-2017-11-17t17-18-25z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.20">v0.10.20</a> - 2017-11-17T17:18:25Z</h2>
|
||||
<h2><a name="v0-10-19-https-github-com-laurent22-joplin-releases-tag-v0-10-19-2017-11-20t18-59-48z" href="#v0-10-19-https-github-com-laurent22-joplin-releases-tag-v0-10-19-2017-11-20t18-59-48z" class="heading-anchor">🔗</a><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.19">v0.10.19</a> - 2017-11-20T18:59:48Z</h2>
|
||||
|
||||
<script>
|
||||
function stickyHeader() {
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,10 +268,10 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="joplin-web-clipper">Joplin Web Clipper</h1>
|
||||
<h1><a name="joplin-web-clipper" href="#joplin-web-clipper" class="heading-anchor">🔗</a>Joplin Web Clipper</h1>
|
||||
<p>The Web Clipper is a browser extension that allows you to save web pages and screenshots from your browser. To start using it, open the Joplin desktop application, go to the <strong>Web Clipper Options</strong> and follow the instructions.</p>
|
||||
<p><img src="https://joplinapp.org/images/WebExtensionScreenshot.png" style="max-width: 50%; border: 1px solid gray;"></p>
|
||||
<h1 id="troubleshooting-the-web-clipper-service">Troubleshooting the web clipper service</h1>
|
||||
<img src="https://joplinapp.org/images/WebExtensionScreenshot.png" style="max-width: 50%; border: 1px solid gray;">
|
||||
<h1><a name="troubleshooting-the-web-clipper-service" href="#troubleshooting-the-web-clipper-service" class="heading-anchor">🔗</a>Troubleshooting the web clipper service</h1>
|
||||
<p>The web clipper extension and the Joplin application communicates via a service, which is started by the Joplin desktop app.</p>
|
||||
<p>However certain things can interfer with this service and prevent it from being accessible or from starting. If something does not work, check the following:</p>
|
||||
<ul>
|
||||
@ -262,8 +280,8 @@
|
||||
<li>Check that no proxy is running on the machine, or make sure that the requests from the web clipper service are filtered and allowed. For example <a href="https://github.com/laurent22/joplin/issues/561#issuecomment-392220191">https://github.com/laurent22/joplin/issues/561#issuecomment-392220191</a></li>
|
||||
</ul>
|
||||
<p>If none of this work, please report it on the <a href="https://discourse.joplin.cozic.net/">forum</a> or <a href="https://github.com/laurent22/joplin/issues">GitHub issue tracker</a></p>
|
||||
<h1 id="debugging-the-extension">Debugging the extension</h1>
|
||||
<h2 id="in-chrome">In Chrome</h2>
|
||||
<h1><a name="debugging-the-extension" href="#debugging-the-extension" class="heading-anchor">🔗</a>Debugging the extension</h1>
|
||||
<h2><a name="in-chrome" href="#in-chrome" class="heading-anchor">🔗</a>In Chrome</h2>
|
||||
<p>To provide as much information as possible when reporting an issue, you may provide the log from the various Chrome console.</p>
|
||||
<p>To do so, first enable developer mode in <a href="chrome://extensions/">chrome://extensions/</a></p>
|
||||
<ul>
|
||||
@ -271,7 +289,7 @@
|
||||
<li>Debugging the background script: In <code>chrome://extensions/</code>, click on "Inspect background script".</li>
|
||||
<li>Debugging the content script: Press Ctrl+Shift+I to open the console of the current page.</li>
|
||||
</ul>
|
||||
<h2 id="in-firefox">In Firefox</h2>
|
||||
<h2><a name="in-firefox" href="#in-firefox" class="heading-anchor">🔗</a>In Firefox</h2>
|
||||
<ul>
|
||||
<li>Open <a href="about:debugging">about:debugging</a> in Firefox.</li>
|
||||
<li>Make sure the checkox "Enable add-on debugging" is ticked.</li>
|
||||
@ -279,10 +297,10 @@
|
||||
<li>Click on "Debugging" - that should open a new console window.</li>
|
||||
</ul>
|
||||
<p>Also press F12 to open the regular Firefox console (some messages from the Joplin extension can go there too).</p>
|
||||
<p>Now use the extension as normal and replicate the bug you're having.</p>
|
||||
<p>Now use the extension as normal and replicate the bug you're having.</p>
|
||||
<p>Copy and paste the content of both the debugging window and the Firefox console, and post it to the <a href="https://discourse.joplin.cozic.net/">forum</a>.</p>
|
||||
<h1 id="using-the-web-clipper-service">Using the Web Clipper service</h1>
|
||||
<p>The Web Clipper service can be used to create, modify or delete notes, notebooks, tags, etc. from any other application. It exposes an API with a number of methods to manage Joplin's data. For more information about this API and how to use it, please check the <a href="https://joplinapp.org/api/">Joplin API documentation</a>.</p>
|
||||
<h1><a name="using-the-web-clipper-service" href="#using-the-web-clipper-service" class="heading-anchor">🔗</a>Using the Web Clipper service</h1>
|
||||
<p>The Web Clipper service can be used to create, modify or delete notes, notebooks, tags, etc. from any other application. It exposes an API with a number of methods to manage Joplin's data. For more information about this API and how to use it, please check the <a href="https://joplinapp.org/api/">Joplin API documentation</a>.</p>
|
||||
|
||||
<script>
|
||||
function stickyHeader() {
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,26 +268,26 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="how-to-enable-debugging">How to enable debugging</h1>
|
||||
<h1><a name="how-to-enable-debugging" href="#how-to-enable-debugging" class="heading-anchor">🔗</a>How to enable debugging</h1>
|
||||
<p>It is possible to get the apps to display or log more information that might help debug various issues.</p>
|
||||
<h2 id="desktop-application">Desktop application</h2>
|
||||
<h2><a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a>Desktop application</h2>
|
||||
<ul>
|
||||
<li>Add a file named "flags.txt" in the config directory (should be <code>~/.config/joplin-desktop</code> or <code>c:\Users\YOUR_NAME\.config\joplin-desktop</code>) with the following content: <code>--open-dev-tools --log-level debug</code></li>
|
||||
<li>Restart the application</li>
|
||||
<li>The development tools should now be opened. Click the "Console" tab</li>
|
||||
<li>Now repeat the action that was causing problem. The console might output warnings or errors - please add them to the GitHub issue. Also open log.txt in the config folder and if there is any error or warning, please also add them to the issue.</li>
|
||||
</ul>
|
||||
<h2 id="cli-application">CLI application</h2>
|
||||
<h2><a name="cli-application" href="#cli-application" class="heading-anchor">🔗</a>CLI application</h2>
|
||||
<ul>
|
||||
<li>Start the app with <code>joplin --log-level debug</code></li>
|
||||
<li>Check the log.txt as specified above for the desktop application and attach the log to the GitHub issue (or just the warnings/errors if any)</li>
|
||||
</ul>
|
||||
<h2 id="mobile-application">Mobile application</h2>
|
||||
<h2><a name="mobile-application" href="#mobile-application" class="heading-anchor">🔗</a>Mobile application</h2>
|
||||
<ul>
|
||||
<li>In the options, enable Advanced Option</li>
|
||||
<li>Open the log in the top right hand corner menu and post a screenshot of any error/warning.</li>
|
||||
</ul>
|
||||
<h1 id="creating-a-low-level-bug-report-on-android">Creating a low-level bug report on Android</h1>
|
||||
<h1><a name="creating-a-low-level-bug-report-on-android" href="#creating-a-low-level-bug-report-on-android" class="heading-anchor">🔗</a>Creating a low-level bug report on Android</h1>
|
||||
<p><a href="https://developer.android.com/studio/debug/bug-report">https://developer.android.com/studio/debug/bug-report</a></p>
|
||||
<p>To get a bugreport directly from your device, do the following:</p>
|
||||
<ul>
|
||||
@ -278,20 +296,34 @@
|
||||
<li>Select the type of bug report you want and tap Report.</li>
|
||||
</ul>
|
||||
<p>After a moment you get a notification that the bug report is ready. To share the bug report, tap the notification.</p>
|
||||
<h1 id="creating-a-low-level-bug-report-on-ios">Creating a low-level bug report on iOS</h1>
|
||||
<p>Some crashes cannot be investigated using Joplin's own tools. In that case, it can be very helpful to provide a native iOS crash report.</p>
|
||||
<h1><a name="creating-a-low-level-bug-report-on-ios" href="#creating-a-low-level-bug-report-on-ios" class="heading-anchor">🔗</a>Creating a low-level bug report on iOS</h1>
|
||||
<p>Some crashes cannot be investigated using Joplin's own tools. In that case, it can be very helpful to provide a native iOS crash report.</p>
|
||||
<p>For this, please follow these instructions:</p>
|
||||
<p>You can send it to this address <a href="https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png">https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png</a></p>
|
||||
<p><a href="https://developer.apple.com/library/content/qa/qa1747/_index.html">https://developer.apple.com/library/content/qa/qa1747/_index.html</a></p>
|
||||
<p>Getting Crash Logs Directly From a Device Without Xcode</p>
|
||||
<p>Your users can retrieve crash reports from their device and send them to you via email by following these instructions.</p>
|
||||
<p>(It is not possible to get device console logs directly from a device)</p>
|
||||
<p>1) Open Settings app</p>
|
||||
<p>2) Go to Privacy, then Diagnostics & Usage</p>
|
||||
<p>3) Select Diagnostics & Usage Data</p>
|
||||
<p>4) Locate the log for the crashed app. The logs will be named in the format: <AppName><em><DateTime></em><DeviceName></p>
|
||||
<p>5) Select the desired log. Then, using the text selection UI select the entire text of the log. Once the text is selected, tap Copy</p>
|
||||
<p>6) Paste the copied text to Mail and send to an email address as desired</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Open Settings app</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Go to Privacy, then Diagnostics & Usage</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Select Diagnostics & Usage Data</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Locate the log for the crashed app. The logs will be named in the format: <AppName><em><DateTime></em><DeviceName></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Select the desired log. Then, using the text selection UI select the entire text of the log. Once the text is selected, tap Copy</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Paste the copied text to Mail and send to an email address as desired</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<script>
|
||||
function stickyHeader() {
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,7 +268,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><img src="https://joplinapp.org/images/DemoDesktop.png" style="max-width: 100%"></p>
|
||||
<img src="https://joplinapp.org/images/DemoDesktop.png" style="max-width: 100%">
|
||||
<p>For general information relevant to all the applications, see also <a href="https://joplinapp.org">Joplin home page</a>.</p>
|
||||
|
||||
<script>
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,20 +268,16 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="support-joplin-development">Support Joplin development</h1>
|
||||
<h1><a name="support-joplin-development" href="#support-joplin-development" class="heading-anchor">🔗</a>Support Joplin development</h1>
|
||||
<p>Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.</p>
|
||||
<p>There are various ways to send a donation:</p>
|
||||
<h2 id="paypal">Paypal</h2>
|
||||
<h2><a name="paypal" href="#paypal" class="heading-anchor">🔗</a>Paypal</h2>
|
||||
<p>To donate via Paypal, please follow this link:</p>
|
||||
<p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=E8JMYD2LQ8MMA&lc=GB&item_name=Joplin+Development¤cy_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="https://joplinapp.org/images/PayPalDonate.png" height="60px"/></a></p>
|
||||
<h2 id="patreon">Patreon</h2>
|
||||
<h2><a name="patreon" href="#patreon" class="heading-anchor">🔗</a>Patreon</h2>
|
||||
<p>Or you may support the project on Patreon:</p>
|
||||
<p><a href="https://www.patreon.com/joplin"><img src="https://joplinapp.org/images/badges/Patreon.png"/></a></p>
|
||||
<h2 id="bitcoin">Bitcoin</h2>
|
||||
<p>Bitcoins are also accepted:</p>
|
||||
<p><a href="bitcoin:1AnbeRd5NZT1ssG93jXzaDoHwzgjQAHX3R?message=Joplin%20development">1AnbeRd5NZT1ssG93jXzaDoHwzgjQAHX3R</a></p>
|
||||
<p><img src="https://joplinapp.org/images/BitcoinQr.png" alt=""></p>
|
||||
<h2 id="other-way-to-support-the-development">Other way to support the development</h2>
|
||||
<h2><a name="other-way-to-support-the-development" href="#other-way-to-support-the-development" class="heading-anchor">🔗</a>Other way to support the development</h2>
|
||||
<p>Finally, there are other ways to support the development of Joplin:</p>
|
||||
<ul>
|
||||
<li>Consider rating the app on <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">Google Play</a> or <a href="https://itunes.apple.com/us/app/joplin/id1315599797">App Store</a>.</li>
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,11 +268,11 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="about-end-to-end-encryption-e2ee-">About End-To-End Encryption (E2EE)</h1>
|
||||
<h1><a name="about-end-to-end-encryption-e2ee-" href="#about-end-to-end-encryption-e2ee-" class="heading-anchor">🔗</a>About End-To-End Encryption (E2EE)</h1>
|
||||
<p>End-to-end encryption (E2EE) is a system where only the owner of the data (i.e. notes, notebooks, tags or resources) can read it. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data.</p>
|
||||
<p>The system is designed to defeat any attempts at surveillance or tampering because no third party can decipher the data being communicated or stored.</p>
|
||||
<p>There is a small overhead to using E2EE since data constantly has to be encrypted and decrypted so consider whether you really need the feature.</p>
|
||||
<h1 id="enabling-e2ee">Enabling E2EE</h1>
|
||||
<h1><a name="enabling-e2ee" href="#enabling-e2ee" class="heading-anchor">🔗</a>Enabling E2EE</h1>
|
||||
<p>Due to the decentralised nature of Joplin, E2EE needs to be manually enabled on all the applications that you synchronise with. It is recommended to first enable it on the desktop or terminal application since they generally run on more powerful devices (unlike the mobile application), and so they can encrypt the initial data faster.</p>
|
||||
<p>To enable it, please follow these steps:</p>
|
||||
<ol>
|
||||
@ -266,9 +284,9 @@
|
||||
<li>Repeat step 5 for each device.</li>
|
||||
</ol>
|
||||
<p>Once all the devices are in sync with E2EE enabled, the encryption/decryption should be mostly transparent. Occasionally you may see encrypted items but they will get decrypted in the background eventually.</p>
|
||||
<h1 id="disabling-e2ee">Disabling E2EE</h1>
|
||||
<h1><a name="disabling-e2ee" href="#disabling-e2ee" class="heading-anchor">🔗</a>Disabling E2EE</h1>
|
||||
<p>Follow the same procedure as above but instead disable E2EE on each device one by one. Again it might be simpler to do it one device at a time and to wait every time for the synchronisation to complete.</p>
|
||||
<h1 id="technical-specification">Technical specification</h1>
|
||||
<h1><a name="technical-specification" href="#technical-specification" class="heading-anchor">🔗</a>Technical specification</h1>
|
||||
<p>For a more technical description, mostly relevant for development or to review the method being used, please see the <a href="https://joplinapp.org/spec/">Encryption specification</a>.</p>
|
||||
|
||||
<script>
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,11 +268,11 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="clicking-edit-in-external-editor-does-nothing-i-want-to-change-the-editor-">Clicking 'Edit in External Editor' does nothing! / I want to change the editor!</h1>
|
||||
<h1><a name="clicking-edit-in-external-editor-does-nothing-i-want-to-change-the-editor-" href="#clicking-edit-in-external-editor-does-nothing-i-want-to-change-the-editor-" class="heading-anchor">🔗</a>Clicking 'Edit in External Editor' does nothing! / I want to change the editor!</h1>
|
||||
<p>The editor command (may include arguments) defines which editor will be used to open a note. If none is provided it will try to auto-detect the default editor. If this does nothing or you want to change it for Joplin, you need to configure it in the Preferences -> Text editor command.</p>
|
||||
<p>Some example configurations are: (comments after #)</p>
|
||||
<p>Linux/Mac:</p>
|
||||
<pre><code class="lang-bash">subl -n # Opens Sublime (subl) in a new window (-n)
|
||||
<pre><code class="language-bash">subl -n # Opens Sublime (subl) in a new window (-n)
|
||||
code -n # Opens Visual Studio Code (code) in a new window (-n)
|
||||
gedit --new-window # Opens gedit (Gnome Text Editor) in a new window
|
||||
xterm -e vim # Opens a new terminal and opens vim. Can be replaced with an
|
||||
@ -263,57 +281,58 @@ xterm -e vim # Opens a new terminal and opens vim. Can be replaced with an
|
||||
open -a <application> # Mac only: opens a GUI application
|
||||
</code></pre>
|
||||
<p>Windows:</p>
|
||||
<pre><code class="lang-bash">subl.exe -n # Opens Sublime (subl) in a new window (-n)
|
||||
<pre><code class="language-bash">subl.exe -n # Opens Sublime (subl) in a new window (-n)
|
||||
code.exe -n # Opens Visual Studio Code in a new window (-n)
|
||||
notepad.exe # Opens Notepad in a new window
|
||||
notepad++.exe --openSession # Opens Notepad ++ in new window
|
||||
</code></pre>
|
||||
<p>Note that the path to directory with your editor executable must exist in your PATH variable (<a href="https://www.computerhope.com/issues/ch000549.htm">Windows</a>, <a href="https://opensource.com/article/17/6/set-path-linux">Linux/Mac</a>) If not, the full path to the executable must be provided.</p>
|
||||
<h1 id="when-i-open-a-note-in-vim-the-cursor-is-not-visible">When I open a note in vim, the cursor is not visible</h1>
|
||||
<h1><a name="when-i-open-a-note-in-vim-the-cursor-is-not-visible" href="#when-i-open-a-note-in-vim-the-cursor-is-not-visible" class="heading-anchor">🔗</a>When I open a note in vim, the cursor is not visible</h1>
|
||||
<p>It seems to be due to the setting <code>set term=ansi</code> in .vimrc. Removing it should fix the issue. See <a href="https://github.com/laurent22/joplin/issues/147">https://github.com/laurent22/joplin/issues/147</a> for more information.</p>
|
||||
<h1 id="all-my-notes-got-deleted-after-changing-the-webdav-url-">All my notes got deleted after changing the WebDAV URL!</h1>
|
||||
<p>When changing the WebDAV URL, make sure that the new location has the same exact content as the old location (i.e. copy all the Joplin data over to the new location). Otherwise, if there's nothing on the new location, Joplin is going to think that you have deleted all your data and will proceed to delete it locally too. So to change the WebDAV URL, please follow these steps:</p>
|
||||
<h1><a name="all-my-notes-got-deleted-after-changing-the-webdav-url-" href="#all-my-notes-got-deleted-after-changing-the-webdav-url-" class="heading-anchor">🔗</a>All my notes got deleted after changing the WebDAV URL!</h1>
|
||||
<p>When changing the WebDAV URL, make sure that the new location has the same exact content as the old location (i.e. copy all the Joplin data over to the new location). Otherwise, if there's nothing on the new location, Joplin is going to think that you have deleted all your data and will proceed to delete it locally too. So to change the WebDAV URL, please follow these steps:</p>
|
||||
<ol>
|
||||
<li>Make a backup of your Joplin data in case something goes wrong. Export to a JEX archive for example.</li>
|
||||
<li>Synchronise one last time all your data from a Joplin client (for example, from the desktop client)</li>
|
||||
<li>Close the Joplin client.</li>
|
||||
<li>On your WebDAV service, copy all the Joplin files from the old location to the new one. Make sure to also copy the <code>.resource</code> directory as it contains your images and other attachments.</li>
|
||||
<li>Once it's done, open Joplin again and change the WebDAV URL.</li>
|
||||
<li>Once it's done, open Joplin again and change the WebDAV URL.</li>
|
||||
<li>Synchronise to verify that everything is working.</li>
|
||||
<li>Do step 5 and 6 for all the other Joplin clients you need to sync.</li>
|
||||
</ol>
|
||||
<h1 id="how-can-i-easily-enter-markdown-tags-in-android-">How can I easily enter Markdown tags in Android?</h1>
|
||||
<h1><a name="how-can-i-easily-enter-markdown-tags-in-android-" href="#how-can-i-easily-enter-markdown-tags-in-android-" class="heading-anchor">🔗</a>How can I easily enter Markdown tags in Android?</h1>
|
||||
<p>You may use a special keyboard such as <a href="https://play.google.com/store/apps/details?id=kl.ime.oh&hl=en">Multiling O Keyboard</a>, which has shortcuts to create Markdown tags. <a href="https://discourse.joplin.cozic.net/t/android-create-new-list-item-with-enter/585/2?u=laurent">More information in this post</a>.</p>
|
||||
<h1 id="the-initial-sync-is-very-slow-how-can-i-speed-it-up-">The initial sync is very slow, how can I speed it up?</h1>
|
||||
<p>Whenever importing a large number of notes, for example from Evernote, it may take a very long time for the first sync to complete. There are various techniques to speed thing up (if you don't want to simply wait for the sync to complete), which are outlined in <a href="https://discourse.joplin.cozic.net/t/workaround-for-slow-initial-bulk-sync-after-evernote-import/746?u=laurent">this post</a>.</p>
|
||||
<h1 id="is-it-possible-to-use-real-file-and-folder-names-in-the-sync-target-">Is it possible to use real file and folder names in the sync target?</h1>
|
||||
<h1><a name="the-initial-sync-is-very-slow-how-can-i-speed-it-up-" href="#the-initial-sync-is-very-slow-how-can-i-speed-it-up-" class="heading-anchor">🔗</a>The initial sync is very slow, how can I speed it up?</h1>
|
||||
<p>Whenever importing a large number of notes, for example from Evernote, it may take a very long time for the first sync to complete. There are various techniques to speed thing up (if you don't want to simply wait for the sync to complete), which are outlined in <a href="https://discourse.joplin.cozic.net/t/workaround-for-slow-initial-bulk-sync-after-evernote-import/746?u=laurent">this post</a>.</p>
|
||||
<h1><a name="is-it-possible-to-use-real-file-and-folder-names-in-the-sync-target-" href="#is-it-possible-to-use-real-file-and-folder-names-in-the-sync-target-" class="heading-anchor">🔗</a>Is it possible to use real file and folder names in the sync target?</h1>
|
||||
<p>Unfortunately it is not possible. Joplin synchronises with file systems using an open format however it does not mean the sync files are meant to be user-editable. The format is designed to be performant and reliable, not user friendly (it cannot be both), and that cannot be changed. Joplin sync directory is basically just a database.</p>
|
||||
<h1 id="could-there-be-a-pin-or-password-to-restrict-access-to-joplin-">Could there be a PIN or password to restrict access to Joplin?</h1>
|
||||
<h1><a name="could-there-be-a-pin-or-password-to-restrict-access-to-joplin-" href="#could-there-be-a-pin-or-password-to-restrict-access-to-joplin-" class="heading-anchor">🔗</a>Could there be a PIN or password to restrict access to Joplin?</h1>
|
||||
<p>Short answer: no. The end to end encryption that Joplin implements is to protect the data during transmission and on the cloud service so that only you can access it.</p>
|
||||
<p>On the local device it is assumed that the data is safe due to the OS built-in security features. If additional security is needed it's always possible to put the notes on an encrypted Truecrypt drive for instance.</p>
|
||||
<p>If someone that you don't trust has access to the computer, they can put a keylogger anyway so any local encryption or PIN access would not be useful.</p>
|
||||
<h1 id="webdav-synchronisation-is-not-working">WebDAV synchronisation is not working</h1>
|
||||
<h2 id="-forbidden-error-in-strato">"Forbidden" error in Strato</h2>
|
||||
<p>On the local device it is assumed that the data is safe due to the OS built-in security features. If additional security is needed it's always possible to put the notes on an encrypted Truecrypt drive for instance.</p>
|
||||
<p>If someone that you don't trust has access to the computer, they can put a keylogger anyway so any local encryption or PIN access would not be useful.</p>
|
||||
<h1><a name="webdav-synchronisation-is-not-working" href="#webdav-synchronisation-is-not-working" class="heading-anchor">🔗</a>WebDAV synchronisation is not working</h1>
|
||||
<h2><a name="forbidden-error-in-strato" href="#forbidden-error-in-strato" class="heading-anchor">🔗</a>"Forbidden" error in Strato</h2>
|
||||
<p>For example:</p>
|
||||
<pre><code>MKCOL .sync/: Unknown error 2 (403): <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
||||
<html><head>
|
||||
<title>403 Forbidden</title>
|
||||
</head><body>
|
||||
<h1>Forbidden</h1>
|
||||
<p>You don't have permission to access /.sync/
|
||||
<p>You don't have permission to access /.sync/
|
||||
on this server.</p>
|
||||
</body></html>
|
||||
</code></pre><p>In this case, <a href="https://github.com/laurent22/joplin/issues/309">make sure you enter the correct WebDAV URL</a>.</p>
|
||||
<h2 id="nextcloud-sync-is-not-working">Nextcloud sync is not working</h2>
|
||||
</code></pre>
|
||||
<p>In this case, <a href="https://github.com/laurent22/joplin/issues/309">make sure you enter the correct WebDAV URL</a>.</p>
|
||||
<h2><a name="nextcloud-sync-is-not-working" href="#nextcloud-sync-is-not-working" class="heading-anchor">🔗</a>Nextcloud sync is not working</h2>
|
||||
<ul>
|
||||
<li>Check your username and password. <strong>Type it manually</strong> (without copying and pasting it) and try again.</li>
|
||||
<li>Check the WebDAV URL - to get the correct URL, go to Nextcloud and, in the left sidebar, click on "Settings" and copy the WebDAV URL from there. <strong>Do not forget to add the folder you've created to that URL</strong>. For example, if the base the WebDAV URL is "<a href="https://example.com/nextcloud/remote.php/webdav/">https://example.com/nextcloud/remote.php/webdav/</a>" and you want the notes to be synced in the "Joplin" directory, you need to give the URL "<a href="https://example.com/nextcloud/remote.php/webdav/Joplin">https://example.com/nextcloud/remote.php/webdav/Joplin</a>" <strong>and you need to create the "Joplin" directory yourself</strong>.</li>
|
||||
<li>Check the WebDAV URL - to get the correct URL, go to Nextcloud and, in the left sidebar, click on "Settings" and copy the WebDAV URL from there. <strong>Do not forget to add the folder you've created to that URL</strong>. For example, if the base the WebDAV URL is "<a href="https://example.com/nextcloud/remote.php/webdav/">https://example.com/nextcloud/remote.php/webdav/</a>" and you want the notes to be synced in the "Joplin" directory, you need to give the URL "<a href="https://example.com/nextcloud/remote.php/webdav/Joplin">https://example.com/nextcloud/remote.php/webdav/Joplin</a>" <strong>and you need to create the "Joplin" directory yourself</strong>.</li>
|
||||
<li>Did you enable <strong>2FA</strong> (Multi-factor authentication) on Nextcloud? In that case, you need to <a href="https://github.com/laurent22/joplin/issues/1453#issuecomment-486640902">create a one-time password for Joplin on the Nextcloud admin interface</a>.</li>
|
||||
</ul>
|
||||
<h1 id="could-you-publish-joplin-on-f-droid-">Could you publish Joplin on F-droid?</h1>
|
||||
<h1><a name="could-you-publish-joplin-on-f-droid-" href="#could-you-publish-joplin-on-f-droid-" class="heading-anchor">🔗</a>Could you publish Joplin on F-droid?</h1>
|
||||
<p>Joplin relies on Firebase to enable reliable notifications on Android. Since F-Droid <a href="https://gitlab.com/fdroid/rfp/issues/434#note_55239154">do not accept applications that depend on this package</a>, it is not currently possible to have Joplin in that repository. To avoid using Google Play, you have the option to directly download the Joplin APK file.</p>
|
||||
<h1 id="why-is-it-named-joplin-">Why is it named Joplin?</h1>
|
||||
<p>The name comes from the composer and pianist <a href="https://en.wikipedia.org/wiki/Scott_Joplin">Scott Joplin</a>, which I often listen to. His name is also easy to remember and type so it fell like a good choice. And, to quote a user on Hacker News, "though Scott Joplin's ragtime musical style has a lot in common with some very informal music, his own approach was more educated, sophisticated, and precise. Every note was in its place for a reason, and he was known to prefer his pieces to be performed exactly as written. So you could say that compared to the people who came before him, his notes were more organized".</p>
|
||||
<h1><a name="why-is-it-named-joplin-" href="#why-is-it-named-joplin-" class="heading-anchor">🔗</a>Why is it named Joplin?</h1>
|
||||
<p>The name comes from the composer and pianist <a href="https://en.wikipedia.org/wiki/Scott_Joplin">Scott Joplin</a>, which I often listen to. His name is also easy to remember and type so it fell like a good choice. And, to quote a user on Hacker News, "though Scott Joplin's ragtime musical style has a lot in common with some very informal music, his own approach was more educated, sophisticated, and precise. Every note was in its place for a reason, and he was known to prefer his pieces to be performed exactly as written. So you could say that compared to the people who came before him, his notes were more organized".</p>
|
||||
|
||||
<script>
|
||||
function stickyHeader() {
|
||||
|
176
docs/index.html
176
docs/index.html
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,16 +268,15 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=E8JMYD2LQ8MMA&lc=GB&item_name=Joplin+Development&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="https://joplinapp.org/images/badges/Donate-PayPal-green.svg" alt="Donate"></a> <a href="https://www.patreon.com/joplin"><img src="https://joplinapp.org/images/badges/Patreon-Badge.svg" alt="Become a patron"></a> <a href="https://travis-ci.org/laurent22/joplin"><img src="https://travis-ci.org/laurent22/joplin.svg?branch=master" alt="Travis Build Status"></a> <a href="https://ci.appveyor.com/project/laurent22/joplin"><img src="https://ci.appveyor.com/api/projects/status/github/laurent22/joplin?branch=master&passingText=master%20-%20OK&svg=true" alt="Appveyor Build Status"></a></p>
|
||||
<p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=E8JMYD2LQ8MMA&lc=GB&item_name=Joplin+Development&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="https://joplinapp.org/images/badges/Donate-PayPal-green.svg" alt="Donate"></a> <a href="https://www.patreon.com/joplin"><img src="https://joplinapp.org/images/badges/Patreon-Badge.svg" alt="Become a patron"></a></p>
|
||||
<p>Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in <a href="#markdown">Markdown format</a>.</p>
|
||||
<p>Notes exported from Evernote via .enex files <a href="#importing">can be imported</a> into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.</p>
|
||||
<p>The notes can be <a href="#synchronisation">synchronised</a> with various cloud services including <a href="https://nextcloud.com/">Nextcloud</a>, Dropbox, OneDrive, WebDAV or the file system (for example with a network directory). When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.</p>
|
||||
<p>The application is available for Windows, Linux, macOS, Android and iOS. A <a href="https://github.com/laurent22/joplin/blob/master/readme/clipper.md">Web Clipper</a>, to save web pages and screenshots from your browser, is also available for <a href="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/">Firefox</a> and <a href="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB">Chrome</a>.</p>
|
||||
<div class="top-screenshot"><img src="https://joplinapp.org/images/AllClients.jpg" style="max-width: 100%; max-height: 35em;"></div>
|
||||
|
||||
<h1 id="installation">Installation</h1>
|
||||
<h1><a name="installation" href="#installation" class="heading-anchor">🔗</a>Installation</h1>
|
||||
<p>Three types of applications are available: for the <strong>desktop</strong> (Windows, macOS and Linux), for <strong>mobile</strong> (Android and iOS) and for <strong>terminal</strong> (Windows, macOS and Linux). All applications have similar user interfaces and can synchronise with each other.</p>
|
||||
<h2 id="desktop-applications">Desktop applications</h2>
|
||||
<h2><a name="desktop-applications" href="#desktop-applications" class="heading-anchor">🔗</a>Desktop applications</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -271,26 +288,22 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Windows (32 and 64-bit)</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.143/Joplin-Setup-1.0.143.exe'><img alt='Get it on Windows' height="40px" src='https://joplinapp.org/images/BadgeWindows.png'/></a></td>
|
||||
<td>or Get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.143/JoplinPortable.exe'>Portable version</a><br>(to run from a USB key, etc.)</td>
|
||||
<td><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></td>
|
||||
<td>Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.143/JoplinPortable.exe'>Portable version</a><br><br>The <a href="https://en.wikipedia.org/wiki/Portable_application">portable application</a> 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.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>macOS</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.143/Joplin-1.0.143.dmg'><img alt='Get it on macOS' height="40px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a></td>
|
||||
<td></td>
|
||||
<td><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></td>
|
||||
<td>You can also use Homebrew: <code>brew cask install joplin</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux</td>
|
||||
<td><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' height="40px" src='https://joplinapp.org/images/BadgeLinux.png'/></a></td>
|
||||
<td>An Arch Linux package<br><a href="#terminal-application">is also available</a>.</td>
|
||||
<td><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></td>
|
||||
<td>An Arch Linux package <a href="#terminal-application">is also available</a>.<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> <code>wget -O - https://raw.githubusercontent.com/laurent22/joplin/master/Joplin_install_and_update.sh | bash</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>The <a href="https://en.wikipedia.org/wiki/Portable_application">portable application</a> 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.</p>
|
||||
<p>On Linux, 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:</p>
|
||||
<pre><code class="lang-sh">wget -O - https://raw.githubusercontent.com/laurent22/joplin/master/Joplin_install_and_update.sh | bash
|
||||
</code></pre>
|
||||
<h2 id="mobile-applications">Mobile applications</h2>
|
||||
<h2><a name="mobile-applications" href="#mobile-applications" class="heading-anchor">🔗</a>Mobile applications</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -303,7 +316,7 @@
|
||||
<tr>
|
||||
<td>Android</td>
|
||||
<td><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></td>
|
||||
<td>or <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.0.243/joplin-v1.0.243.apk">Download APK File</a></td>
|
||||
<td>or <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.0.246/joplin-v1.0.246.apk">Download APK File</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>iOS</td>
|
||||
@ -312,7 +325,7 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="terminal-application">Terminal application</h2>
|
||||
<h2><a name="terminal-application" href="#terminal-application" class="heading-anchor">🔗</a>Terminal application</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -337,9 +350,9 @@
|
||||
</table>
|
||||
<p>To start it, type <code>joplin</code>.</p>
|
||||
<p>For usage information, please refer to the full <a href="https://joplinapp.org/terminal/">Joplin Terminal Application Documentation</a>.</p>
|
||||
<h2 id="web-clipper">Web Clipper</h2>
|
||||
<h2><a name="web-clipper" href="#web-clipper" class="heading-anchor">🔗</a>Web Clipper</h2>
|
||||
<p>The Web Clipper is a browser extension that allows you to save web pages and screenshots from your browser. For more information on how to install and use it, see the <a href="https://github.com/laurent22/joplin/blob/master/readme/clipper.md">Web Clipper Help Page</a>.</p>
|
||||
<h1 id="features">Features</h1>
|
||||
<h1><a name="features" href="#features" class="heading-anchor">🔗</a>Features</h1>
|
||||
<ul>
|
||||
<li>Desktop, mobile and terminal applications.</li>
|
||||
<li><a href="https://github.com/laurent22/joplin/blob/master/readme/clipper.md">Web Clipper</a> for Firefox and Chrome.</li>
|
||||
@ -358,23 +371,25 @@
|
||||
<li>Supports multiple languages</li>
|
||||
<li>External editor support - open notes in your favorite external editor with one click in Joplin.</li>
|
||||
</ul>
|
||||
<h1 id="importing">Importing</h1>
|
||||
<h2 id="importing-from-evernote">Importing from Evernote</h2>
|
||||
<h1><a name="importing" href="#importing" class="heading-anchor">🔗</a>Importing</h1>
|
||||
<h2><a name="importing-from-evernote" href="#importing-from-evernote" class="heading-anchor">🔗</a>Importing from Evernote</h2>
|
||||
<p>Joplin was designed as a replacement for Evernote and so can import complete Evernote notebooks, as well as notes, tags, resources (attached files) and note metadata (such as author, geo-location, etc.) via ENEX files. In terms of data, the only two things that might slightly differ are:</p>
|
||||
<ul>
|
||||
<li><p>Recognition data - Evernote images, in particular scanned (or photographed) documents have <a href="https://en.wikipedia.org/wiki/Optical_character_recognition">recognition data</a> associated with them. It is the text that Evernote has been able to recognise in the document. This data is not preserved when the note are imported into Joplin. However, should it become supported in the search tool or other parts of Joplin, it should be possible to regenerate this recognition data since the actual image would still be available.</p>
|
||||
<li>
|
||||
<p>Recognition data - Evernote images, in particular scanned (or photographed) documents have <a href="https://en.wikipedia.org/wiki/Optical_character_recognition">recognition data</a> associated with them. It is the text that Evernote has been able to recognise in the document. This data is not preserved when the note are imported into Joplin. However, should it become supported in the search tool or other parts of Joplin, it should be possible to regenerate this recognition data since the actual image would still be available.</p>
|
||||
</li>
|
||||
<li><p>Colour, font sizes and faces - Evernote text is stored as HTML and this is converted to Markdown during the import process. For notes that are mostly plain text or with basic formatting (bold, italic, bullet points, links, etc.) this is a lossless conversion, and the note, once rendered back to HTML should be very similar. Tables are also imported and converted to Markdown tables. For very complex notes, some formatting data might be lost - in particular colours, font sizes and font faces will not be imported. The text itself however is always imported in full regardless of formatting.</p>
|
||||
<li>
|
||||
<p>Colour, font sizes and faces - Evernote text is stored as HTML and this is converted to Markdown during the import process. For notes that are mostly plain text or with basic formatting (bold, italic, bullet points, links, etc.) this is a lossless conversion, and the note, once rendered back to HTML should be very similar. Tables are also imported and converted to Markdown tables. For very complex notes, some formatting data might be lost - in particular colours, font sizes and font faces will not be imported. The text itself however is always imported in full regardless of formatting.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>To import Evernote data, first export your Evernote notebooks to ENEX files as described <a href="https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks">here</a>. Then follow these steps:</p>
|
||||
<p>On the <strong>desktop application</strong>, open File > Import > ENEX and select your file. The notes will be imported into a new separate notebook. If needed they can then be moved to a different notebook, or the notebook can be renamed, etc.</p>
|
||||
<p>On the <strong>terminal application</strong>, in <a href="https://joplinapp.org/terminal/#command-line-mode">command-line mode</a>, type <code>import /path/to/file.enex</code>. This will import the notes into a new notebook named after the filename.</p>
|
||||
<h2 id="importing-from-markdown-files">Importing from Markdown files</h2>
|
||||
<h2><a name="importing-from-markdown-files" href="#importing-from-markdown-files" class="heading-anchor">🔗</a>Importing from Markdown files</h2>
|
||||
<p>Joplin can import notes from plain Markdown file. You can either import a complete directory of Markdown files or individual files.</p>
|
||||
<p>On the <strong>desktop application</strong>, open File > Import > MD and select your Markdown file or directory.</p>
|
||||
<p>On the <strong>terminal application</strong>, in <a href="https://joplinapp.org/terminal/#command-line-mode">command-line mode</a>, type <code>import --format md /path/to/file.md</code> or <code>import --format md /path/to/directory/</code>.</p>
|
||||
<h2 id="importing-from-other-applications">Importing from other applications</h2>
|
||||
<h2><a name="importing-from-other-applications" href="#importing-from-other-applications" class="heading-anchor">🔗</a>Importing from other applications</h2>
|
||||
<p>In general the way to import notes from any application into Joplin is to convert the notes to ENEX files (Evernote format) and to import these ENEX files into Joplin using the method above. Most note-taking applications support ENEX files so it should be relatively straightforward. For help about specific applications, see below:</p>
|
||||
<ul>
|
||||
<li>Standard Notes: Please see <a href="https://programadorwebvalencia.com/migrate-notes-from-standard-notes-to-joplin/">this tutorial</a></li>
|
||||
@ -382,26 +397,28 @@
|
||||
<li>OneNote: First <a href="https://discussion.evernote.com/topic/107736-is-there-a-way-to-import-from-onenote-into-evernote-on-the-mac/">import the notes from OneNote into Evernote</a>. Then export the ENEX file from Evernote and import it into Joplin.</li>
|
||||
<li>NixNote: Synchronise with Evernote, then export the ENEX files and import them into Joplin. More info <a href="https://discourse.joplin.cozic.net/t/import-from-nixnote/183/3">in this thread</a>.</li>
|
||||
</ul>
|
||||
<h1 id="exporting">Exporting</h1>
|
||||
<h1><a name="exporting" href="#exporting" class="heading-anchor">🔗</a>Exporting</h1>
|
||||
<p>Joplin can export to the JEX format (Joplin Export file), which is a tar file that can contain multiple notes, notebooks, etc. This is a lossless format in that all the notes, but also metadata such as geo-location, updated time, tags, etc. are preserved. This format is convenient for backup purposes and can be re-imported into Joplin. A "raw" format is also available. This is the same as the JEX format except that the data is saved to a directory and each item represented by a single file.</p>
|
||||
<h1 id="synchronisation">Synchronisation</h1>
|
||||
<h1><a name="synchronisation" href="#synchronisation" class="heading-anchor">🔗</a>Synchronisation</h1>
|
||||
<p>One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.</p>
|
||||
<p>Currently, synchronisation is possible with Nextcloud, Dropbox (by default), OneDrive or the local filesystem. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.</p>
|
||||
<h2 id="nextcloud-synchronisation">Nextcloud synchronisation</h2>
|
||||
<p><img src="https://joplinapp.org/images/nextcloud-logo-background.png" width="100" align="left"> <a href="https://nextcloud.com/">Nextcloud</a> is a self-hosted, private cloud solution. It can store documents, images and videos but also calendars, passwords and countless other things and can sync them to your laptop or phone. As you can host your own Nextcloud server, you own both the data on your device and infrastructure used for synchronisation. As such it is a good fit for Joplin. The platform is also well supported and with a strong community, so it is likely to be around for a while - since it's open source anyway, it is not a service that can be closed, it can exist on a server for as long as one chooses.</p>
|
||||
<h2><a name="nextcloud-synchronisation" href="#nextcloud-synchronisation" class="heading-anchor">🔗</a>Nextcloud synchronisation</h2>
|
||||
<p><img src="https://joplinapp.org/images/nextcloud-logo-background.png" width="100" align="left"> <a href="https://nextcloud.com/">Nextcloud</a> is a self-hosted, private cloud solution. It can store documents, images and videos but also calendars, passwords and countless other things and can sync them to your laptop or phone. As you can host your own Nextcloud server, you own both the data on your device and infrastructure used for synchronisation. As such it is a good fit for Joplin. The platform is also well supported and with a strong community, so it is likely to be around for a while - since it's open source anyway, it is not a service that can be closed, it can exist on a server for as long as one chooses.</p>
|
||||
<p>On the <strong>desktop application</strong> or <strong>mobile application</strong>, go to the config screen and select Nextcloud as the synchronisation target. Then input the WebDAV URL (to get it, click on Settings in the bottom left corner of the page, in Nextcloud), this is normally <code>https://example.com/nextcloud/remote.php/webdav/Joplin</code> (<strong>make sure to create the "Joplin" directory in Nextcloud</strong>), and set the username and password. If it does not work, please <a href="https://github.com/laurent22/joplin/issues/61#issuecomment-373282608">see this explanation</a> for more details.</p>
|
||||
<p>On the <strong>terminal application</strong>, you will need to set the <code>sync.target</code> config variable and all the <code>sync.5.path</code>, <code>sync.5.username</code> and <code>sync.5.password</code> config variables to, respectively the Nextcloud WebDAV URL, your username and your password. This can be done from the command line mode using:</p>
|
||||
<pre><code>:config sync.5.path https://example.com/nextcloud/remote.php/webdav/Joplin
|
||||
:config sync.5.username YOUR_USERNAME
|
||||
:config sync.5.password YOUR_PASSWORD
|
||||
:config sync.target 5
|
||||
</code></pre><p>If synchronisation does not work, please consult the logs in the app profile directory - it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.</p>
|
||||
<h2 id="dropbox-synchronisation">Dropbox synchronisation</h2>
|
||||
</code></pre>
|
||||
<p>If synchronisation does not work, please consult the logs in the app profile directory - it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.</p>
|
||||
<h2><a name="dropbox-synchronisation" href="#dropbox-synchronisation" class="heading-anchor">🔗</a>Dropbox synchronisation</h2>
|
||||
<p>When syncing with Dropbox, Joplin creates a sub-directory in Dropbox, in <code>/Apps/Joplin</code> and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.</p>
|
||||
<p>On the <strong>desktop application</strong> or <strong>mobile application</strong>, select "Dropbox" as the synchronisation target in the config screen (it is selected by default). Then, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar and follow the instructions.</p>
|
||||
<p>On the <strong>terminal application</strong>, to initiate the synchronisation process, type <code>:sync</code>. You will be asked to follow a link to authorise the application. It is possible to also synchronise outside of the user interface by typing <code>joplin sync</code> from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:</p>
|
||||
<pre><code>*/30 * * * * /path/to/joplin sync
|
||||
</code></pre><h2 id="webdav-synchronisation">WebDAV synchronisation</h2>
|
||||
</code></pre>
|
||||
<h2><a name="webdav-synchronisation" href="#webdav-synchronisation" class="heading-anchor">🔗</a>WebDAV synchronisation</h2>
|
||||
<p>Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.</p>
|
||||
<p>WebDAV-compatible services that are known to work with Joplin:</p>
|
||||
<ul>
|
||||
@ -418,21 +435,21 @@
|
||||
<li><a href="https://www.schimera.com/products/webdav-nav-server/">WebDAV Nav</a>, a macOS server.</li>
|
||||
<li><a href="https://www.zimbra.com/">Zimbra</a></li>
|
||||
</ul>
|
||||
<h2 id="onedrive-synchronisation">OneDrive synchronisation</h2>
|
||||
<h2><a name="onedrive-synchronisation" href="#onedrive-synchronisation" class="heading-anchor">🔗</a>OneDrive synchronisation</h2>
|
||||
<p>When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.</p>
|
||||
<p>On the <strong>desktop application</strong> or <strong>mobile application</strong>, select "OneDrive" as the synchronisation target in the config screen. Then, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar and follow the instructions.</p>
|
||||
<p>On the <strong>terminal application</strong>, to initiate the synchronisation process, type <code>:sync</code>. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive).</p>
|
||||
<h1 id="encryption">Encryption</h1>
|
||||
<h1><a name="encryption" href="#encryption" class="heading-anchor">🔗</a>Encryption</h1>
|
||||
<p>Joplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a system where only the owner of the notes, notebooks, tags or resources can read them. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data. Please see the <a href="https://joplinapp.org/e2ee/">End-To-End Encryption Tutorial</a> for more information about this feature and how to enable it.</p>
|
||||
<p>For a more technical description, mostly relevant for development or to review the method being used, please see the <a href="https://joplinapp.org/spec/">Encryption specification</a>.</p>
|
||||
<h1 id="external-text-editor">External text editor</h1>
|
||||
<h1><a name="external-text-editor" href="#external-text-editor" class="heading-anchor">🔗</a>External text editor</h1>
|
||||
<p>Joplin notes can be opened and edited using an external editor of your choice. It can be a simple text editor like Notepad++ or Sublime Text or an actual Markdown editor like Typora. In that case, images will also be displayed within the editor. To open the note in an external editor, click on the icon in the toolbar or press Ctrl+E (or Cmd+E). Your default text editor will be used to open the note. If needed, you can also specify the editor directly in the General Options, under "Text editor command".</p>
|
||||
<h1 id="attachments-resources">Attachments / Resources</h1>
|
||||
<h1><a name="attachments-resources" href="#attachments-resources" class="heading-anchor">🔗</a>Attachments / Resources</h1>
|
||||
<p>Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.</p>
|
||||
<p>On the <strong>desktop application</strong>, images can be attached either by clicking on "Attach file" or by pasting (with Ctrl+V) an image directly in the editor, or by drag and dropping an image.</p>
|
||||
<p>Resources that are not attached to any note will be automatically deleted after 10 days (see <a href="https://github.com/laurent22/joplin/issues/154#issuecomment-356582366">rationale</a>).</p>
|
||||
<p><strong>Important:</strong> Resources larger than 10 MB are not currently supported on mobile. They will crash the application when synchronising so it is recommended not to attach such resources at the moment. The issue is being looked at.</p>
|
||||
<h1 id="notifications">Notifications</h1>
|
||||
<h1><a name="notifications" href="#notifications" class="heading-anchor">🔗</a>Notifications</h1>
|
||||
<p>On the desktop and mobile apps, an alarm can be associated with any to-do. It will be triggered at the given time by displaying a notification. How the notification will be displayed depends on the operating system since each has a different way to handle this. Please see below for the requirements for the desktop applications:</p>
|
||||
<ul>
|
||||
<li><strong>Windows</strong>: >= 8. Make sure the Action Center is enabled on Windows. Task bar balloon for Windows < 8. Growl as fallback. Growl takes precedence over Windows balloons.</li>
|
||||
@ -442,7 +459,7 @@
|
||||
<p>See <a href="https://github.com/mikaelbr/node-notifier/blob/master/DECISION_FLOW.md">documentation and flow chart for reporter choice</a></p>
|
||||
<p>On mobile, the alarms will be displayed using the built-in notification system.</p>
|
||||
<p>If for any reason the notifications do not work, please <a href="https://github.com/laurent22/joplin/issues">open an issue</a>.</p>
|
||||
<h1 id="sub-notebooks">Sub-notebooks</h1>
|
||||
<h1><a name="sub-notebooks" href="#sub-notebooks" class="heading-anchor">🔗</a>Sub-notebooks</h1>
|
||||
<p>Sub-notebooks allow organising multiple notebooks into a tree of notebooks. For example it can be used to regroup all the notebooks related to work, to family or to a particular project under a parent notebook.</p>
|
||||
<p><img src="https://joplinapp.org/images/SubNotebooks.png" alt=""></p>
|
||||
<ul>
|
||||
@ -450,14 +467,15 @@
|
||||
<li>The <strong>mobile application</strong> supports displaying and collapsing/expanding the tree of notebooks, however it does not currently support moving the subnotebooks to different notebooks.</li>
|
||||
<li>The <strong>terminal app</strong> supports displaying the tree of subnotebooks but it does not support collapsing/expanding them or moving the subnotebooks around.</li>
|
||||
</ul>
|
||||
<h1 id="markdown">Markdown</h1>
|
||||
<h1><a name="markdown" href="#markdown" class="heading-anchor">🔗</a>Markdown</h1>
|
||||
<p>Joplin uses and renders <a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet">Github-flavoured Markdown</a> with a few variations and additions. In particular:</p>
|
||||
<h2 id="links-to-other-notes">Links to other notes</h2>
|
||||
<h2><a name="links-to-other-notes" href="#links-to-other-notes" class="heading-anchor">🔗</a>Links to other notes</h2>
|
||||
<p>You can create a link to a note by specifying its ID in the URL. For example:</p>
|
||||
<pre><code>[Link to my note](:/0b0d62d15e60409dac34f354b6e9e839)
|
||||
</code></pre><p>Since getting the ID of a note is not straightforward, each app provides a way to create such link. In the <strong>desktop app</strong>, right click on a note an select "Copy Markdown link". In the <strong>mobile app</strong>, open a note and, in the top right menu, select "Copy Markdown link". You can then paste this link anywhere in another note.</p>
|
||||
<h2 id="plugins">Plugins</h2>
|
||||
<p>Joplin supports a number of plugins that can be toggled on top the standard markdown features you would expect. These toggle-able plugins are listed below. Note: not all of the plugins are enabled by default, if the enable field is 'no' below, then enter Tools->General Options to enable the plugin. Plugins can be disabled in the same manner.</p>
|
||||
</code></pre>
|
||||
<p>Since getting the ID of a note is not straightforward, each app provides a way to create such link. In the <strong>desktop app</strong>, right click on a note an select "Copy Markdown link". In the <strong>mobile app</strong>, open a note and, in the top right menu, select "Copy Markdown link". You can then paste this link anywhere in another note.</p>
|
||||
<h2><a name="plugins" href="#plugins" class="heading-anchor">🔗</a>Plugins</h2>
|
||||
<p>Joplin supports a number of plugins that can be toggled on top the standard markdown features you would expect. These toggle-able plugins are listed below. Note: not all of the plugins are enabled by default, if the enable field is 'no' below, then enter Tools->General Options to enable the plugin. Plugins can be disabled in the same manner.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -482,7 +500,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/markdown-it/markdown-it-footnote">Footnote</a></td>
|
||||
<td><code>Simples inline footnote ^[I'm inline!]</code></td>
|
||||
<td><code>Simples inline footnote ^[I'm inline!]</code></td>
|
||||
<td>See <a href="https://github.com/markdown-it/markdown-it-footnote">plugin page</a> for full description</td>
|
||||
<td>yes</td>
|
||||
</tr>
|
||||
@ -536,31 +554,35 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="math-notation">Math notation</h2>
|
||||
<h2><a name="math-notation" href="#math-notation" class="heading-anchor">🔗</a>Math notation</h2>
|
||||
<p>Math expressions can be added using the <a href="https://khan.github.io/KaTeX/">KaTeX notation</a>. To add an inline equation, wrap the expression in <code>$EXPRESSION$</code>, eg. <code>$\sqrt{3x-1}+(1+x)^2$</code>. To create an expression block, wrap it as follow:</p>
|
||||
<pre><code>$$
|
||||
EXPRESSION
|
||||
$$
|
||||
</code></pre><p>For example:</p>
|
||||
</code></pre>
|
||||
<p>For example:</p>
|
||||
<pre><code>$$
|
||||
f(x) = \int_{-\infty}^\infty
|
||||
\hat f(\xi)\,e^{2 \pi i \xi x}
|
||||
\,d\xi
|
||||
\hat f(\xi)\,e^{2 \pi i \xi x}
|
||||
\,d\xi
|
||||
$$
|
||||
</code></pre><p>Here is an example with the Markdown and rendered result side by side:</p>
|
||||
<p><img src="https://joplinapp.org/images/Katex.png" height="400px"></p>
|
||||
<h2 id="checkboxes">Checkboxes</h2>
|
||||
</code></pre>
|
||||
<p>Here is an example with the Markdown and rendered result side by side:</p>
|
||||
<img src="https://joplinapp.org/images/Katex.png" height="400px">
|
||||
<h2><a name="checkboxes" href="#checkboxes" class="heading-anchor">🔗</a>Checkboxes</h2>
|
||||
<p>Checkboxes can be added like so:</p>
|
||||
<pre><code>- [ ] Milk
|
||||
- [ ] Rice
|
||||
- [ ] Eggs
|
||||
</code></pre><p>The checkboxes can then be ticked in the mobile and desktop applications.</p>
|
||||
<h2 id="html-support">HTML support</h2>
|
||||
<p>It is generally recommended to enter the notes as Markdown as it makes the notes easier to edit. However for cases where certain features aren't supported (such as strikethrough or to highlight text), you can also use HTML code directly. For example this would be a valid note:</p>
|
||||
</code></pre>
|
||||
<p>The checkboxes can then be ticked in the mobile and desktop applications.</p>
|
||||
<h2><a name="html-support" href="#html-support" class="heading-anchor">🔗</a>HTML support</h2>
|
||||
<p>It is generally recommended to enter the notes as Markdown as it makes the notes easier to edit. However for cases where certain features aren't supported (such as strikethrough or to highlight text), you can also use HTML code directly. For example this would be a valid note:</p>
|
||||
<pre><code>This is <s>strikethrough text</s> mixed with regular **Markdown**.
|
||||
</code></pre><h2 id="custom-css">Custom CSS</h2>
|
||||
<p>Rendered markdown can be customized by placing a userstyle file in the profile directory <code>~/.config/joplin-desktop/userstyle.css</code> (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, <strong>not when printing or exporting to PDF</strong>. 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.</p>
|
||||
<h1 id="searching">Searching</h1>
|
||||
</code></pre>
|
||||
<h2><a name="custom-css" href="#custom-css" class="heading-anchor">🔗</a>Custom CSS</h2>
|
||||
<p>Rendered markdown can be customized by placing a userstyle file in the profile directory <code>~/.config/joplin-desktop/userstyle.css</code> (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 <em><strong>must</strong></em> 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, <strong>not when printing or exporting to PDF</strong>. 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.</p>
|
||||
<h1><a name="searching" href="#searching" class="heading-anchor">🔗</a>Searching</h1>
|
||||
<p>Joplin implements the SQLite Full Text Search (FTS4) extension. It means the content of all the notes is indexed in real time and search queries return results very fast. Both <a href="https://www.sqlite.org/fts3.html#simple_fts_queries">Simple FTS Queries</a> and <a href="https://www.sqlite.org/fts3.html#full_text_index_queries">Full-Text Index Queries</a> are supported. See below for the list of supported queries:</p>
|
||||
<table>
|
||||
<thead>
|
||||
@ -599,19 +621,19 @@ $$
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Notes are sorted by "relevance". Currently it means the notes that contain the requested terms the most times are on top. For queries with multiple terms, it also matter how close to each others are the terms. This is a bit experimental so if you notice a search query that returns unexpected results, please report it in the forum, providing as much details as possible to replicate the issue.</p>
|
||||
<h1 id="donations">Donations</h1>
|
||||
<h1><a name="donations" href="#donations" class="heading-anchor">🔗</a>Donations</h1>
|
||||
<p>Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.</p>
|
||||
<p>Please see the <a href="https://joplinapp.org/donate/">donation page</a> for information on how to support the development of Joplin.</p>
|
||||
<h1 id="community">Community</h1>
|
||||
<h1><a name="community" href="#community" class="heading-anchor">🔗</a>Community</h1>
|
||||
<ul>
|
||||
<li>For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the <a href="https://discourse.joplin.cozic.net/">Joplin Forum</a>. It is possible to login with your GitHub account.</li>
|
||||
<li>Also see here for information about <a href="https://discourse.joplin.cozic.net/c/news">the latest releases and general news</a>.</li>
|
||||
<li>For bug reports and feature requests, go to the <a href="https://github.com/laurent22/joplin/issues">GitHub Issue Tracker</a>.</li>
|
||||
<li>The latest news are posted <a href="https://www.patreon.com/joplin">on the Patreon page</a>.</li>
|
||||
</ul>
|
||||
<h1 id="contributing">Contributing</h1>
|
||||
<h1><a name="contributing" href="#contributing" class="heading-anchor">🔗</a>Contributing</h1>
|
||||
<p>Please see the guide for information on how to contribute to the development of Joplin: <a href="https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md">https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md</a></p>
|
||||
<h1 id="localisation">Localisation</h1>
|
||||
<h1><a name="localisation" href="#localisation" class="heading-anchor">🔗</a>Localisation</h1>
|
||||
<p>Joplin is currently available in the languages below. If you would like to contribute a <strong>new translation</strong>, it is quite straightforward, please follow these steps:</p>
|
||||
<ul>
|
||||
<li><a href="https://poedit.net/">Download Poedit</a>, the translation editor, and install it.</li>
|
||||
@ -626,7 +648,7 @@ $$
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
<th>Language</th>
|
||||
<th>Po File</th>
|
||||
<th>Last translator</th>
|
||||
@ -638,7 +660,7 @@ $$
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" alt=""></td>
|
||||
<td>Arabic</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ar.po">ar</a></td>
|
||||
<td>عبد الناصر سعيد (as@althobaity.com)</td>
|
||||
<td>عبد الناصر سعيد (<a href="mailto:as@althobaity.com">as@althobaity.com</a>)</td>
|
||||
<td>95%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -659,14 +681,14 @@ $$
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/hr.png" alt=""></td>
|
||||
<td>Croatian</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po">hr_HR</a></td>
|
||||
<td>Hrvoje Mandić (trbuhom@net.hr)</td>
|
||||
<td>Hrvoje Mandić (<a href="mailto:trbuhom@net.hr">trbuhom@net.hr</a>)</td>
|
||||
<td>44%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/cz.png" alt=""></td>
|
||||
<td>Czech</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po">cs_CZ</a></td>
|
||||
<td>Lukas Helebrandt (lukas@aiya.cz)</td>
|
||||
<td>Lukas Helebrandt (<a href="mailto:lukas@aiya.cz">lukas@aiya.cz</a>)</td>
|
||||
<td>69%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -680,7 +702,7 @@ $$
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/de.png" alt=""></td>
|
||||
<td>Deutsch</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po">de_DE</a></td>
|
||||
<td>Michael Sonntag (ms@editorei.de)</td>
|
||||
<td>Michael Sonntag (<a href="mailto:ms@editorei.de">ms@editorei.de</a>)</td>
|
||||
<td>100%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -715,7 +737,7 @@ $$
|
||||
<td><img src="https://joplinapp.org/images/flags/es/galicia.png" alt=""></td>
|
||||
<td>Galician</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po">gl_ES</a></td>
|
||||
<td>Marcos Lans (marcoslansgarza@gmail.com)</td>
|
||||
<td>Marcos Lans (<a href="mailto:marcoslansgarza@gmail.com">marcoslansgarza@gmail.com</a>)</td>
|
||||
<td>69%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -736,21 +758,21 @@ $$
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/nl.png" alt=""></td>
|
||||
<td>Nederlands</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_NL.po">nl_NL</a></td>
|
||||
<td>Heimen Stoffels (vistausss@outlook.com)</td>
|
||||
<td>Heimen Stoffels (<a href="mailto:vistausss@outlook.com">vistausss@outlook.com</a>)</td>
|
||||
<td>83%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/no.png" alt=""></td>
|
||||
<td>Norwegian</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/nb_NO.po">nb_NO</a></td>
|
||||
<td>Mats Estensen (code@mxe.no)</td>
|
||||
<td>Mats Estensen (<a href="mailto:code@mxe.no">code@mxe.no</a>)</td>
|
||||
<td>96%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/br.png" alt=""></td>
|
||||
<td>Português (Brasil)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po">pt_BR</a></td>
|
||||
<td>Renato Nunes Bastos (rnbastos@gmail.com)</td>
|
||||
<td>Renato Nunes Bastos (<a href="mailto:rnbastos@gmail.com">rnbastos@gmail.com</a>)</td>
|
||||
<td>90%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -771,21 +793,21 @@ $$
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/se.png" alt=""></td>
|
||||
<td>Svenska</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/sv.po">sv</a></td>
|
||||
<td>Jonatan Nyberg (jonatan@autistici.org)</td>
|
||||
<td>Jonatan Nyberg (<a href="mailto:jonatan@autistici.org">jonatan@autistici.org</a>)</td>
|
||||
<td>93%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/tr.png" alt=""></td>
|
||||
<td>Türkçe</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/tr_TR.po">tr_TR</a></td>
|
||||
<td>Zorbey Doğangüneş (zorbeyd@gmail.com)</td>
|
||||
<td>Zorbey Doğangüneş (<a href="mailto:zorbeyd@gmail.com">zorbeyd@gmail.com</a>)</td>
|
||||
<td>90%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/ru.png" alt=""></td>
|
||||
<td>Русский</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po">ru_RU</a></td>
|
||||
<td>Artyom Karlov (artyom.karlov@gmail.com)</td>
|
||||
<td>Artyom Karlov (<a href="mailto:artyom.karlov@gmail.com">artyom.karlov@gmail.com</a>)</td>
|
||||
<td>96%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -799,14 +821,14 @@ $$
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/tw.png" alt=""></td>
|
||||
<td>中文 (繁體)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_TW.po">zh_TW</a></td>
|
||||
<td>penguinsam (samliu@gmail.com)</td>
|
||||
<td>penguinsam (<a href="mailto:samliu@gmail.com">samliu@gmail.com</a>)</td>
|
||||
<td>83%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/jp.png" alt=""></td>
|
||||
<td>日本語</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po">ja_JP</a></td>
|
||||
<td>AWASHIRO Ikuya (ikunya@gmail.com)</td>
|
||||
<td>AWASHIRO Ikuya (<a href="mailto:ikunya@gmail.com">ikunya@gmail.com</a>)</td>
|
||||
<td>90%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -819,13 +841,13 @@ $$
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
<h1 id="known-bugs">Known bugs</h1>
|
||||
<h1><a name="known-bugs" href="#known-bugs" class="heading-anchor">🔗</a>Known bugs</h1>
|
||||
<ul>
|
||||
<li>Resources larger than 10 MB are not currently supported on mobile. They will crash the application so it is recommended not to attach such resources at the moment. The issue is being looked at.</li>
|
||||
<li>Non-alphabetical characters such as Chinese or Arabic might create glitches in the terminal on Windows. This is a limitation of the current Windows console.</li>
|
||||
<li>It is only possible to upload files of up to 4MB to OneDrive due to a limitation of <a href="https://docs.microsoft.com/en-gb/onedrive/developer/rest-api/api/driveitem_put_content">the API</a> being currently used. There is currently no plan to support OneDrive "large file" API.</li>
|
||||
</ul>
|
||||
<h1 id="license">License</h1>
|
||||
<h1><a name="license" href="#license" class="heading-anchor">🔗</a>License</h1>
|
||||
<p>MIT License</p>
|
||||
<p>Copyright (c) 2016-2019 Laurent Cozic</p>
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,7 +268,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="getting-pre-releases">Getting pre-releases</h1>
|
||||
<h1><a name="getting-pre-releases" href="#getting-pre-releases" class="heading-anchor">🔗</a>Getting pre-releases</h1>
|
||||
<p>Pre-releases are available for the desktop application. They are pretty much like regular releases, except that they have not yet been tested by many users, so it is possible that a bug or two went through.</p>
|
||||
<p>You can help the development of Joplin by choosing to receive these early releases when updating the application. If you find any bug or other issue, you may report it <a href="https://discourse.joplin.cozic.net/">on the forum</a> or <a href="https://github.com/laurent22/joplin/issues">GitHub</a>.</p>
|
||||
<p>In general it is safe to use these pre-releases (they do not include any experimental or unstable features). In fact most pre-release eventually become regular releases after a few days.</p>
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,10 +268,10 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="encryption">Encryption</h1>
|
||||
<h1><a name="encryption" href="#encryption" class="heading-anchor">🔗</a>Encryption</h1>
|
||||
<p>Encrypted data is encoded to ASCII because encryption/decryption functions in React Native can only deal with strings. So for compatibility with all the apps we need to use the lowest common denominator.</p>
|
||||
<h2 id="encrypted-data-format">Encrypted data format</h2>
|
||||
<h3 id="header">Header</h3>
|
||||
<h2><a name="encrypted-data-format" href="#encrypted-data-format" class="heading-anchor">🔗</a>Encrypted data format</h2>
|
||||
<h3><a name="header" href="#header" class="heading-anchor">🔗</a>Header</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -296,7 +314,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<p>See <code>lib/services/EncryptionService.js</code> for the list of available encryption methods.</p>
|
||||
<h3 id="data-chunk">Data chunk</h3>
|
||||
<h3><a name="data-chunk" href="#data-chunk" class="heading-anchor">🔗</a>Data chunk</h3>
|
||||
<p>The data is encoded in one or more chunks for performance reasons. That way it is possible to take a block of data from one file and encrypt it to another block in another file. Encrypting/decrypting the whole file in one go would not work (on mobile especially).</p>
|
||||
<table>
|
||||
<thead>
|
||||
@ -316,27 +334,31 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="master-keys">Master Keys</h2>
|
||||
<h2><a name="master-keys" href="#master-keys" class="heading-anchor">🔗</a>Master Keys</h2>
|
||||
<p>The master keys are used to encrypt and decrypt data. They can be generated from the Encryption Service and are saved to the database. They are themselves encrypted via a user password using a <a href="https://github.com/laurent22/joplin/blob/fb6dee32ac035b00153106273135fb16be4b4fa5/ReactNativeClient/lib/services/EncryptionService.js#L263">strong encyption method</a>.</p>
|
||||
<p>These encrypted master keys are transmitted with the sync data so that they can be available to each client. Each client will need to supply the user password to decrypt each key.</p>
|
||||
<p>The application supports multiple master keys in order to handle cases where one offline client starts encrypting notes, then another offline client starts encrypting notes too, and later both sync. Both master keys will have to be decrypted separately with the user password.</p>
|
||||
<p>Only one master key can be active for encryption purposes. For decryption, the algorithm will check the Master Key ID in the header, then check if it's available to the current app and, if so, use this for decryption.</p>
|
||||
<h2 id="encryption-service">Encryption Service</h2>
|
||||
<p>Only one master key can be active for encryption purposes. For decryption, the algorithm will check the Master Key ID in the header, then check if it's available to the current app and, if so, use this for decryption.</p>
|
||||
<h2><a name="encryption-service" href="#encryption-service" class="heading-anchor">🔗</a>Encryption Service</h2>
|
||||
<p>The applications make use of the <code>EncryptionService</code> class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and be marked as "active".</p>
|
||||
<h2 id="encryption-workflow">Encryption workflow</h2>
|
||||
<h2><a name="encryption-workflow" href="#encryption-workflow" class="heading-anchor">🔗</a>Encryption workflow</h2>
|
||||
<p>Items are encrypted only during synchronisation, when they are serialised (via <code>BaseItem.serializeForSync</code>), so before being sent to the sync target.</p>
|
||||
<p>They are decrypted by DecryptionWorker in the background.</p>
|
||||
<p>The apps handle displaying both decrypted and encrypted items, so that user is aware that these items are there even if not yet decrypted. Encrypted items are mostly read-only to the user, except that they can be deleted.</p>
|
||||
<h2 id="enabling-and-disabling-encryption">Enabling and disabling encryption</h2>
|
||||
<h2><a name="enabling-and-disabling-encryption" href="#enabling-and-disabling-encryption" class="heading-anchor">🔗</a>Enabling and disabling encryption</h2>
|
||||
<p>Enabling/disabling E2EE while two clients are in sync might have an unintuitive behaviour (although that behaviour might be correct), so below some scenarios are explained:</p>
|
||||
<ul>
|
||||
<li><p>If client 1 enables E2EE, all items will be synced to target and will appear encrypted on target. Although all items have been re-uploaded to the target, their timestamps did <em>not</em> change (because the item data itself has not changed, only its representation). Because of this, client 2 will not re-download the items - it does not need to do so anyway since it has already the item data.</p>
|
||||
<li>
|
||||
<p>If client 1 enables E2EE, all items will be synced to target and will appear encrypted on target. Although all items have been re-uploaded to the target, their timestamps did <em>not</em> change (because the item data itself has not changed, only its representation). Because of this, client 2 will not re-download the items - it does not need to do so anyway since it has already the item data.</p>
|
||||
</li>
|
||||
<li><p>When a client sync and download a master key for the first time, encryption will be automatically enabled (user will need to supply the master key password). In that case, all items that are not encrypted will be re-synced. Uploading only non-encrypted items is an optimisation since if an item is already encrypted locally it means it's encrypted on target too.</p>
|
||||
<li>
|
||||
<p>When a client sync and download a master key for the first time, encryption will be automatically enabled (user will need to supply the master key password). In that case, all items that are not encrypted will be re-synced. Uploading only non-encrypted items is an optimisation since if an item is already encrypted locally it means it's encrypted on target too.</p>
|
||||
</li>
|
||||
<li><p>If both clients are in sync with E2EE enabled: if client 1 disable E2EE, it's going to re-upload all the items unencrypted. Client 2 again will not re-download the items for the same reason as above (data did not change, only representation). Note that user <em>must</em> manually disable E2EE on all clients otherwise some will continue to upload encrypted items. Since synchronisation is stateless, clients do not know whether other clients use E2EE or not so this step has to be manual.</p>
|
||||
<li>
|
||||
<p>If both clients are in sync with E2EE enabled: if client 1 disable E2EE, it's going to re-upload all the items unencrypted. Client 2 again will not re-download the items for the same reason as above (data did not change, only representation). Note that user <em>must</em> manually disable E2EE on all clients otherwise some will continue to upload encrypted items. Since synchronisation is stateless, clients do not know whether other clients use E2EE or not so this step has to be manual.</p>
|
||||
</li>
|
||||
<li><p>Although messy, Joplin supports having some clients send encrypted items and others unencrypted ones. The situation gets resolved once all the clients have the same E2EE settings.</p>
|
||||
<li>
|
||||
<p>Although messy, Joplin supports having some clients send encrypted items and others unencrypted ones. The situation gets resolved once all the clients have the same E2EE settings.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -250,7 +268,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="joplin-statistics">Joplin statistics</h1>
|
||||
<h1><a name="joplin-statistics" href="#joplin-statistics" class="heading-anchor">🔗</a>Joplin statistics</h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -261,15 +279,15 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Total Windows downloads</td>
|
||||
<td>231,356</td>
|
||||
<td>235,344</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total macOs downloads</td>
|
||||
<td>102,823</td>
|
||||
<td>103,970</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Linux downloads</td>
|
||||
<td>81,349</td>
|
||||
<td>82,558</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows %</td>
|
||||
@ -300,26 +318,26 @@
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.143">v1.0.143</a></td>
|
||||
<td>2019-04-22T10:51:38Z</td>
|
||||
<td>5,241</td>
|
||||
<td>1,982</td>
|
||||
<td>1,006</td>
|
||||
<td>8,229</td>
|
||||
<td>9,117</td>
|
||||
<td>3,084</td>
|
||||
<td>2,161</td>
|
||||
<td>14,362</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.142">v1.0.142</a></td>
|
||||
<td>2019-04-02T16:44:51Z</td>
|
||||
<td>14,419</td>
|
||||
<td>4,491</td>
|
||||
<td>4,704</td>
|
||||
<td>23,614</td>
|
||||
<td>14,469</td>
|
||||
<td>4,504</td>
|
||||
<td>4,705</td>
|
||||
<td>23,678</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.140">v1.0.140</a></td>
|
||||
<td>2019-03-10T20:59:58Z</td>
|
||||
<td>13,406</td>
|
||||
<td>13,431</td>
|
||||
<td>4,109</td>
|
||||
<td>2,976</td>
|
||||
<td>20,491</td>
|
||||
<td>2,978</td>
|
||||
<td>20,518</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.139">v1.0.139</a></td>
|
||||
@ -348,18 +366,18 @@
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.135">v1.0.135</a></td>
|
||||
<td>2019-02-27T23:36:57Z</td>
|
||||
<td>12,094</td>
|
||||
<td>3,914</td>
|
||||
<td>12,100</td>
|
||||
<td>3,915</td>
|
||||
<td>4,043</td>
|
||||
<td>20,051</td>
|
||||
<td>20,058</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.134">v1.0.134</a></td>
|
||||
<td>2019-02-27T10:21:44Z</td>
|
||||
<td>1,428</td>
|
||||
<td>531</td>
|
||||
<td>200</td>
|
||||
<td>2,159</td>
|
||||
<td>201</td>
|
||||
<td>2,160</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.132">v1.0.132</a></td>
|
||||
@ -372,10 +390,10 @@
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.127">v1.0.127</a></td>
|
||||
<td>2019-02-14T23:12:48Z</td>
|
||||
<td>9,360</td>
|
||||
<td>9,366</td>
|
||||
<td>3,126</td>
|
||||
<td>2,902</td>
|
||||
<td>15,388</td>
|
||||
<td>2,903</td>
|
||||
<td>15,395</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.126">v1.0.126</a></td>
|
||||
@ -396,18 +414,18 @@
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.120">v1.0.120</a></td>
|
||||
<td>2019-01-10T21:42:53Z</td>
|
||||
<td>15,554</td>
|
||||
<td>15,555</td>
|
||||
<td>5,163</td>
|
||||
<td>6,488</td>
|
||||
<td>27,205</td>
|
||||
<td>27,206</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.119">v1.0.119</a></td>
|
||||
<td>2018-12-18T12:40:22Z</td>
|
||||
<td>8,855</td>
|
||||
<td>3,200</td>
|
||||
<td>3,201</td>
|
||||
<td>1,992</td>
|
||||
<td>14,047</td>
|
||||
<td>14,048</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.118">v1.0.118</a></td>
|
||||
@ -422,8 +440,8 @@
|
||||
<td>2018-11-24T12:05:24Z</td>
|
||||
<td>16,208</td>
|
||||
<td>4,846</td>
|
||||
<td>6,356</td>
|
||||
<td>27,410</td>
|
||||
<td>6,357</td>
|
||||
<td>27,411</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.116">v1.0.116</a></td>
|
||||
@ -438,8 +456,8 @@
|
||||
<td>2018-11-16T16:52:02Z</td>
|
||||
<td>3,628</td>
|
||||
<td>1,279</td>
|
||||
<td>782</td>
|
||||
<td>5,689</td>
|
||||
<td>783</td>
|
||||
<td>5,690</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.114">v1.0.114</a></td>
|
||||
@ -452,10 +470,10 @@
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.111">v1.0.111</a></td>
|
||||
<td>2018-09-30T20:15:09Z</td>
|
||||
<td>11,859</td>
|
||||
<td>11,867</td>
|
||||
<td>3,131</td>
|
||||
<td>3,648</td>
|
||||
<td>18,638</td>
|
||||
<td>18,646</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.110">v1.0.110</a></td>
|
||||
@ -479,7 +497,7 @@
|
||||
<td>13</td>
|
||||
<td>6</td>
|
||||
<td>5</td>
|
||||
<td>24 </td>
|
||||
<td>24</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.107">v1.0.107</a></td>
|
||||
@ -510,8 +528,8 @@
|
||||
<td>2018-06-28T20:25:36Z</td>
|
||||
<td>14,990</td>
|
||||
<td>4,656</td>
|
||||
<td>7,057</td>
|
||||
<td>26,703</td>
|
||||
<td>7,059</td>
|
||||
<td>26,705</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.103">v1.0.103</a></td>
|
||||
@ -558,8 +576,8 @@
|
||||
<td>2018-05-26T16:36:39Z</td>
|
||||
<td>2,686</td>
|
||||
<td>1,200</td>
|
||||
<td>1,245</td>
|
||||
<td>5,131</td>
|
||||
<td>1,250</td>
|
||||
<td>5,136</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.95">v1.0.95</a></td>
|
||||
@ -581,9 +599,9 @@
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.93">v1.0.93</a></td>
|
||||
<td>2018-05-14T11:36:01Z</td>
|
||||
<td>1,770</td>
|
||||
<td>913</td>
|
||||
<td>919</td>
|
||||
<td>742</td>
|
||||
<td>3,425</td>
|
||||
<td>3,431</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.91">v1.0.91</a></td>
|
||||
@ -612,42 +630,42 @@
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.83">v1.0.83</a></td>
|
||||
<td>2018-04-04T19:43:58Z</td>
|
||||
<td>4,546</td>
|
||||
<td>2,419</td>
|
||||
<td>2,631</td>
|
||||
<td>9,596</td>
|
||||
<td>4,551</td>
|
||||
<td>2,426</td>
|
||||
<td>2,632</td>
|
||||
<td>9,609</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.82">v1.0.82</a></td>
|
||||
<td>2018-03-31T19:16:31Z</td>
|
||||
<td>684</td>
|
||||
<td>385</td>
|
||||
<td>386</td>
|
||||
<td>99</td>
|
||||
<td>1,168</td>
|
||||
<td>1,169</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.81">v1.0.81</a></td>
|
||||
<td>2018-03-28T08:13:58Z</td>
|
||||
<td>985</td>
|
||||
<td>568</td>
|
||||
<td>747</td>
|
||||
<td>2,300</td>
|
||||
<td>569</td>
|
||||
<td>748</td>
|
||||
<td>2,302</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.79">v1.0.79</a></td>
|
||||
<td>2018-03-23T18:00:11Z</td>
|
||||
<td>919</td>
|
||||
<td>512</td>
|
||||
<td>356</td>
|
||||
<td>1,787</td>
|
||||
<td>357</td>
|
||||
<td>1,788</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.78">v1.0.78</a></td>
|
||||
<td>2018-03-17T15:27:18Z</td>
|
||||
<td>1,302</td>
|
||||
<td>840</td>
|
||||
<td>847</td>
|
||||
<td>2,989</td>
|
||||
<td>848</td>
|
||||
<td>2,990</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.77">v1.0.77</a></td>
|
||||
@ -661,9 +679,9 @@
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.72">v1.0.72</a></td>
|
||||
<td>2018-03-14T09:44:35Z</td>
|
||||
<td>396</td>
|
||||
<td>233</td>
|
||||
<td>234</td>
|
||||
<td>36</td>
|
||||
<td>665</td>
|
||||
<td>666</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.70">v1.0.70</a></td>
|
||||
@ -677,9 +695,9 @@
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.67">v1.0.67</a></td>
|
||||
<td>2018-02-19T22:51:08Z</td>
|
||||
<td>1,805</td>
|
||||
<td>581</td>
|
||||
<td>582</td>
|
||||
<td>0</td>
|
||||
<td>2,386</td>
|
||||
<td>2,387</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.66">v1.0.66</a></td>
|
||||
@ -718,8 +736,8 @@
|
||||
<td>2018-02-12T20:19:58Z</td>
|
||||
<td>549</td>
|
||||
<td>276</td>
|
||||
<td>354</td>
|
||||
<td>1,179</td>
|
||||
<td>355</td>
|
||||
<td>1,180</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.61">v0.10.61</a></td>
|
||||
@ -734,16 +752,16 @@
|
||||
<td>2018-02-06T13:09:56Z</td>
|
||||
<td>711</td>
|
||||
<td>497</td>
|
||||
<td>541</td>
|
||||
<td>1,749</td>
|
||||
<td>542</td>
|
||||
<td>1,750</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.54">v0.10.54</a></td>
|
||||
<td>2018-01-31T20:21:30Z</td>
|
||||
<td>1,811</td>
|
||||
<td>1,440</td>
|
||||
<td>308</td>
|
||||
<td>3,559</td>
|
||||
<td>1,441</td>
|
||||
<td>309</td>
|
||||
<td>3,561</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.52">v0.10.52</a></td>
|
||||
@ -766,8 +784,8 @@
|
||||
<td>2018-01-23T11:19:51Z</td>
|
||||
<td>1,956</td>
|
||||
<td>1,732</td>
|
||||
<td>19</td>
|
||||
<td>3,707</td>
|
||||
<td>20</td>
|
||||
<td>3,708</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.47">v0.10.47</a></td>
|
||||
@ -781,33 +799,33 @@
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.43">v0.10.43</a></td>
|
||||
<td>2018-01-08T10:12:10Z</td>
|
||||
<td>3,427</td>
|
||||
<td>2,327</td>
|
||||
<td>1,197</td>
|
||||
<td>6,951</td>
|
||||
<td>2,328</td>
|
||||
<td>1,198</td>
|
||||
<td>6,953</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.41">v0.10.41</a></td>
|
||||
<td>2018-01-05T20:38:12Z</td>
|
||||
<td>1,029</td>
|
||||
<td>1,528</td>
|
||||
<td>227</td>
|
||||
<td>2,784</td>
|
||||
<td>1,529</td>
|
||||
<td>228</td>
|
||||
<td>2,786</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.40">v0.10.40</a></td>
|
||||
<td>2018-01-02T23:16:57Z</td>
|
||||
<td>1,586</td>
|
||||
<td>1,752</td>
|
||||
<td>326</td>
|
||||
<td>3,664</td>
|
||||
<td>327</td>
|
||||
<td>3,665</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.39">v0.10.39</a></td>
|
||||
<td>2017-12-11T21:19:44Z</td>
|
||||
<td>5,484</td>
|
||||
<td>3,974</td>
|
||||
<td>2,881</td>
|
||||
<td>12,339</td>
|
||||
<td>5,495</td>
|
||||
<td>3,984</td>
|
||||
<td>2,891</td>
|
||||
<td>12,370</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.38">v0.10.38</a></td>
|
||||
@ -894,8 +912,8 @@
|
||||
<td>2017-11-24T14:27:49Z</td>
|
||||
<td>136</td>
|
||||
<td>676</td>
|
||||
<td>5,406</td>
|
||||
<td>6,218</td>
|
||||
<td>5,425</td>
|
||||
<td>6,237</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.23">v0.10.23</a></td>
|
||||
|
@ -180,6 +180,24 @@
|
||||
color: gray;
|
||||
font-size: .9em;
|
||||
}
|
||||
a.heading-anchor {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 1.3em;
|
||||
font-size: 0.7em;
|
||||
margin-left: -1.3em;
|
||||
line-height: 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.heading-anchor:hover,
|
||||
h1:hover a.heading-anchor,
|
||||
h2:hover a.heading-anchor,
|
||||
h3:hover a.heading-anchor,
|
||||
h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
@media all and (min-width: 400px) {
|
||||
.nav-right .share-btn {
|
||||
display: inline-block;
|
||||
@ -253,8 +271,8 @@
|
||||
<p>Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified with your own text editor.</p>
|
||||
<p>Notes exported from Evernote via .enex files <a href="https://joplinapp.org/#importing">can be imported</a> into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.</p>
|
||||
<p>The notes can be <a href="#synchronisation">synchronised</a> with various targets including the file system (for example with a network directory), Nextcloud, Dropbox, OneDrive or WebDAV. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.</p>
|
||||
<p><img src="https://joplinapp.org/images/ScreenshotTerminal.png" style="max-width: 60%"></p>
|
||||
<h1 id="installation">Installation</h1>
|
||||
<img src="https://joplinapp.org/images/ScreenshotTerminal.png" style="max-width: 60%">
|
||||
<h1><a name="installation" href="#installation" class="heading-anchor">🔗</a>Installation</h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -278,14 +296,14 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<p>To start it, type <code>joplin</code>.</p>
|
||||
<h1 id="usage">Usage</h1>
|
||||
<h1><a name="usage" href="#usage" class="heading-anchor">🔗</a>Usage</h1>
|
||||
<p>To start the application type <code>joplin</code>. This will open the user interface, which has three main panes: Notebooks, Notes and the text of the current note. There are also additional panels that can be toggled on and off via <a href="#shortcuts">shortcuts</a>.</p>
|
||||
<p><img src="https://joplinapp.org/images/ScreenshotTerminalCaptions.png" height="450px"></p>
|
||||
<h2 id="input-modes">Input modes</h2>
|
||||
<img src="https://joplinapp.org/images/ScreenshotTerminalCaptions.png" height="450px">
|
||||
<h2><a name="input-modes" href="#input-modes" class="heading-anchor">🔗</a>Input modes</h2>
|
||||
<p>Joplin user interface is partly based on the text editor Vim and offers two different modes to interact with the notes and notebooks:</p>
|
||||
<h3 id="normal-mode">Normal mode</h3>
|
||||
<h3><a name="normal-mode" href="#normal-mode" class="heading-anchor">🔗</a>Normal mode</h3>
|
||||
<p>Allows moving from one pane to another using the <code>Tab</code> and <code>Shift-Tab</code> keys, and to select/view notes using the arrow keys. Text area can be scrolled using the arrow keys too. Press <code>Enter</code> to edit a note. Various other <a href="#shortcuts">shortcuts</a> are available.</p>
|
||||
<h3 id="command-line-mode">Command-line mode</h3>
|
||||
<h3><a name="command-line-mode" href="#command-line-mode" class="heading-anchor">🔗</a>Command-line mode</h3>
|
||||
<p>Press <code>:</code> to enter command line mode. From there, the Joplin commands such as <code>mknote</code> or <code>search</code> are available. See the <a href="#commands">full list of commands</a>.</p>
|
||||
<p>It is possible to refer to a note or notebook by title or ID. However the simplest way is to refer to the currently selected item using one of these shortcuts:</p>
|
||||
<table>
|
||||
@ -311,21 +329,27 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Examples:</strong></p>
|
||||
<p>Create a new note with title "Wednesday's meeting":</p>
|
||||
<pre><code>mknote "Wednesday's meeting"
|
||||
</code></pre><p>Create a new to-do:</p>
|
||||
<p>Create a new note with title "Wednesday's meeting":</p>
|
||||
<pre><code>mknote "Wednesday's meeting"
|
||||
</code></pre>
|
||||
<p>Create a new to-do:</p>
|
||||
<pre><code>mktodo "Buy bread"
|
||||
</code></pre><p>Move the currently selected note ($n) to the notebook with title "Personal"</p>
|
||||
</code></pre>
|
||||
<p>Move the currently selected note ($n) to the notebook with title "Personal"</p>
|
||||
<pre><code>mv $n "Personal"
|
||||
</code></pre><p>Rename the currently selected notebook ($b) to "Something":</p>
|
||||
</code></pre>
|
||||
<p>Rename the currently selected notebook ($b) to "Something":</p>
|
||||
<pre><code>ren $b "Something"
|
||||
</code></pre><p>Attach a local file to the currently selected note ($n):</p>
|
||||
</code></pre>
|
||||
<p>Attach a local file to the currently selected note ($n):</p>
|
||||
<pre><code>attach $n /home/laurent/pictures/Vacation12.jpg
|
||||
</code></pre><p>The configuration can also be changed from command-line mode. For example, to change the current editor to Sublime Text:</p>
|
||||
</code></pre>
|
||||
<p>The configuration can also be changed from command-line mode. For example, to change the current editor to Sublime Text:</p>
|
||||
<pre><code>config editor "subl -w"
|
||||
</code></pre><h2 id="editing-a-note">Editing a note</h2>
|
||||
</code></pre>
|
||||
<h2><a name="editing-a-note" href="#editing-a-note" class="heading-anchor">🔗</a>Editing a note</h2>
|
||||
<p>To edit a note, select it and press <code>ENTER</code>. Or, in command-line mode, type <code>edit $n</code> to edit the currently selected note, or <code>edit "Note title"</code> to edit a particular note.</p>
|
||||
<h2 id="getting-help">Getting help</h2>
|
||||
<h2><a name="getting-help" href="#getting-help" class="heading-anchor">🔗</a>Getting help</h2>
|
||||
<p>The complete usage information is available from command-line mode, by typing one of these commands:</p>
|
||||
<table>
|
||||
<thead>
|
||||
@ -351,50 +375,55 @@
|
||||
</table>
|
||||
<p>If the help is not fully visible, press <code>Tab</code> multiple times till the console is in focus and use the arrow keys or page up/down to scroll the text.</p>
|
||||
<p>For general information relevant to all the applications, see also <a href="https://joplinapp.org">Joplin home page</a>.</p>
|
||||
<h1 id="importing-notes-from-evernote">Importing notes from Evernote</h1>
|
||||
<h1><a name="importing-notes-from-evernote" href="#importing-notes-from-evernote" class="heading-anchor">🔗</a>Importing notes from Evernote</h1>
|
||||
<p>To import Evernote data, follow these steps:</p>
|
||||
<ul>
|
||||
<li>First, export your Evernote notebooks to ENEX files as described <a href="https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks">here</a>.</li>
|
||||
<li>In Joplin, in <a href="#command-line-mode">command-line mode</a>, type <code>import /path/to/file.enex</code>. This will import the notes into a new notebook named after the filename.</li>
|
||||
<li>Then repeat the process for each notebook that needs to be imported.</li>
|
||||
</ul>
|
||||
<h1 id="synchronisation">Synchronisation</h1>
|
||||
<h1><a name="synchronisation" href="#synchronisation" class="heading-anchor">🔗</a>Synchronisation</h1>
|
||||
<p>One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or OneDrive, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.</p>
|
||||
<p>Currently, synchronisation is possible with Nextcloud, Dropbox (by default) and OneDrive, or the local filesystem. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.</p>
|
||||
<h2 id="nextcloud-synchronisation">Nextcloud synchronisation</h2>
|
||||
<h2><a name="nextcloud-synchronisation" href="#nextcloud-synchronisation" class="heading-anchor">🔗</a>Nextcloud synchronisation</h2>
|
||||
<p>You will need to set the <code>sync.target</code> config variable and all the <code>sync.5.path</code>, <code>sync.5.username</code> and <code>sync.5.password</code> config variables to, respectively the Nextcloud WebDAV URL, your username and your password. This can be done from the command line mode using:</p>
|
||||
<pre><code>:config sync.target 5
|
||||
:config sync.5.path https://example.com/nextcloud/remote.php/webdav/Joplin
|
||||
:config sync.5.username YOUR_USERNAME
|
||||
:config sync.5.password YOUR_PASSWORD
|
||||
</code></pre><p>If synchronisation does not work, please consult the logs in the app profile directory (<code>~/.config/joplin</code>)- it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.</p>
|
||||
<h2 id="webdav-synchronisation">WebDAV synchronisation</h2>
|
||||
</code></pre>
|
||||
<p>If synchronisation does not work, please consult the logs in the app profile directory (<code>~/.config/joplin</code>)- it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.</p>
|
||||
<h2><a name="webdav-synchronisation" href="#webdav-synchronisation" class="heading-anchor">🔗</a>WebDAV synchronisation</h2>
|
||||
<p>Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.</p>
|
||||
<h2 id="onedrive-and-dropbox-synchronisation">OneDrive and Dropbox synchronisation</h2>
|
||||
<h2><a name="onedrive-and-dropbox-synchronisation" href="#onedrive-and-dropbox-synchronisation" class="heading-anchor">🔗</a>OneDrive and Dropbox synchronisation</h2>
|
||||
<p>For Dropbox, type <code>:config sync.target 7</code>. For OneDrive, type <code>:config sync.target 3</code>. Then type <code>sync</code> to login to the service and start the synchronisation process.</p>
|
||||
<p>It is possible to also synchronise outside of the user interface by typing <code>joplin sync</code> from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:</p>
|
||||
<pre><code>*/30 * * * * /path/to/joplin sync
|
||||
</code></pre><h1 id="urls">URLs</h1>
|
||||
</code></pre>
|
||||
<h1><a name="urls" href="#urls" class="heading-anchor">🔗</a>URLs</h1>
|
||||
<p>When Ctrl+Clicking a URL, most terminals will open that URL in the default browser. However, one issue, especially with long URLs, is that they can end up like this:</p>
|
||||
<p><img src="https://joplinapp.org/images/UrlCut.png" width="300px"></p>
|
||||
<img src="https://joplinapp.org/images/UrlCut.png" width="300px">
|
||||
<p>Not only it makes the text hard to read, but the link, being cut in two, will also not be clickable.</p>
|
||||
<p>As a solution Joplin tries to start a mini-server in the background and, if successful, all the links will be converted to a much shorter URL:</p>
|
||||
<p><img src="https://joplinapp.org/images/UrlNoCut.png" width="300px"></p>
|
||||
<img src="https://joplinapp.org/images/UrlNoCut.png" width="300px">
|
||||
<p>Since this is still an actual URL, the terminal will still make it clickable. And with shorter URLs, the text is more readable and the links unlikely to be cut. Both resources (files that are attached to notes) and external links are handled in this way.</p>
|
||||
<h1 id="attachments-resources">Attachments / Resources</h1>
|
||||
<h1><a name="attachments-resources" href="#attachments-resources" class="heading-anchor">🔗</a>Attachments / Resources</h1>
|
||||
<p>In Markdown, links to resources are represented as a simple ID to the resource. In order to give access to these resources, they will be, like links, converted to local URLs. Clicking this link will then open a browser, which will handle the file - i.e. display the image, open the PDF file, etc.</p>
|
||||
<h1 id="shell-mode">Shell mode</h1>
|
||||
<h1><a name="shell-mode" href="#shell-mode" class="heading-anchor">🔗</a>Shell mode</h1>
|
||||
<p>Commands can also be used directly from a shell. To view the list of available commands, type <code>joplin help all</code>. To reference a note, notebook or tag you can either use the ID (type <code>joplin ls -l</code> to view the ID) or by title.</p>
|
||||
<p>For example, this will create a new note "My note" in the notebook "My notebook":</p>
|
||||
<pre><code>$ joplin mkbook "My notebook"
|
||||
$ joplin use "My notebook"
|
||||
$ joplin mknote "My note"
|
||||
</code></pre><p>To view the newly created note:</p>
|
||||
</code></pre>
|
||||
<p>To view the newly created note:</p>
|
||||
<pre><code>$ joplin ls -l
|
||||
fe889 07/12/2017 17:57 My note
|
||||
</code></pre><p>Give a new title to the note:</p>
|
||||
</code></pre>
|
||||
<p>Give a new title to the note:</p>
|
||||
<pre><code>$ joplin set fe889 title "New title"
|
||||
</code></pre><h1 id="shortcuts">Shortcuts</h1>
|
||||
</code></pre>
|
||||
<h1><a name="shortcuts" href="#shortcuts" class="heading-anchor">🔗</a>Shortcuts</h1>
|
||||
<p>There are two types of shortcuts: those that manipulate the user interface directly, such as <code>TAB</code> to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing <code>mn</code> ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with <code>mknote ""</code> from where the title of the note can be entered. See below for the full list of default shortcuts:</p>
|
||||
<pre><code>: enter_command_line_mode
|
||||
TAB focus_next
|
||||
@ -414,27 +443,28 @@ mt mktodo ""
|
||||
mb mkbook ""
|
||||
yn cp $n ""
|
||||
dn mv $n ""
|
||||
</code></pre><p>Shortcut can be configured by adding a keymap file to the profile directory in <code>~/.config/joplin/keymap.json</code>. The content of this file is a JSON array with each entry defining a command and the keys associated with it.</p>
|
||||
</code></pre>
|
||||
<p>Shortcut can be configured by adding a keymap file to the profile directory in <code>~/.config/joplin/keymap.json</code>. The content of this file is a JSON array with each entry defining a command and the keys associated with it.</p>
|
||||
<p>As an example, this is the default keymap, but read below for a detailed explanation of each property.</p>
|
||||
<pre><code class="lang-json">[
|
||||
{ "keys": [":"], "type": "function", "command": "enter_command_line_mode" },
|
||||
{ "keys": ["TAB"], "type": "function", "command": "focus_next" },
|
||||
{ "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" },
|
||||
{ "keys": ["UP"], "type": "function", "command": "move_up" },
|
||||
{ "keys": ["DOWN"], "type": "function", "command": "move_down" },
|
||||
{ "keys": ["PAGE_UP"], "type": "function", "command": "page_up" },
|
||||
{ "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" },
|
||||
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
|
||||
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
|
||||
{ "keys": [" "], "command": "todo toggle $n" },
|
||||
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
|
||||
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
|
||||
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 }
|
||||
<pre><code class="language-json">[
|
||||
{ "keys": [":"], "type": "function", "command": "enter_command_line_mode" },
|
||||
{ "keys": ["TAB"], "type": "function", "command": "focus_next" },
|
||||
{ "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" },
|
||||
{ "keys": ["UP"], "type": "function", "command": "move_up" },
|
||||
{ "keys": ["DOWN"], "type": "function", "command": "move_down" },
|
||||
{ "keys": ["PAGE_UP"], "type": "function", "command": "page_up" },
|
||||
{ "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" },
|
||||
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
|
||||
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
|
||||
{ "keys": [" "], "command": "todo toggle $n" },
|
||||
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
|
||||
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
|
||||
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 }
|
||||
]
|
||||
</code></pre>
|
||||
<p>Each entry can have the following properties:</p>
|
||||
@ -448,7 +478,7 @@ dn mv $n ""
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>keys</code></td>
|
||||
<td>The array of keys that will trigger the action. Special keys such as page up, down arrow, etc. needs to be specified UPPERCASE. See the <a href="https://github.com/cronvel/terminal-kit/blob/3114206a9556f518cc63abbcb3d188fe1995100d/lib/termconfig/xterm.js#L531">list of available special keys</a>. For example, <code>['DELETE', 'BACKSPACE']</code> means the command will run if the user pressed either the delete or backspace key. Key combinations can also be provided - in that case specify them lowercase. For example "tc" means that the command will be executed when the user pressed "t" then "c". Special keys can also be used in this fashion - simply write them one after the other. For instance, <code>CTRL_WCTRL_W</code> means the action would be executed if the user pressed "ctrl-w ctrl-w".</td>
|
||||
<td>The array of keys that will trigger the action. Special keys such as page up, down arrow, etc. needs to be specified UPPERCASE. See the <a href="https://github.com/cronvel/terminal-kit/blob/3114206a9556f518cc63abbcb3d188fe1995100d/lib/termconfig/xterm.js#L531">list of available special keys</a>. For example, <code>['DELETE', 'BACKSPACE']</code> means the command will run if the user pressed either the delete or backspace key. Key combinations can also be provided - in that case specify them lowercase. For example "tc" means that the command will be executed when the user pressed "t" then "c". Special keys can also be used in this fashion - simply write them one after the other. For instance, <code>CTRL_WCTRL_W</code> means the action would be executed if the user pressed "ctrl-w ctrl-w".</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>type</code></td>
|
||||
@ -519,7 +549,7 @@ dn mv $n ""
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h1 id="commands">Commands</h1>
|
||||
<h1><a name="commands" href="#commands" class="heading-anchor">🔗</a>Commands</h1>
|
||||
<p>The following commands are available in <a href="#command-line-mode">command-line mode</a>:</p>
|
||||
<pre><code>attach <note> <file>
|
||||
|
||||
@ -553,7 +583,7 @@ Possible keys/values:
|
||||
(Svenska), ru_RU (Русский), zh_CN (中文 (简体)),
|
||||
zh_TW (中文 (繁體)), ja_JP (日本語), ko (한국말).
|
||||
Default: "en_GB"
|
||||
|
||||
|
||||
dateFormat Date format.
|
||||
Type: Enum.
|
||||
Possible values: DD/MM/YYYY (30/01/2017),
|
||||
@ -561,48 +591,48 @@ Possible keys/values:
|
||||
MM/DD/YY (01/30/17), YYYY-MM-DD (2017-01-30),
|
||||
DD.MM.YYYY (30.01.2017).
|
||||
Default: "DD/MM/YYYY"
|
||||
|
||||
|
||||
timeFormat Time format.
|
||||
Type: Enum.
|
||||
Possible values: HH:mm (20:30), h:mm A (8:30 PM).
|
||||
Default: "HH:mm"
|
||||
|
||||
|
||||
uncompletedTodosOnTop Uncompleted to-dos on top.
|
||||
Type: bool.
|
||||
Default: true
|
||||
|
||||
|
||||
showCompletedTodos Show completed to-dos.
|
||||
Type: bool.
|
||||
Default: true
|
||||
|
||||
|
||||
notes.sortOrder.field Sort notes by.
|
||||
Type: Enum.
|
||||
Possible values: user_updated_time (Updated
|
||||
date), user_created_time (Created date), title
|
||||
(Title).
|
||||
Default: "user_updated_time"
|
||||
|
||||
|
||||
notes.sortOrder.reverse Reverse sort order.
|
||||
Type: bool.
|
||||
Default: true
|
||||
|
||||
|
||||
trackLocation Save geo-location with notes.
|
||||
Type: bool.
|
||||
Default: true
|
||||
|
||||
|
||||
sync.interval Synchronisation interval.
|
||||
Type: Enum.
|
||||
Possible values: 0 (Disabled), 300 (5 minutes),
|
||||
600 (10 minutes), 1800 (30 minutes), 3600 (1
|
||||
hour), 43200 (12 hours), 86400 (24 hours).
|
||||
Default: 300
|
||||
|
||||
|
||||
editor Text editor command.
|
||||
The editor command (may include arguments) that
|
||||
will be used to open a note. If none is provided
|
||||
it will try to auto-detect the default editor.
|
||||
Type: string.
|
||||
|
||||
|
||||
sync.target Synchronisation target.
|
||||
The target to synchonise to. Each sync target may
|
||||
have additional parameters which are named as
|
||||
@ -612,38 +642,38 @@ Possible keys/values:
|
||||
(OneDrive Dev (For testing only)), 5 (Nextcloud),
|
||||
6 (WebDAV), 7 (Dropbox).
|
||||
Default: 7
|
||||
|
||||
|
||||
sync.2.path Directory to synchronise with (absolute path).
|
||||
The path to synchronise with when file system
|
||||
synchronisation is enabled. See `sync.target`.
|
||||
Type: string.
|
||||
|
||||
|
||||
sync.5.path Nextcloud WebDAV URL.
|
||||
Attention: If you change this location, make sure
|
||||
you copy all your content to it before syncing,
|
||||
otherwise all files will be removed! See the FAQ
|
||||
for more details: https://joplinapp.org/faq/
|
||||
Type: string.
|
||||
|
||||
|
||||
sync.5.username Nextcloud username.
|
||||
Type: string.
|
||||
|
||||
|
||||
sync.5.password Nextcloud password.
|
||||
Type: string.
|
||||
|
||||
|
||||
sync.6.path WebDAV URL.
|
||||
Attention: If you change this location, make sure
|
||||
you copy all your content to it before syncing,
|
||||
otherwise all files will be removed! See the FAQ
|
||||
for more details: https://joplinapp.org/faq/
|
||||
Type: string.
|
||||
|
||||
|
||||
sync.6.username WebDAV username.
|
||||
Type: string.
|
||||
|
||||
|
||||
sync.6.password WebDAV password.
|
||||
Type: string.
|
||||
|
||||
|
||||
net.customCertificates Custom TLS certificates.
|
||||
Comma-separated list of paths to directories to
|
||||
load the certificates from, or path to individual
|
||||
@ -653,7 +683,7 @@ Possible keys/values:
|
||||
before clicking on "Check synchronisation
|
||||
configuration".
|
||||
Type: string.
|
||||
|
||||
|
||||
net.ignoreTlsErrors Ignore TLS certificate errors.
|
||||
Type: bool.
|
||||
Default: false
|
||||
@ -791,7 +821,8 @@ use <notebook>
|
||||
version
|
||||
|
||||
Displays version information
|
||||
</code></pre><h1 id="license">License</h1>
|
||||
</code></pre>
|
||||
<h1><a name="license" href="#license" class="heading-anchor">🔗</a>License</h1>
|
||||
<p>Copyright (c) 2016-2019 Laurent Cozic</p>
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>
|
||||
<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>
|
||||
|
10
readme/changelog_cli.md
Normal file
10
readme/changelog_cli.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Joplin terminal app changelog
|
||||
|
||||
## [cli-v1.0.125](https://github.com/laurent22/joplin/releases/tag/cli-v1.0.125) - 2019-04-29T18:38:05Z
|
||||
|
||||
- Improved: Improved support for Japanese, Chinese, Korean search queries (also applies to Goto Anything)
|
||||
- Improved: Display warning when changing dir for filesystem sync
|
||||
- Fixed: Remove message "Processing a path that has already been done" as this is not an error (#1353)
|
||||
- Fixed: Some resources could incorrectly be deleted even though they are still present in a note. Also added additional verifications before deleting a resource. (#1433)
|
||||
- Fixed: Handle invalid resource tags that contain no data when importing ENEX (#1405)
|
||||
- Fixed: Restored inline code styling (#1326)
|
@ -16,14 +16,6 @@ Or you may support the project on Patreon:
|
||||
|
||||
<a href="https://www.patreon.com/joplin"><img src="https://joplinapp.org/images/badges/Patreon.png"/></a>
|
||||
|
||||
## Bitcoin
|
||||
|
||||
Bitcoins are also accepted:
|
||||
|
||||
<a href="bitcoin:1AnbeRd5NZT1ssG93jXzaDoHwzgjQAHX3R?message=Joplin%20development">1AnbeRd5NZT1ssG93jXzaDoHwzgjQAHX3R</a>
|
||||
|
||||

|
||||
|
||||
## Other way to support the development
|
||||
|
||||
Finally, there are other ways to support the development of Joplin:
|
||||
|
@ -2,79 +2,79 @@
|
||||
|
||||
Name | Value
|
||||
--- | ---
|
||||
Total Windows downloads | 231,356
|
||||
Total macOs downloads | 102,823
|
||||
Total Linux downloads | 81,349
|
||||
Total Windows downloads | 235,344
|
||||
Total macOs downloads | 103,970
|
||||
Total Linux downloads | 82,558
|
||||
Windows % | 56%
|
||||
macOS % | 25%
|
||||
Linux % | 20%
|
||||
|
||||
Version | Date | Windows | macOS | Linux | Total
|
||||
--- | --- | --- | --- | --- | ---
|
||||
[v1.0.143](https://github.com/laurent22/joplin/releases/tag/v1.0.143) | 2019-04-22T10:51:38Z | 5,241 | 1,982 | 1,006 | 8,229
|
||||
[v1.0.142](https://github.com/laurent22/joplin/releases/tag/v1.0.142) | 2019-04-02T16:44:51Z | 14,419 | 4,491 | 4,704 | 23,614
|
||||
[v1.0.140](https://github.com/laurent22/joplin/releases/tag/v1.0.140) | 2019-03-10T20:59:58Z | 13,406 | 4,109 | 2,976 | 20,491
|
||||
[v1.0.143](https://github.com/laurent22/joplin/releases/tag/v1.0.143) | 2019-04-22T10:51:38Z | 9,117 | 3,084 | 2,161 | 14,362
|
||||
[v1.0.142](https://github.com/laurent22/joplin/releases/tag/v1.0.142) | 2019-04-02T16:44:51Z | 14,469 | 4,504 | 4,705 | 23,678
|
||||
[v1.0.140](https://github.com/laurent22/joplin/releases/tag/v1.0.140) | 2019-03-10T20:59:58Z | 13,431 | 4,109 | 2,978 | 20,518
|
||||
[v1.0.139](https://github.com/laurent22/joplin/releases/tag/v1.0.139) | 2019-03-09T10:06:48Z | 83 | 19 | 23 | 125
|
||||
[v1.0.138](https://github.com/laurent22/joplin/releases/tag/v1.0.138) | 2019-03-03T17:23:00Z | 111 | 41 | 68 | 220
|
||||
[v1.0.137](https://github.com/laurent22/joplin/releases/tag/v1.0.137) | 2019-03-03T01:12:51Z | 547 | 26 | 68 | 641
|
||||
[v1.0.135](https://github.com/laurent22/joplin/releases/tag/v1.0.135) | 2019-02-27T23:36:57Z | 12,094 | 3,914 | 4,043 | 20,051
|
||||
[v1.0.134](https://github.com/laurent22/joplin/releases/tag/v1.0.134) | 2019-02-27T10:21:44Z | 1,428 | 531 | 200 | 2,159
|
||||
[v1.0.135](https://github.com/laurent22/joplin/releases/tag/v1.0.135) | 2019-02-27T23:36:57Z | 12,100 | 3,915 | 4,043 | 20,058
|
||||
[v1.0.134](https://github.com/laurent22/joplin/releases/tag/v1.0.134) | 2019-02-27T10:21:44Z | 1,428 | 531 | 201 | 2,160
|
||||
[v1.0.132](https://github.com/laurent22/joplin/releases/tag/v1.0.132) | 2019-02-26T23:02:05Z | 1,046 | 411 | 79 | 1,536
|
||||
[v1.0.127](https://github.com/laurent22/joplin/releases/tag/v1.0.127) | 2019-02-14T23:12:48Z | 9,360 | 3,126 | 2,902 | 15,388
|
||||
[v1.0.127](https://github.com/laurent22/joplin/releases/tag/v1.0.127) | 2019-02-14T23:12:48Z | 9,366 | 3,126 | 2,903 | 15,395
|
||||
[v1.0.126](https://github.com/laurent22/joplin/releases/tag/v1.0.126) | 2019-02-09T19:46:16Z | 914 | 52 | 108 | 1,074
|
||||
[v1.0.125](https://github.com/laurent22/joplin/releases/tag/v1.0.125) | 2019-01-26T18:14:33Z | 10,230 | 3,518 | 1,689 | 15,437
|
||||
[v1.0.120](https://github.com/laurent22/joplin/releases/tag/v1.0.120) | 2019-01-10T21:42:53Z | 15,554 | 5,163 | 6,488 | 27,205
|
||||
[v1.0.119](https://github.com/laurent22/joplin/releases/tag/v1.0.119) | 2018-12-18T12:40:22Z | 8,855 | 3,200 | 1,992 | 14,047
|
||||
[v1.0.120](https://github.com/laurent22/joplin/releases/tag/v1.0.120) | 2019-01-10T21:42:53Z | 15,555 | 5,163 | 6,488 | 27,206
|
||||
[v1.0.119](https://github.com/laurent22/joplin/releases/tag/v1.0.119) | 2018-12-18T12:40:22Z | 8,855 | 3,201 | 1,992 | 14,048
|
||||
[v1.0.118](https://github.com/laurent22/joplin/releases/tag/v1.0.118) | 2019-01-11T08:34:13Z | 676 | 215 | 75 | 966
|
||||
[v1.0.117](https://github.com/laurent22/joplin/releases/tag/v1.0.117) | 2018-11-24T12:05:24Z | 16,208 | 4,846 | 6,356 | 27,410
|
||||
[v1.0.117](https://github.com/laurent22/joplin/releases/tag/v1.0.117) | 2018-11-24T12:05:24Z | 16,208 | 4,846 | 6,357 | 27,411
|
||||
[v1.0.116](https://github.com/laurent22/joplin/releases/tag/v1.0.116) | 2018-11-20T19:09:24Z | 3,458 | 1,087 | 704 | 5,249
|
||||
[v1.0.115](https://github.com/laurent22/joplin/releases/tag/v1.0.115) | 2018-11-16T16:52:02Z | 3,628 | 1,279 | 782 | 5,689
|
||||
[v1.0.115](https://github.com/laurent22/joplin/releases/tag/v1.0.115) | 2018-11-16T16:52:02Z | 3,628 | 1,279 | 783 | 5,690
|
||||
[v1.0.114](https://github.com/laurent22/joplin/releases/tag/v1.0.114) | 2018-10-24T20:14:10Z | 11,369 | 3,475 | 3,822 | 18,666
|
||||
[v1.0.111](https://github.com/laurent22/joplin/releases/tag/v1.0.111) | 2018-09-30T20:15:09Z | 11,859 | 3,131 | 3,648 | 18,638
|
||||
[v1.0.111](https://github.com/laurent22/joplin/releases/tag/v1.0.111) | 2018-09-30T20:15:09Z | 11,867 | 3,131 | 3,648 | 18,646
|
||||
[v1.0.110](https://github.com/laurent22/joplin/releases/tag/v1.0.110) | 2018-09-29T12:29:21Z | 926 | 376 | 103 | 1,405
|
||||
[v1.0.109](https://github.com/laurent22/joplin/releases/tag/v1.0.109) | 2018-09-27T18:01:41Z | 2,074 | 680 | 311 | 3,065
|
||||
[v1.0.108](https://github.com/laurent22/joplin/releases/tag/v1.0.108) | 2018-09-29T18:49:29Z | 13 | 6 | 5 | 24
|
||||
[v1.0.107](https://github.com/laurent22/joplin/releases/tag/v1.0.107) | 2018-09-16T19:51:07Z | 7,127 | 2,115 | 1,698 | 10,940
|
||||
[v1.0.106](https://github.com/laurent22/joplin/releases/tag/v1.0.106) | 2018-09-08T15:23:40Z | 4,529 | 1,440 | 309 | 6,278
|
||||
[v1.0.105](https://github.com/laurent22/joplin/releases/tag/v1.0.105) | 2018-09-05T11:29:36Z | 4,591 | 1,554 | 1,440 | 7,585
|
||||
[v1.0.104](https://github.com/laurent22/joplin/releases/tag/v1.0.104) | 2018-06-28T20:25:36Z | 14,990 | 4,656 | 7,057 | 26,703
|
||||
[v1.0.104](https://github.com/laurent22/joplin/releases/tag/v1.0.104) | 2018-06-28T20:25:36Z | 14,990 | 4,656 | 7,059 | 26,705
|
||||
[v1.0.103](https://github.com/laurent22/joplin/releases/tag/v1.0.103) | 2018-06-21T19:38:13Z | 2,006 | 858 | 668 | 3,532
|
||||
[v1.0.101](https://github.com/laurent22/joplin/releases/tag/v1.0.101) | 2018-06-17T18:35:11Z | 1,290 | 581 | 401 | 2,272
|
||||
[v1.0.100](https://github.com/laurent22/joplin/releases/tag/v1.0.100) | 2018-06-14T17:41:43Z | 857 | 410 | 228 | 1,495
|
||||
[v1.0.99](https://github.com/laurent22/joplin/releases/tag/v1.0.99) | 2018-06-10T13:18:23Z | 1,237 | 581 | 372 | 2,190
|
||||
[v1.0.97](https://github.com/laurent22/joplin/releases/tag/v1.0.97) | 2018-06-09T19:23:34Z | 297 | 138 | 54 | 489
|
||||
[v1.0.96](https://github.com/laurent22/joplin/releases/tag/v1.0.96) | 2018-05-26T16:36:39Z | 2,686 | 1,200 | 1,245 | 5,131
|
||||
[v1.0.96](https://github.com/laurent22/joplin/releases/tag/v1.0.96) | 2018-05-26T16:36:39Z | 2,686 | 1,200 | 1,250 | 5,136
|
||||
[v1.0.95](https://github.com/laurent22/joplin/releases/tag/v1.0.95) | 2018-05-25T13:04:30Z | 387 | 190 | 87 | 664
|
||||
[v1.0.94](https://github.com/laurent22/joplin/releases/tag/v1.0.94) | 2018-05-21T20:52:59Z | 1,099 | 557 | 362 | 2,018
|
||||
[v1.0.93](https://github.com/laurent22/joplin/releases/tag/v1.0.93) | 2018-05-14T11:36:01Z | 1,770 | 913 | 742 | 3,425
|
||||
[v1.0.93](https://github.com/laurent22/joplin/releases/tag/v1.0.93) | 2018-05-14T11:36:01Z | 1,770 | 919 | 742 | 3,431
|
||||
[v1.0.91](https://github.com/laurent22/joplin/releases/tag/v1.0.91) | 2018-05-10T14:48:04Z | 814 | 535 | 290 | 1,639
|
||||
[v1.0.89](https://github.com/laurent22/joplin/releases/tag/v1.0.89) | 2018-05-09T13:05:05Z | 476 | 212 | 97 | 785
|
||||
[v1.0.85](https://github.com/laurent22/joplin/releases/tag/v1.0.85) | 2018-05-01T21:08:24Z | 1,641 | 934 | 615 | 3,190
|
||||
[v1.0.83](https://github.com/laurent22/joplin/releases/tag/v1.0.83) | 2018-04-04T19:43:58Z | 4,546 | 2,419 | 2,631 | 9,596
|
||||
[v1.0.82](https://github.com/laurent22/joplin/releases/tag/v1.0.82) | 2018-03-31T19:16:31Z | 684 | 385 | 99 | 1,168
|
||||
[v1.0.81](https://github.com/laurent22/joplin/releases/tag/v1.0.81) | 2018-03-28T08:13:58Z | 985 | 568 | 747 | 2,300
|
||||
[v1.0.79](https://github.com/laurent22/joplin/releases/tag/v1.0.79) | 2018-03-23T18:00:11Z | 919 | 512 | 356 | 1,787
|
||||
[v1.0.78](https://github.com/laurent22/joplin/releases/tag/v1.0.78) | 2018-03-17T15:27:18Z | 1,302 | 840 | 847 | 2,989
|
||||
[v1.0.83](https://github.com/laurent22/joplin/releases/tag/v1.0.83) | 2018-04-04T19:43:58Z | 4,551 | 2,426 | 2,632 | 9,609
|
||||
[v1.0.82](https://github.com/laurent22/joplin/releases/tag/v1.0.82) | 2018-03-31T19:16:31Z | 684 | 386 | 99 | 1,169
|
||||
[v1.0.81](https://github.com/laurent22/joplin/releases/tag/v1.0.81) | 2018-03-28T08:13:58Z | 985 | 569 | 748 | 2,302
|
||||
[v1.0.79](https://github.com/laurent22/joplin/releases/tag/v1.0.79) | 2018-03-23T18:00:11Z | 919 | 512 | 357 | 1,788
|
||||
[v1.0.78](https://github.com/laurent22/joplin/releases/tag/v1.0.78) | 2018-03-17T15:27:18Z | 1,302 | 840 | 848 | 2,990
|
||||
[v1.0.77](https://github.com/laurent22/joplin/releases/tag/v1.0.77) | 2018-03-16T15:12:35Z | 165 | 87 | 25 | 277
|
||||
[v1.0.72](https://github.com/laurent22/joplin/releases/tag/v1.0.72) | 2018-03-14T09:44:35Z | 396 | 233 | 36 | 665
|
||||
[v1.0.72](https://github.com/laurent22/joplin/releases/tag/v1.0.72) | 2018-03-14T09:44:35Z | 396 | 234 | 36 | 666
|
||||
[v1.0.70](https://github.com/laurent22/joplin/releases/tag/v1.0.70) | 2018-02-28T20:04:30Z | 1,844 | 1,025 | 1,226 | 4,095
|
||||
[v1.0.67](https://github.com/laurent22/joplin/releases/tag/v1.0.67) | 2018-02-19T22:51:08Z | 1,805 | 581 | 0 | 2,386
|
||||
[v1.0.67](https://github.com/laurent22/joplin/releases/tag/v1.0.67) | 2018-02-19T22:51:08Z | 1,805 | 582 | 0 | 2,387
|
||||
[v1.0.66](https://github.com/laurent22/joplin/releases/tag/v1.0.66) | 2018-02-18T23:09:09Z | 314 | 107 | 72 | 493
|
||||
[v1.0.65](https://github.com/laurent22/joplin/releases/tag/v1.0.65) | 2018-02-17T20:02:25Z | 185 | 107 | 117 | 409
|
||||
[v1.0.64](https://github.com/laurent22/joplin/releases/tag/v1.0.64) | 2018-02-16T00:58:20Z | 1,074 | 529 | 1,116 | 2,719
|
||||
[v1.0.63](https://github.com/laurent22/joplin/releases/tag/v1.0.63) | 2018-02-14T19:40:36Z | 291 | 143 | 83 | 517
|
||||
[v1.0.62](https://github.com/laurent22/joplin/releases/tag/v1.0.62) | 2018-02-12T20:19:58Z | 549 | 276 | 354 | 1,179
|
||||
[v1.0.62](https://github.com/laurent22/joplin/releases/tag/v1.0.62) | 2018-02-12T20:19:58Z | 549 | 276 | 355 | 1,180
|
||||
[v0.10.61](https://github.com/laurent22/joplin/releases/tag/v0.10.61) | 2018-02-08T18:27:39Z | 963 | 605 | 944 | 2,512
|
||||
[v0.10.60](https://github.com/laurent22/joplin/releases/tag/v0.10.60) | 2018-02-06T13:09:56Z | 711 | 497 | 541 | 1,749
|
||||
[v0.10.54](https://github.com/laurent22/joplin/releases/tag/v0.10.54) | 2018-01-31T20:21:30Z | 1,811 | 1,440 | 308 | 3,559
|
||||
[v0.10.60](https://github.com/laurent22/joplin/releases/tag/v0.10.60) | 2018-02-06T13:09:56Z | 711 | 497 | 542 | 1,750
|
||||
[v0.10.54](https://github.com/laurent22/joplin/releases/tag/v0.10.54) | 2018-01-31T20:21:30Z | 1,811 | 1,441 | 309 | 3,561
|
||||
[v0.10.52](https://github.com/laurent22/joplin/releases/tag/v0.10.52) | 2018-01-31T19:25:18Z | 37 | 616 | 6 | 659
|
||||
[v0.10.51](https://github.com/laurent22/joplin/releases/tag/v0.10.51) | 2018-01-28T18:47:02Z | 1,316 | 1,581 | 318 | 3,215
|
||||
[v0.10.48](https://github.com/laurent22/joplin/releases/tag/v0.10.48) | 2018-01-23T11:19:51Z | 1,956 | 1,732 | 19 | 3,707
|
||||
[v0.10.48](https://github.com/laurent22/joplin/releases/tag/v0.10.48) | 2018-01-23T11:19:51Z | 1,956 | 1,732 | 20 | 3,708
|
||||
[v0.10.47](https://github.com/laurent22/joplin/releases/tag/v0.10.47) | 2018-01-16T17:27:17Z | 1,216 | 1,249 | 59 | 2,524
|
||||
[v0.10.43](https://github.com/laurent22/joplin/releases/tag/v0.10.43) | 2018-01-08T10:12:10Z | 3,427 | 2,327 | 1,197 | 6,951
|
||||
[v0.10.41](https://github.com/laurent22/joplin/releases/tag/v0.10.41) | 2018-01-05T20:38:12Z | 1,029 | 1,528 | 227 | 2,784
|
||||
[v0.10.40](https://github.com/laurent22/joplin/releases/tag/v0.10.40) | 2018-01-02T23:16:57Z | 1,586 | 1,752 | 326 | 3,664
|
||||
[v0.10.39](https://github.com/laurent22/joplin/releases/tag/v0.10.39) | 2017-12-11T21:19:44Z | 5,484 | 3,974 | 2,881 | 12,339
|
||||
[v0.10.43](https://github.com/laurent22/joplin/releases/tag/v0.10.43) | 2018-01-08T10:12:10Z | 3,427 | 2,328 | 1,198 | 6,953
|
||||
[v0.10.41](https://github.com/laurent22/joplin/releases/tag/v0.10.41) | 2018-01-05T20:38:12Z | 1,029 | 1,529 | 228 | 2,786
|
||||
[v0.10.40](https://github.com/laurent22/joplin/releases/tag/v0.10.40) | 2018-01-02T23:16:57Z | 1,586 | 1,752 | 327 | 3,665
|
||||
[v0.10.39](https://github.com/laurent22/joplin/releases/tag/v0.10.39) | 2017-12-11T21:19:44Z | 5,495 | 3,984 | 2,891 | 12,370
|
||||
[v0.10.38](https://github.com/laurent22/joplin/releases/tag/v0.10.38) | 2017-12-08T10:12:06Z | 1,041 | 1,213 | 299 | 2,553
|
||||
[v0.10.37](https://github.com/laurent22/joplin/releases/tag/v0.10.37) | 2017-12-07T19:38:05Z | 256 | 829 | 74 | 1,159
|
||||
[v0.10.36](https://github.com/laurent22/joplin/releases/tag/v0.10.36) | 2017-12-05T09:34:40Z | 1,008 | 1,340 | 430 | 2,778
|
||||
@ -85,7 +85,7 @@ Version | Date | Windows | macOS | Linux | Total
|
||||
[v0.10.30](https://github.com/laurent22/joplin/releases/tag/v0.10.30) | 2017-11-30T20:28:16Z | 711 | 1,352 | 410 | 2,473
|
||||
[v0.10.28](https://github.com/laurent22/joplin/releases/tag/v0.10.28) | 2017-11-30T01:07:46Z | 1,276 | 1,682 | 864 | 3,822
|
||||
[v0.10.26](https://github.com/laurent22/joplin/releases/tag/v0.10.26) | 2017-11-29T16:02:17Z | 180 | 685 | 255 | 1,120
|
||||
[v0.10.25](https://github.com/laurent22/joplin/releases/tag/v0.10.25) | 2017-11-24T14:27:49Z | 136 | 676 | 5,406 | 6,218
|
||||
[v0.10.25](https://github.com/laurent22/joplin/releases/tag/v0.10.25) | 2017-11-24T14:27:49Z | 136 | 676 | 5,425 | 6,237
|
||||
[v0.10.23](https://github.com/laurent22/joplin/releases/tag/v0.10.23) | 2017-11-21T19:38:41Z | 124 | 628 | 20 | 772
|
||||
[v0.10.22](https://github.com/laurent22/joplin/releases/tag/v0.10.22) | 2017-11-20T21:45:57Z | 76 | 627 | 11 | 714
|
||||
[v0.10.21](https://github.com/laurent22/joplin/releases/tag/v0.10.21) | 2017-11-18T00:53:15Z | 44 | 619 | 4 | 667
|
||||
|
Loading…
x
Reference in New Issue
Block a user