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

Compare commits

...

74 Commits

Author SHA1 Message Date
Laurent Cozic
d25d9b3f44 CLI v0.10.93 2018-02-06 13:17:10 +00:00
Laurent Cozic
9d762a4319 Android release v0.10.86 2018-02-06 13:01:20 +00:00
Laurent Cozic
18d94c7585 Electron release v0.10.60 2018-02-06 12:57:40 +00:00
Laurent Cozic
af82345eb8 Fixed tray icon and update issue 2018-02-06 13:11:59 +00:00
Laurent Cozic
1e94a22986 Merge branch 'master' of github.com:laurent22/joplin 2018-02-06 13:06:59 +00:00
Laurent Cozic
e19a8a99ff macOS: Fixed startup crash 2018-02-06 13:05:36 +00:00
Laurent Cozic
f975009e24 Update readme 2018-02-05 19:02:11 +00:00
Laurent Cozic
90640fafc7 Merge branch 'master' of github.com:laurent22/joplin 2018-02-05 18:49:58 +00:00
Laurent Cozic
42e0e1e5a5 Updated build instructions 2018-02-06 09:42:20 +00:00
Laurent Cozic
61f64fa933 Added Markdown doc 2018-02-05 22:53:10 +00:00
Laurent Cozic
0d0ffd6d27 Added Markdown doc 2018-02-05 22:50:17 +00:00
Laurent Cozic
023ccffd2e Electron release v0.10.59 2018-02-05 22:20:31 +00:00
Laurent Cozic
bc26098c7d Android release v0.10.85 2018-02-05 22:19:21 +00:00
Laurent Cozic
7257a71a18 Mobile: Fixes #159: Make sure text fields aren't hidden by keyboard on iOS 2018-02-05 18:32:59 +00:00
Laurent Cozic
8ad8b73585 Better error handling for Katex and handling of code blocks and inline 2018-02-05 17:55:35 +00:00
Laurent Cozic
9a06815db9 Electron release v0.10.58 2018-02-05 17:34:39 +00:00
Laurent Cozic
66947d4954 Fixing appveyor script 2018-02-05 17:34:31 +00:00
Laurent Cozic
3ec22185d5 Electron release v0.10.57 2018-02-05 17:32:54 +00:00
Laurent Cozic
0f05c23e26 Fixing deployment script 2018-02-05 17:32:48 +00:00
Laurent Cozic
74493fece0 Android release v0.10.83 2018-02-05 17:32:00 +00:00
Laurent Cozic
557a96e814 Electron release v0.10.56 2018-02-05 17:27:38 +00:00
Laurent Cozic
4b23b419a4 Electron release v0.10.55 2018-02-05 17:27:09 +00:00
Laurent Cozic
8b7f5b1151 Fix scrolling issue in Electron app 2018-02-04 18:45:52 +00:00
Laurent Cozic
29e9ccf216 Electron: Reverted to older Sharp version to fix Ubuntu issue 2018-02-04 18:31:13 +00:00
Laurent Cozic
2c04f5c8bc Improved Android and Electron release process 2018-02-04 18:05:07 +00:00
Laurent Cozic
5430a747e9 Improved Android and Electron release process 2018-02-04 17:55:22 +00:00
Laurent Cozic
13bc185829 Improved Android and Electron release process 2018-02-04 17:51:42 +00:00
Laurent Cozic
ed87581a8a Improved Android and Electron release process 2018-02-04 17:48:29 +00:00
Laurent Cozic
2645ec96a8 Fixed tool utils 2018-02-04 17:44:32 +00:00
Laurent Cozic
d278d830f0 Improved Android and Electron release process 2018-02-04 17:42:33 +00:00
Laurent Cozic
b4dce0ed46 All: Added Katex support 2018-02-04 17:12:24 +00:00
Laurent Cozic
e8416042d4 Merge branch 'master' into math-support 2018-02-02 20:35:32 +00:00
Laurent Cozic
70adbe5e76 Added flags 2018-02-01 20:21:54 +00:00
Laurent Cozic
f66be08d1d Added list of translated languages to README file 2018-02-01 20:15:31 +00:00
Laurent Cozic
fad96f5266 All: Added section to list missing master keys 2018-02-01 19:01:20 +00:00
Laurent Cozic
c33a7f5f47 Updated French translation 2018-02-02 00:08:37 +00:00
Laurent Cozic
28afbcde02 Updated translations 2018-02-02 00:02:47 +00:00
Laurent Cozic
691292d2b3 Merge branch 'master' of github.com:laurent22/joplin 2018-02-01 23:40:15 +00:00
Laurent Cozic
30ff81064f All: Made WebDAV driver more generic to support services other than Nextcloud and added WebDAV sync target 2018-02-01 23:40:05 +00:00
Laurent Cozic
f9f398ad98 Merge pull request #194 from rtmkrlv/russian-locale
Update Russian translation
2018-02-01 15:26:15 +01:00
rtmkrlv
537884bdcd Merge branch 'master' into russian-locale 2018-02-01 15:50:39 +02:00
rtmkrlv
d54400a7cb Updated Russian translation 2018-02-01 15:43:26 +02:00
Laurent Cozic
42c78264fb Update website 2018-01-31 20:21:38 +00:00
Laurent Cozic
c52da82447 Electron: Fix: Don't allow adding notes and to-do to conflict notebook 2018-01-31 20:19:11 +00:00
Laurent Cozic
cca43624e4 Electron: Added tray icon support 2018-01-31 20:10:32 +00:00
Laurent Cozic
dac1cd7668 Mobile: Allow filtering log by warning/error 2018-01-31 19:51:29 +00:00
Laurent Cozic
b4c00db0e3 Electron release v0.10.54 2018-01-31 19:40:40 +00:00
Laurent Cozic
3ce393a8b2 Electron release v0.10.53 2018-01-31 19:34:47 +00:00
Laurent Cozic
2b627fe4ab Fixed auto-update check 2018-01-31 19:34:38 +00:00
Laurent Cozic
fcf8a1649d CLI v0.10.92 2018-01-31 19:28:31 +00:00
Laurent Cozic
8d3b050831 Update website 2018-01-31 19:26:21 +00:00
Laurent Cozic
43297ef0a3 Updated translations 2018-01-31 19:14:32 +00:00
Laurent Cozic
551fabdfc9 Tweak and error handling on auto-update 2018-01-31 19:10:45 +00:00
Laurent Cozic
d6de56b2db All: Fixed crash when having invalid UTF-8 string in text editor 2018-01-31 19:01:11 +00:00
Laurent Cozic
9e979804f3 Electron release v0.10.52 2018-01-31 17:53:18 +00:00
Laurent Cozic
b8e0f182cc Android release v0.10.81 2018-01-31 17:51:22 +00:00
Laurent Cozic
9a41b9e192 Electron: Improved auto-update process to avoid random crashes 2018-01-30 22:35:50 +00:00
Laurent Cozic
9b8f520b9f Electron: Allow focusing either title or body when creating a new note or to-odo 2018-01-30 21:49:22 +00:00
Laurent Cozic
5b6019805c Electron: Fixed auto-title when title is manually entered first 2018-01-30 21:36:54 +00:00
Laurent Cozic
a4106436c4 Fixed delta function when processing many items 2018-01-30 21:10:54 +00:00
Laurent Cozic
f6b4eb511e Add Content-Size header for WebDAV, which is required by some services 2018-01-30 20:24:09 +00:00
Laurent Cozic
eb67ac17a0 Allow decryption to continue even if an item cannot be decrypted 2018-01-30 20:15:05 +00:00
Laurent Cozic
7b760d03ef All: Handle case where file is left half-uploaded on Nextcloud instance (possibly an ocloud.de issue only) 2018-01-30 20:10:36 +00:00
Laurent Cozic
2805ae2acf Fixed crash when calling fetch() with invalid URL in RN app 2018-01-30 19:01:07 +00:00
Laurent Cozic
5cb5ccc781 All: Optimised Nextcloud sync delta functionality 2018-01-29 20:51:14 +00:00
Laurent Cozic
0dba2821b6 Merge pull request #189 from strobeltobias/patch-2
Update German translation
2018-01-29 18:59:20 +00:00
Tobias Strobel
1db7825b22 Update German translation
Grammar and wording fixes, translating new strings.
2018-01-29 14:24:50 +01:00
Laurent Cozic
8a92d6ad70 Update website 2018-01-28 18:47:22 +00:00
Laurent Cozic
138ad9fcad Mobile: Fixes #114: Update geolocation in metadata after it has been set 2018-01-28 18:42:43 +00:00
Laurent Cozic
08cb518c25 Check if current folder exists 2018-01-28 18:19:56 +00:00
Laurent Cozic
6d04eab200 Merge pull request #185 from pf-siedler/autocomp_file_and_item
Autocompletion for files and items
2018-01-28 18:17:01 +00:00
pf-siedler
7bccf7f65d Implement auto completaion for <item> usage 2018-01-28 23:12:54 +09:00
pf-siedler
c62a24a9cb Implement auto completion for <file> usage
Suggest file names from current directory.
2018-01-28 22:59:58 +09:00
Laurent Cozic
53da63e371 Trying to add math support 2018-01-11 19:51:01 +00:00
100 changed files with 2943 additions and 705 deletions

5
.gitignore vendored
View File

@@ -37,4 +37,7 @@ _mydocs
Assets/DownloadBadges*.psd
node_modules
Tools/github_oauth_token.txt
_releases
_releases
ReactNativeClient/lib/csstojs/
ElectronClient/app/gui/note-viewer/fonts/
Tools/commit_hook.txt

View File

@@ -1,5 +1,5 @@
# Only build tags (Doesn't work - doesn't build anything)
# if: tag IS present
if: tag IS present
rvm: 2.3.3
@@ -46,7 +46,8 @@ before_install:
script:
- |
cd ElectronClient/app
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
cd Tools
npm install
yarn dist
cd ../ElectronClient/app
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
npm install && yarn dist

View File

@@ -17,6 +17,15 @@ If you get a node-gyp related error you might need to manually install it: `npm
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
# Building the tools
Before building any of the applications, you need to build the tools:
```
cd Tools
npm install
```
# Building the Electron application
```

View File

@@ -4,6 +4,7 @@ var Folder = require('lib/models/Folder.js');
var Tag = require('lib/models/Tag.js');
var { cliUtils } = require('./cli-utils.js');
var yargParser = require('yargs-parser');
var fs = require('fs-extra');
async function handleAutocompletionPromise(line) {
// Auto-complete the command name
@@ -71,8 +72,10 @@ async function handleAutocompletionPromise(line) {
let argName = cmdUsage[positionalArgs - 1];
argName = cliUtils.parseCommandArg(argName).name;
if (argName == 'note' || argName == 'note-pattern' && app().currentFolder()) {
const notes = await Note.previews(app().currentFolder().id, { titlePattern: next + '*' });
const currentFolder = app().currentFolder();
if (argName == 'note' || argName == 'note-pattern') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
l.push(...notes.map((n) => n.title));
}
@@ -81,11 +84,22 @@ async function handleAutocompletionPromise(line) {
l.push(...folders.map((n) => n.title));
}
if (argName == 'item') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
const folders = await Folder.search({ titlePattern: next + '*' });
l.push(...notes.map((n) => n.title), folders.map((n) => n.title));
}
if (argName == 'tag') {
let tags = await Tag.search({ titlePattern: next + '*' });
l.push(...tags.map((n) => n.title));
}
if (argName == 'file') {
let files = await fs.readdir('.');
l.push(...files);
}
if (argName == 'tag-command') {
let c = filterList(['add', 'remove', 'list'], next);
l.push(...c);

View File

@@ -101,7 +101,7 @@ class Command extends BaseCommand {
this.releaseLockFn_ = null;
// Lock is unique per profile/database
const lockFilePath = osTmpdir() + '/synclock_' + md5(Setting.value('profileDir'));
const lockFilePath = osTmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');
try {

View File

@@ -7,13 +7,13 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Samuel Blickle <blickle.samuel@gmail.com>\n"
"Last-Translator: Tobias Strobel <git@strobeltobias.de>\n"
"Language-Team: \n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.5\n"
"X-Generator: Poedit 2.0.6\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Give focus to next pane"
@@ -50,7 +50,7 @@ msgstr ""
"soll."
msgid "Set a to-do as completed / not completed"
msgstr "Ein To-Do as abgeschlossen / nicht abgeschlossen markieren"
msgstr "Ein To-Do als abgeschlossen / nicht abgeschlossen markieren"
#, fuzzy
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
@@ -117,7 +117,7 @@ msgid "The command \"%s\" is only available in GUI mode"
msgstr "Der Befehl \"%s\" ist nur im GUI Modus verfügbar"
msgid "Cannot change encrypted item"
msgstr ""
msgstr "Kann verschlüsseltes Objekt nicht ändern"
#, javascript-format
msgid "Missing required argument: %s"
@@ -187,31 +187,34 @@ msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Verwaltet die E2EE-Konfiguration. Die Befehle sind `enable`, `disable`, "
"`decrypt`, `status` und `target-status`."
msgid "Enter master password:"
msgstr ""
msgstr "Master-Passwort eingeben:"
msgid "Operation cancelled"
msgstr ""
msgstr "Vorgang abgebrochen"
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
"Entschlüsselung starten.... Warte bitte, da es einige Minuten dauern kann, "
"je nachdem, wie viel es zu entschlüsseln gibt."
msgid "Completed decryption."
msgstr ""
msgstr "Entschlüsselung abgeschlossen."
#, fuzzy
msgid "Enabled"
msgstr "Deaktiviert"
msgstr "Aktiviert"
msgid "Disabled"
msgstr "Deaktiviert"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgstr "Die Verschlüsselung ist: %s"
msgid "Edit note."
msgstr "Notiz bearbeiten."
@@ -236,7 +239,7 @@ msgstr ""
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgstr "Fehler beim Öffnen der Notiz im Editor: %s"
msgid "Note has been saved."
msgstr "Die Notiz wurde gespeichert."
@@ -267,13 +270,12 @@ msgstr "Zeigt die Nutzungsstatistik an."
msgid "Shortcuts are not available in CLI mode."
msgstr "Tastenkürzel sind im CLI Modus nicht verfügbar."
#, fuzzy
msgid ""
"Type `help [command]` for more information about a command; or type `help "
"all` for the complete usage information."
msgstr ""
"Tippe `help [Befehl]` ein, um mehr Informationen über einen Befehl zu "
"erhalten."
"Tippe `help [Befehl]` für weitere Informationen über einen Befehl; oder "
"tippe `help all` für die vollständigen Informationen zur Befehlsverwendung."
msgid "The possible commands are:"
msgstr "Mögliche Befehle sind:"
@@ -483,9 +485,10 @@ msgstr ""
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
"Keine Authentifizierung mit %s. Gib bitte alle fehlenden Zugangsdaten an."
msgid "Synchronisation is already in progress."
msgstr "Synchronisation ist bereits im Gange."
msgstr "Synchronisation wird bereits ausgeführt."
#, javascript-format
msgid ""
@@ -508,9 +511,8 @@ msgid "Starting synchronisation..."
msgstr "Starte Synchronisation..."
msgid "Cancelling... Please wait."
msgstr "Breche ab... Bitte warten."
msgstr "Abbrechen... Bitte warten."
#, 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 "
@@ -519,7 +521,7 @@ msgstr ""
"<tag-command> kann \"add\", \"remove\" or \"list\" sein, um eine "
"[Markierung] zu [Notiz] zuzuweisen oder zu entfernen, oder um mit "
"[Markierung] markierte Notizen anzuzeigen. Mit dem Befehl `tag list` können "
"alle Notizen angezeigt werden."
"alle Markierungen angezeigt werden."
#, javascript-format
msgid "Invalid command: \"%s\""
@@ -554,7 +556,7 @@ msgid "%s %s (%s)"
msgstr "%s %s (%s)"
msgid "Enum"
msgstr ""
msgstr "Aufzählung"
#, javascript-format
msgid "Type: %s."
@@ -620,6 +622,10 @@ msgid ""
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
"Ein oder mehrere Objekte sind derzeit verschlüsselt und es kann erforderlich "
"sein, ein Master-Passwort zu hinterlegen. Gib dazu bitte `e2ee decrypt` ein. "
"Wenn du das Passwort bereits eingegeben hast, werden die verschlüsselten "
"Objekte im Hintergrund entschlüsselt und stehen in Kürze zur Verfügung."
msgid "File"
msgstr "Datei"
@@ -664,11 +670,10 @@ msgid "Synchronisation status"
msgstr "Status der Synchronisation"
msgid "Encryption options"
msgstr ""
msgstr "Verschlüsselungsoptionen"
#, fuzzy
msgid "General Options"
msgstr "Optionen"
msgstr "Allgemeine Einstellungen"
msgid "Help"
msgstr "Hilfe"
@@ -676,6 +681,9 @@ msgstr "Hilfe"
msgid "Website and documentation"
msgstr "Webseite und Dokumentation"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Über Joplin"
@@ -683,12 +691,46 @@ msgstr "Über Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Auf %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Abbrechen"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Notizen und Einstellungen gespeichert in: %s"
@@ -701,6 +743,9 @@ msgid ""
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
"continue?"
msgstr ""
"Durch die Deaktivierung der Verschlüsselung werden *alle* Notizen und "
"Anhänge neu synchronisiert und unverschlüsselt an das Synchronisierungsziel "
"gesendet. Möchtest du fortfahren?"
msgid ""
"Enabling encryption means *all* your notes and attachments are going to be "
@@ -708,15 +753,20 @@ msgid ""
"password as, for security purposes, this will be the *only* way to decrypt "
"the data! To enable encryption, please enter your password below."
msgstr ""
"Durch das Aktivieren der Verschlüsselung werden alle Notizen und Anhänge neu "
"synchronisiert und verschlüsselt an das Synchronisationsziel gesendet. Achte "
"darauf, dass du das Passwort nicht verlierst, da dies aus Sicherheitsgründen "
"die einzige Möglichkeit ist, deine Daten zu entschlüsseln! Um die "
"Verschlüsselung zu aktivieren, gib bitte unten dein Passwort ein."
msgid "Disable encryption"
msgstr ""
msgstr "Verschlüsselung deaktivieren"
msgid "Enable encryption"
msgstr ""
msgstr "Verschlüsselung aktivieren"
msgid "Master Keys"
msgstr ""
msgstr "Hauptschlüssel"
msgid "Active"
msgstr "Aktiv"
@@ -749,11 +799,21 @@ msgstr ""
"verwendet werden, abhängig davon, wie die jeweiligen Notizen oder "
"Notizbücher ursprünglich verschlüsselt wurden."
#, fuzzy
msgid "Missing Master Keys"
msgstr "Hauptschlüssel"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
msgid "Encryption is:"
msgstr ""
msgstr "Die Verschlüsselung ist:"
msgid "Back"
msgstr "Zurück"
@@ -795,12 +855,11 @@ msgstr "Manche Objekte können nicht synchronisiert werden."
msgid "View them now"
msgstr "Zeige sie jetzt an"
#, fuzzy
msgid "Some items cannot be decrypted."
msgstr "Kann Synchronisierer nicht initialisieren."
msgstr "Einige Objekte können nicht entschlüsselt werden."
msgid "Set the password"
msgstr ""
msgstr "Setze ein Passwort"
msgid "Add or remove tags"
msgstr "Markierungen hinzufügen oder entfernen"
@@ -839,7 +898,7 @@ msgid "Refresh"
msgstr "Aktualisieren"
msgid "Clear"
msgstr ""
msgstr "Leeren"
msgid "OneDrive Login"
msgstr "OneDrive Login"
@@ -854,7 +913,7 @@ msgid "Synchronisation Status"
msgstr "Synchronisations Status"
msgid "Encryption Options"
msgstr ""
msgstr "Verschlüsselungsoptionen"
msgid "Remove this tag from all the notes?"
msgstr "Diese Markierung von allen Notizen entfernen?"
@@ -893,7 +952,7 @@ msgid "File system"
msgstr "Dateisystem"
msgid "Nextcloud (Beta)"
msgstr ""
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
@@ -901,6 +960,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Nur für Tests)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Unbekanntes Log Level: %s"
@@ -959,16 +1021,16 @@ msgstr "Lokale Objekte gelöscht: %d."
msgid "Deleted remote items: %d."
msgstr "Remote Objekte gelöscht: %d."
#, fuzzy, javascript-format
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Lokale Objekte erstellt: %d."
msgstr "Geladene Objekte: %d/%d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Status: \"%s\"."
msgid "Cancelling..."
msgstr "Breche ab..."
msgstr "Abbrechen..."
#, javascript-format
msgid "Completed: %s"
@@ -979,11 +1041,10 @@ msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronisation ist bereits im Gange. Status: %s"
msgid "Encrypted"
msgstr ""
msgstr "Verschlüsselt"
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Manche Objekte können nicht synchronisiert werden."
msgstr "Verschlüsselte Objekte können nicht verändert werden."
msgid "Conflicts"
msgstr "Konflikte"
@@ -1046,9 +1107,27 @@ msgstr "Zeige unvollständige To-Dos oben in der Liste"
msgid "Save geo-location with notes"
msgstr "Momentanen Standort zusammen mit Notizen speichern"
msgid "Set application zoom percentage"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Erstellt ein neues To-Do."
#, fuzzy
msgid "Focus title"
msgstr "Notiz Titel:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Erstellt eine neue Notiz."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr "Einstellen des Anwendungszooms"
msgid "Automatically update the application"
msgstr "Die Applikation automatisch aktualisieren"
@@ -1077,6 +1156,9 @@ msgid ""
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"Das Ziel, mit dem synchronisiert werden soll. Jedes Synchronisationsziel "
"kann zusätzliche Parameter haben, die als `sync.NUM.NAME` (alle unten "
"dokumentiert) bezeichnet werden."
msgid "Directory to synchronise with (absolute path)"
msgstr "Verzeichnis zum synchronisieren (absoluter Pfad)"
@@ -1085,17 +1167,29 @@ msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
"See `sync.target`."
msgstr ""
"Der Pfad, mit dem synchronisiert wird, wenn Dateisystem-Synchronisation "
"aktiviert ist. Siehe `sync.target`."
"Der Pfad, mit dem synchronisiert werden soll, wenn die Dateisystem-"
"Synchronisation aktiviert ist. Siehe `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgstr "Nexcloud WebDAV URL"
msgid "Nexcloud username"
msgstr ""
msgstr "Nexcloud Benutzername"
msgid "Nexcloud password"
msgstr ""
msgstr "Nexcloud Passwort"
#, fuzzy
msgid "WebDAV URL"
msgstr "Nexcloud WebDAV URL"
#, fuzzy
msgid "WebDAV username"
msgstr "Nexcloud Benutzername"
#, fuzzy
msgid "WebDAV password"
msgstr "Setze ein Passwort"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
@@ -1104,15 +1198,18 @@ msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
msgid "Items that cannot be synchronised"
msgstr "Objekte können nicht synchronisiert werden"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s %s (%s)"
msgstr "%s (%s): %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
"target. In order to find these items, either search for the title or the ID "
"(which is displayed in brackets above)."
msgstr ""
"Diese Objekte verbleiben auf dem Gerät, werden aber nicht zum "
"Synchronisationsziel hochgeladen. Um diese Objekte zu finden, suchen Sie "
"entweder nach dem Titel oder der ID (die oben in Klammern angezeigt wird)."
msgid "Sync status (synced items / total items)"
msgstr "Synchronisationsstatus (synchronisierte Objekte / gesamte Objekte)"
@@ -1156,13 +1253,13 @@ msgid "Delete these notes?"
msgstr "Sollen diese Notizen gelöscht werden?"
msgid "Log"
msgstr "Log"
msgstr "Protokoll"
msgid "Export Debug Report"
msgstr "Fehlerbreicht exportieren"
msgstr "Fehlerbericht exportieren"
msgid "Encryption Config"
msgstr ""
msgstr "Verschlüsselungskonfiguration"
msgid "Configuration"
msgstr "Konfiguration"
@@ -1175,7 +1272,7 @@ msgid "Move %d notes to notebook \"%s\"?"
msgstr "%d Notizen in das Notizbuch \"%s\" verschieben?"
msgid "Press to set the decryption password."
msgstr ""
msgstr "Tippe hier, um das Entschlüsselungspasswort festzulegen."
msgid "Select date"
msgstr "Datum auswählen"
@@ -1188,22 +1285,20 @@ msgstr "Synchronisation abbrechen"
#, javascript-format
msgid "Master Key %s"
msgstr ""
msgstr "Hauptschlüssel %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Created: %s"
msgstr "Erstellt: %d."
msgstr "Erstellt: %s"
#, fuzzy
msgid "Password:"
msgstr "Passwort"
msgstr "Passwort:"
msgid "Password cannot be empty"
msgstr ""
msgstr "Passwort darf nicht leer sein"
#, fuzzy
msgid "Enable"
msgstr "Deaktiviert"
msgstr "Aktivieren"
#, javascript-format
msgid "The notebook could not be saved: %s"
@@ -1212,6 +1307,12 @@ msgstr "Dieses Notizbuch konnte nicht gespeichert werden: %s"
msgid "Edit notebook"
msgstr "Notizbuch bearbeiten"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Diese Notiz wurde verändert:"
@@ -1276,9 +1377,6 @@ msgstr "Willkommen"
#~ "dem Dateisystem synchronisiert werden soll, setze den Wert zu `sync.2."
#~ "path`, um den Zielpfad zu spezifizieren."
#~ msgid "Note title:"
#~ msgstr "Notizen Titel:"
#~ msgid "To-do title:"
#~ msgstr "To-Do Titel:"

View File

@@ -589,6 +589,9 @@ msgstr ""
msgid "Website and documentation"
msgstr ""
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr ""
@@ -596,12 +599,45 @@ msgstr ""
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -658,6 +694,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr ""
@@ -802,6 +847,9 @@ msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -934,6 +982,21 @@ msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "When creating a new to-do:"
msgstr ""
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
msgid "When creating a new note:"
msgstr ""
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
@@ -983,6 +1046,15 @@ msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@@ -1094,6 +1166,12 @@ msgstr ""
msgid "Edit notebook"
msgstr ""
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr ""

View File

@@ -647,6 +647,9 @@ msgstr "Ayuda"
msgid "Website and documentation"
msgstr "Sitio web y documentacion"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Acerca de Joplin"
@@ -654,12 +657,46 @@ msgstr "Acerca de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "En %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "Ok"
msgid "Cancel"
msgstr "Cancelar"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -718,6 +755,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Estatus"
@@ -872,6 +918,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev(Solo para pruebas)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nivel de log desconocido: %s"
@@ -1020,6 +1069,24 @@ msgstr "Mostrar lista de tareas incompletas al inio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar notas con geo-licalización"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Crea una nueva lista de tareas."
#, fuzzy
msgid "Focus title"
msgstr "Título de nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Crea una nueva nota."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
@@ -1071,6 +1138,15 @@ msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Valor inválido de opción: \"%s\". Los válores inválidos son: %s."
@@ -1188,6 +1264,12 @@ msgstr "Esta libreta no pudo ser guardada: %s"
msgid "Edit notebook"
msgstr "Editar libreta"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Esta nota ha sido modificada:"
@@ -1253,9 +1335,6 @@ msgstr "Bienvenido"
#~ "El objetivo para sincronizarse a. Si sincronizando con el sistema de "
#~ "archivos, establecer `sync.2.path` especifique el directorio destino."
#~ msgid "Note title:"
#~ msgstr "Título de nota:"
#~ msgid "To-do title:"
#~ msgstr "Títuto de lista de tareas:"

View File

@@ -661,6 +661,9 @@ msgstr "Ayuda"
msgid "Website and documentation"
msgstr "Sitio web y documentación"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Acerca de Joplin"
@@ -668,12 +671,46 @@ msgstr "Acerca de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "En %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Cancelar"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Las notas y los ajustes se guardan en: %s"
@@ -730,6 +767,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Estado"
@@ -875,6 +921,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Solo para pruebas)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nivel de log desconocido: %s"
@@ -1019,6 +1068,24 @@ msgstr "Mostrar tareas incompletas al inicio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar geolocalización en las notas"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Crea una nueva lista de tareas."
#, fuzzy
msgid "Focus title"
msgstr "Título de la nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Crea una nueva nota."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
@@ -1070,6 +1137,15 @@ msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Opción inválida: «%s». Los valores posibles son: %s."
@@ -1182,6 +1258,12 @@ msgstr "No se ha podido guardar esta libreta: %s"
msgid "Edit notebook"
msgstr "Editar libreta"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Esta nota ha sido modificada:"
@@ -1243,9 +1325,6 @@ msgstr "Bienvenido"
#~ "El destino de la sincronización. Si se sincroniza con el sistema de "
#~ "archivos, indique el directorio destino en «sync.2.path»."
#~ msgid "Note title:"
#~ msgstr "Título de la nota:"
#~ msgid "To-do title:"
#~ msgstr "Títuto de lista de tareas:"

View File

@@ -7,13 +7,15 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Last-Translator: Laurent Cozic\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.3\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
msgid "Give focus to next pane"
msgstr "Activer le volet suivant"
@@ -664,6 +666,9 @@ msgstr "Aide"
msgid "Website and documentation"
msgstr "Documentation en ligne"
msgid "Check for updates..."
msgstr "Vérifier les mises à jour..."
msgid "About Joplin"
msgstr "A propos de Joplin"
@@ -671,12 +676,48 @@ msgstr "A propos de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, javascript-format
msgid "Open %s"
msgstr "Ouvrir %s"
msgid "Exit"
msgstr "Quitter"
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Annuler"
msgid "Error"
msgstr "Erreur"
msgid "An update is available, do you want to update now?"
msgstr ""
"Une mise à jour est disponible, souhaitez vous la télécharger maintenant ?"
msgid "Yes"
msgstr "Oui"
msgid "No"
msgstr "Non"
#, javascript-format
msgid "Could not download the update: %s"
msgstr "Impossible de télécharger la mise à jour : %s"
msgid "Current version is up-to-date."
msgstr "La version actuelle est à jour."
msgid "New version downloaded - application will quit now and update..."
msgstr ""
"La nouvelle version a été téléchargée - le programme va se fermer et se "
"mettre à jour..."
#, javascript-format
msgid "Could not install the update: %s"
msgstr "Impossible d'installer la mise à jour : %s"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Les notes et paramètres se trouve dans : %s"
@@ -745,6 +786,18 @@ msgstr ""
"pour le décryptage, selon la façon dont les notes ou carnets étaient cryptés "
"à l'origine."
msgid "Missing Master Keys"
msgstr "Clefs maître manquantes"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
"Les clefs maître avec ces identifiants sont utilisées pour crypter certains "
"de vos objets, cependant le logiciel n'y a pour l'instant pas accès. Il est "
"probable qu'elle vont être prochainement disponible via la synchronisation."
msgid "Status"
msgstr "État"
@@ -895,6 +948,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dév (Pour tester uniquement)"
msgid "WebDAV (Beta)"
msgstr "WebDAV (Bêta)"
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Paramètre inconnu : %s"
@@ -1037,6 +1093,21 @@ msgstr "Tâches non-terminées en haut des listes"
msgid "Save geo-location with notes"
msgstr "Enregistrer l'emplacement avec les notes"
msgid "When creating a new to-do:"
msgstr "Lors de la création d'une tâche :"
msgid "Focus title"
msgstr "Curseur sur le titre"
msgid "Focus body"
msgstr "Curseur sur corps du message"
msgid "When creating a new note:"
msgstr "Lors de la création d'une note :"
msgid "Show tray icon"
msgstr "Afficher icône dans la zone de notifications"
msgid "Set application zoom percentage"
msgstr "Niveau de zoom"
@@ -1091,6 +1162,15 @@ msgstr "Nextcloud : Nom utilisateur"
msgid "Nexcloud password"
msgstr "Nextcloud : Mot de passe"
msgid "WebDAV URL"
msgstr "WebDAV : URL"
msgid "WebDAV username"
msgstr "WebDAV : Nom utilisateur"
msgid "WebDAV password"
msgstr "WebDAV : Mot de passe"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
@@ -1165,7 +1245,7 @@ msgid "Configuration"
msgstr "Configuration"
msgid "Move to notebook..."
msgstr "Déplacer la note vers carnet..."
msgstr "Déplacer vers..."
#, javascript-format
msgid "Move %d notes to notebook \"%s\"?"
@@ -1207,6 +1287,12 @@ msgstr "Ce carnet n'a pas pu être sauvegardé : %s"
msgid "Edit notebook"
msgstr "Éditer le carnet"
msgid "Show all"
msgstr "Afficher tous"
msgid "Errors only"
msgstr "Erreurs seulement"
msgid "This note has been modified:"
msgstr "Cette note a été modifiée :"
@@ -1269,9 +1355,6 @@ msgstr "Bienvenue"
#~ "La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
#~ "fichier, veuillez spécifier le répertoire avec `sync.2.path`."
#~ msgid "Note title:"
#~ msgstr "Titre de la note :"
#~ msgid "To-do title:"
#~ msgstr "Titre de la tâche :"
@@ -1336,9 +1419,6 @@ msgstr "Bienvenue"
#~ msgid "Todo filter"
#~ msgstr "Filtre des tâches"
#~ msgid "Show all"
#~ msgstr "Afficher tous"
#~ msgid "Non-completed and recently completed ones"
#~ msgstr "Tâches non-complétées et récentes"

View File

@@ -669,6 +669,9 @@ msgstr "Pomoć"
msgid "Website and documentation"
msgstr "Website i dokumentacija"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "O Joplinu"
@@ -676,12 +679,46 @@ msgstr "O Joplinu"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "On %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "U redu"
msgid "Cancel"
msgstr "Odustani"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Bilješke i postavke su pohranjene u: %s"
@@ -738,6 +775,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
@@ -885,6 +931,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Samo za testiranje)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nepoznata razina logiranja: %s"
@@ -1026,6 +1075,24 @@ msgstr "Prikaži nezavršene zadatke na vrhu liste"
msgid "Save geo-location with notes"
msgstr "Spremi geolokacijske podatke sa bilješkama"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Stvara novi zadatak."
#, fuzzy
msgid "Focus title"
msgstr "Naslov bilješke:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Stvara novu bilješku."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
@@ -1077,6 +1144,15 @@ msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s."
@@ -1189,6 +1265,12 @@ msgstr "Bilježnicu nije moguće snimiti: %s"
msgid "Edit notebook"
msgstr "Uredi bilježnicu"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Bilješka je promijenjena:"
@@ -1249,9 +1331,6 @@ msgstr "Dobro došli"
#~ "Meta sinkronizacije. U slučaju sinkroniziranja s vlastitim datotečnim "
#~ "sustavom, postavi `sync.2.path` na ciljani direktorij."
#~ msgid "Note title:"
#~ msgstr "Naslov bilješke:"
#~ msgid "To-do title:"
#~ msgstr "Naslov zadatka:"

View File

@@ -647,6 +647,9 @@ msgstr "Aiuto"
msgid "Website and documentation"
msgstr "Sito web e documentazione"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Informazione si Joplin"
@@ -654,12 +657,46 @@ msgstr "Informazione si Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Su %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Cancella"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -718,6 +755,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Stato"
@@ -865,6 +911,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (solo per test)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Livello di log sconosciuto: %s"
@@ -1008,6 +1057,24 @@ msgstr "Mostra todo inclompleti in cima alla lista"
msgid "Save geo-location with notes"
msgstr "Salva geo-localizzazione con le note"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Crea una nuova attività."
#, fuzzy
msgid "Focus title"
msgstr "Titolo della Nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Crea una nuova nota."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
@@ -1059,6 +1126,15 @@ msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Oprione non valida: \"%s\". I valori possibili sono: %s."
@@ -1171,6 +1247,12 @@ msgstr "Il blocco note non può essere salvato: %s"
msgid "Edit notebook"
msgstr "Modifica blocco note"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Questa note è stata modificata:"
@@ -1234,9 +1316,6 @@ msgstr "Benvenuto"
#~ "system, impostare ' Sync. 2. Path ' per specificare la directory di "
#~ "destinazione."
#~ msgid "Note title:"
#~ msgstr "Titolo della Nota:"
#~ msgid "To-do title:"
#~ msgstr "Titolo dell'attività:"

View File

@@ -646,6 +646,9 @@ msgstr "ヘルプ"
msgid "Website and documentation"
msgstr "Webサイトとドキュメント"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Joplinについて"
@@ -653,12 +656,45 @@ msgstr "Joplinについて"
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr "キャンセル"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "ノートと設定は、%sに保存されます。"
@@ -719,6 +755,15 @@ msgstr ""
"注意:\"active\"に指定されたマスターキーのみが暗号化に使用されます。暗号化に"
"使用されたキーの応じて、すべてのキーが暗号解除のために使用されます。"
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "状態"
@@ -866,6 +911,9 @@ msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -1011,6 +1059,24 @@ msgstr "未完のToDoをリストの上部に表示"
msgid "Save geo-location with notes"
msgstr "ノートに位置情報を保存"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "新しいToDoを作成します。"
#, fuzzy
msgid "Focus title"
msgstr "ノートの題名:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "あたらしいノートを作成します。"
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
@@ -1062,6 +1128,15 @@ msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "無効な設定値: \"%s\"。有効な値は: %sです。"
@@ -1174,6 +1249,12 @@ msgstr "ノートブックは保存できませんでした:%s"
msgid "Edit notebook"
msgstr "ノートブックの編集"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "ノートは変更されています:"
@@ -1236,8 +1317,5 @@ msgstr "ようこそ"
#~ "同期先です。ローカルのファイルシステムと同期する場合は、`sync.2.path`を同"
#~ "期先のディレクトリに設定してください。"
#~ msgid "Note title:"
#~ msgstr "ノートの題名:"
#~ msgid "To-do title:"
#~ msgstr "ToDoの題名:"

View File

@@ -589,6 +589,9 @@ msgstr ""
msgid "Website and documentation"
msgstr ""
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr ""
@@ -596,12 +599,45 @@ msgstr ""
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -658,6 +694,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr ""
@@ -802,6 +847,9 @@ msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -934,6 +982,21 @@ msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "When creating a new to-do:"
msgstr ""
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
msgid "When creating a new note:"
msgstr ""
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
@@ -983,6 +1046,15 @@ msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@@ -1094,6 +1166,12 @@ msgstr ""
msgid "Edit notebook"
msgstr ""
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr ""

View File

@@ -664,6 +664,9 @@ msgstr "Help"
msgid "Website and documentation"
msgstr "Website en documentatie"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Over Joplin"
@@ -671,12 +674,46 @@ msgstr "Over Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Op %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Annuleer"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Notities en instellingen zijn opgeslaan in %s"
@@ -744,6 +781,16 @@ msgstr ""
"(aangeduid met \"active\"). Alle sleutels kunnen gebruikt worden voor "
"decodering, afhankelijk van hoe de notitieboeken initieel versleuteld zijn."
#, fuzzy
msgid "Missing Master Keys"
msgstr "Hoofdsleutels"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
@@ -892,6 +939,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Alleen voor testen)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Onbekend log level: %s"
@@ -1037,6 +1087,23 @@ msgstr "Toon onvoltooide to-do's aan de top van de lijsten"
msgid "Save geo-location with notes"
msgstr "Sla geo-locatie op bij notities"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Maakt nieuwe to-do aan."
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Maakt een nieuwe notitie aan."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
@@ -1089,6 +1156,16 @@ msgstr ""
msgid "Nexcloud password"
msgstr "Stel wachtwoord in"
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
#, fuzzy
msgid "WebDAV password"
msgstr "Stel wachtwoord in"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ongeldige optie: \"%s\". Geldige waarden zijn: %s."
@@ -1204,6 +1281,12 @@ msgstr "Het notitieboek kon niet opgeslaan worden: %s"
msgid "Edit notebook"
msgstr "Bewerk notitieboek"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Deze notitie werd aangepast:"

View File

@@ -642,6 +642,9 @@ msgstr "Ajuda"
msgid "Website and documentation"
msgstr "Website e documentação"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Sobre o Joplin"
@@ -649,12 +652,46 @@ msgstr "Sobre o Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Em %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Cancelar"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -713,6 +750,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
@@ -863,6 +909,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (apenas para testes)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nível de log desconhecido: %s"
@@ -1007,6 +1056,24 @@ msgstr "Mostrar tarefas incompletas no topo das listas"
msgid "Save geo-location with notes"
msgstr "Salvar geolocalização com notas"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Cria uma nova tarefa."
#, fuzzy
msgid "Focus title"
msgstr "Título da nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Cria uma nova nota."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
@@ -1058,6 +1125,15 @@ msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Valor da opção inválida: \"%s\". Os valores possíveis são: %s."
@@ -1170,6 +1246,12 @@ msgstr "O caderno não pôde ser salvo: %s"
msgid "Edit notebook"
msgstr "Editar caderno"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Esta nota foi modificada:"
@@ -1230,9 +1312,6 @@ msgstr "Bem-vindo"
#~ "O alvo para sincronizar. Se estiver sincronizando com o sistema de "
#~ "arquivos, configure `sync.2.path` para especificar o diretório de destino."
#~ msgid "Note title:"
#~ msgstr "Título da nota:"
#~ msgid "To-do title:"
#~ msgstr "Título da tarefa:"

View File

@@ -7,13 +7,13 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: rtmkrlv <artyom.karlov@gmail.com>\n"
"Last-Translator: Artyom Karlov <artyom.karlov@gmail.com>\n"
"Language-Team: \n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.5\n"
"X-Generator: Poedit 2.0.6\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
@@ -112,7 +112,7 @@ msgid "The command \"%s\" is only available in GUI mode"
msgstr "Команда «%s» доступна только в режиме GUI"
msgid "Cannot change encrypted item"
msgstr ""
msgstr "Не удалось изменить зашифрованный элемент"
#, javascript-format
msgid "Missing required argument: %s"
@@ -123,7 +123,7 @@ msgid "%s: %s"
msgstr "%s: %s"
msgid "Your choice: "
msgstr "Ваш выбор:"
msgstr "Ваш выбор: "
#, javascript-format
msgid "Invalid answer: %s"
@@ -180,22 +180,24 @@ msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Управляет конфигурацией E2EE. Команды: `enable`, `disable`, `decrypt`, "
"`status` и `target-status`."
#, fuzzy
msgid "Enter master password:"
msgstr "Установить пароль"
msgstr "Введите мастер-пароль:"
msgid "Operation cancelled"
msgstr ""
msgstr "Операция отменена"
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
"Запуск расшифровки... Пожалуйста, ожидайте. Время расшифровки зависит от "
"объёма расшифровываемых данных."
#, fuzzy
msgid "Completed decryption."
msgstr "Включить шифрование"
msgstr "Расшифровка завершена."
msgid "Enabled"
msgstr "Включено"
@@ -203,9 +205,9 @@ msgstr "Включено"
msgid "Disabled"
msgstr "Отключено"
#, fuzzy, javascript-format
#, javascript-format
msgid "Encryption is: %s"
msgstr "Шифрование:"
msgstr "Шифрование: %s"
msgid "Edit note."
msgstr "Редактировать заметку."
@@ -230,7 +232,7 @@ msgstr ""
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgstr "Ошибка при открытии заметки в редакторе: %s"
msgid "Note has been saved."
msgstr "Заметка сохранена."
@@ -281,7 +283,7 @@ msgstr ""
"элемент."
msgid "To move from one pane to another, press Tab or Shift+Tab."
msgstr "Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab"
msgstr "Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab."
msgid ""
"Use the arrows and page up/down to scroll the lists and text areas "
@@ -303,7 +305,7 @@ msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
msgstr ""
"Для просмотра списка доступных клавиатурных сочетаний введите `help "
"shortcuts`."
"shortcuts`"
msgid "Imports an Evernote notebook file (.enex file)."
msgstr "Импортирует файл блокнотов Evernote (.enex-файл)."
@@ -466,6 +468,7 @@ msgstr "Аутентификация не была завершена (не по
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
"Не аутентифицировано с %s. Пожалуйста, предоставьте все недостающие данные."
msgid "Synchronisation is already in progress."
msgstr "Синхронизация уже выполняется."
@@ -601,6 +604,10 @@ msgid ""
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
"Один или несколько элементов сейчас зашифрованы и может потребоваться, чтобы "
"вы предоставили мастер-пароль. Для этого введите, пожалуйста, «e2ee "
"decrypt». Если пароль уже был вами предоставлен, зашифрованные элементы "
"расшифруются в фоновом режиме и вскоре станут доступны."
msgid "File"
msgstr "Файл"
@@ -644,13 +651,11 @@ msgstr "Инструменты"
msgid "Synchronisation status"
msgstr "Статус синхронизации"
#, fuzzy
msgid "Encryption options"
msgstr "Настройки шифрования"
#, fuzzy
msgid "General Options"
msgstr "Настройки"
msgstr "Основные настройки"
msgid "Help"
msgstr "Помощь"
@@ -658,6 +663,9 @@ msgstr "Помощь"
msgid "Website and documentation"
msgstr "Сайт и документация"
msgid "Check for updates..."
msgstr "Проверить обновления..."
msgid "About Joplin"
msgstr "О Joplin"
@@ -665,12 +673,47 @@ msgstr "О Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "В %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Отмена"
msgid "Error"
msgstr "Ошибка"
msgid "An update is available, do you want to update now?"
msgstr "Доступно обновление. Обновить сейчас?"
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr "Не удалось загрузить обновление: %s"
msgid "Current version is up-to-date."
msgstr "Вы используете самую свежую версию."
msgid "New version downloaded - application will quit now and update..."
msgstr ""
"Новая версия загружена — приложение сейчас будет закрыто и обновлено..."
#, javascript-format
msgid "Could not install the update: %s"
msgstr "Не удалось установить обновление: %s"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Заметки и настройки сохранены в: %s"
@@ -739,6 +782,16 @@ msgstr ""
"ключей, в зависимости от того, как изначально были зашифрованы заметки или "
"блокноты."
#, fuzzy
msgid "Missing Master Keys"
msgstr "Мастер-ключи"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Статус"
@@ -875,7 +928,7 @@ msgid "File system"
msgstr "Файловая система"
msgid "Nextcloud (Beta)"
msgstr ""
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
@@ -883,6 +936,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (только для тестирования)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Неизвестный уровень лога: %s"
@@ -941,9 +997,9 @@ msgstr "Удалено локальных элементов: %d."
msgid "Deleted remote items: %d."
msgstr "Удалено удалённых элементов: %d."
#, fuzzy, javascript-format
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Создано локальных элементов: %d."
msgstr "Получено элементов: %d/%d."
#, javascript-format
msgid "State: \"%s\"."
@@ -960,13 +1016,11 @@ msgstr "Завершено: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Синхронизация уже выполняется. Статус: %s"
#, fuzzy
msgid "Encrypted"
msgstr "Шифрование:"
msgstr "Зашифровано"
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Некоторые элементы не могут быть синхронизированы."
msgstr "Зашифрованные элементы не могут быть изменены"
msgid "Conflicts"
msgstr "Конфликты"
@@ -1027,9 +1081,24 @@ msgstr "Показывать незавершённые задачи вверх
msgid "Save geo-location with notes"
msgstr "Сохранять информацию о геолокации в заметках"
msgid "Set application zoom percentage"
msgid "When creating a new to-do:"
msgstr "При создании новой задачи:"
msgid "Focus title"
msgstr "Фокус на названии"
msgid "Focus body"
msgstr "Фокус на содержимом"
msgid "When creating a new note:"
msgstr "При создании новой заметки:"
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr "Масштаб приложения в процентах"
msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение"
@@ -1058,6 +1127,8 @@ msgid ""
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"Цель синхронизации. Каждая цель синхронизации может иметь дополнительные "
"параметры, именованные как «sync.NUM.NAME» (все описаны ниже)."
msgid "Directory to synchronise with (absolute path)"
msgstr "Каталог синхронизации (абсолютный путь)"
@@ -1070,13 +1141,24 @@ msgstr ""
"`sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgstr "Nexcloud WebDAV URL"
msgid "Nexcloud username"
msgstr ""
msgstr "Имя пользователя Nexcloud"
msgid "Nexcloud password"
msgstr "Пароль Nexcloud"
#, fuzzy
msgid "Nexcloud password"
msgid "WebDAV URL"
msgstr "Nexcloud WebDAV URL"
#, fuzzy
msgid "WebDAV username"
msgstr "Имя пользователя Nexcloud"
#, fuzzy
msgid "WebDAV password"
msgstr "Установить пароль"
#, javascript-format
@@ -1086,15 +1168,18 @@ msgstr "Неверное значение параметра: «%s». Досту
msgid "Items that cannot be synchronised"
msgstr "Элементы, которые не могут быть синхронизированы"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s %s (%s)"
msgstr "%s (%s): %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
"target. In order to find these items, either search for the title or the ID "
"(which is displayed in brackets above)."
msgstr ""
"Эти элементы будут оставаться на устройстве, но не будут загружены в целевой "
"объект синхронизации. Чтобы найти эти элементы, воспользуйтесь поиском по "
"названию или ID (который указывается в скобках выше)."
msgid "Sync status (synced items / total items)"
msgstr "Статус синхронизации (элементов синхронизировано/всего)"
@@ -1141,9 +1226,8 @@ msgstr "Лог"
msgid "Export Debug Report"
msgstr "Экспортировать отладочный отчёт"
#, fuzzy
msgid "Encryption Config"
msgstr "Шифрование:"
msgstr "Конфигурация шифрования"
msgid "Configuration"
msgstr "Конфигурация"
@@ -1156,7 +1240,7 @@ msgid "Move %d notes to notebook \"%s\"?"
msgstr "Переместить %d заметок в блокнот «%s»?"
msgid "Press to set the decryption password."
msgstr ""
msgstr "Нажмите, чтобы установить пароль для расшифровки."
msgid "Select date"
msgstr "Выбрать дату"
@@ -1167,22 +1251,20 @@ msgstr "Подтвердить"
msgid "Cancel synchronisation"
msgstr "Отменить синхронизацию"
#, fuzzy, javascript-format
#, javascript-format
msgid "Master Key %s"
msgstr "Мастер-ключи"
msgstr "Мастер-ключ %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Created: %s"
msgstr "Создано: %d."
msgstr "Создано: %s"
#, fuzzy
msgid "Password:"
msgstr "Пароль"
msgstr "Пароль:"
msgid "Password cannot be empty"
msgstr ""
msgstr "Пароль не может быть пустым"
#, fuzzy
msgid "Enable"
msgstr "Включено"
@@ -1193,6 +1275,13 @@ msgstr "Не удалось сохранить блокнот: %s"
msgid "Edit notebook"
msgstr "Редактировать блокнот"
msgid "Show all"
msgstr ""
#, fuzzy
msgid "Errors only"
msgstr "Ошибка"
msgid "This note has been modified:"
msgstr "Эта заметка была изменена:"
@@ -1253,9 +1342,6 @@ msgstr "Добро пожаловать"
#~ "То, с чем будет осуществляться синхронизация. При синхронизации с "
#~ "файловой системой в `sync.2.path` указывается целевой каталог."
#~ msgid "Note title:"
#~ msgstr "Название заметки:"
#~ msgid "To-do title:"
#~ msgstr "Название задачи:"

View File

@@ -613,6 +613,9 @@ msgstr "帮助"
msgid "Website and documentation"
msgstr "网站与文档"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "关于Joplin"
@@ -620,12 +623,46 @@ msgstr "关于Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "%s:%s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "确认"
msgid "Cancel"
msgstr "取消"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "否"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -684,6 +721,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "状态"
@@ -831,6 +877,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive开发员(仅测试用)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "未知日志level:%s"
@@ -969,6 +1018,24 @@ msgstr "在列表上方显示未完成的待办事项"
msgid "Save geo-location with notes"
msgstr "保存笔记时同时保存地理定位信息"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "创建新待办事项。"
#, fuzzy
msgid "Focus title"
msgstr "笔记标题:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "创建新笔记。"
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
@@ -1018,6 +1085,15 @@ msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "无效的选项值:\"%s\"。可用值为:%s。"
@@ -1130,6 +1206,12 @@ msgstr "此笔记本无法保存:%s"
msgid "Edit notebook"
msgstr "编辑笔记本"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "此笔记已被修改:"
@@ -1186,9 +1268,6 @@ msgstr "欢迎"
#~ "`sync.2.path` to specify the target directory."
#~ msgstr "同步的目标。若与文件系统同步,设置`sync.2.path`为指定目标目录。"
#~ msgid "Note title:"
#~ msgstr "笔记标题:"
#~ msgid "To-do title:"
#~ msgstr "待办事项标题:"

View File

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

View File

@@ -19,7 +19,7 @@
],
"owner": "Laurent Cozic"
},
"version": "0.10.91",
"version": "0.10.93",
"bin": {
"joplin": "./main.js"
},

View File

@@ -8,7 +8,7 @@ process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('Encryption', function() {
describe('ArrayUtils', function() {
beforeEach(async (done) => {
done();
@@ -29,4 +29,19 @@ describe('Encryption', function() {
done();
});
it('should find items using binary search', async (done) => {
let items = ['aaa', 'ccc', 'bbb'];
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(-1); // Array not sorted!
items.sort();
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(1);
expect(ArrayUtils.binarySearch(items, 'ccc')).toBe(2);
expect(ArrayUtils.binarySearch(items, 'oops')).toBe(-1);
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(0);
items = [];
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(-1);
done();
});
});

View File

@@ -19,7 +19,7 @@ process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built
jasmine.DEFAULT_TIMEOUT_INTERVAL = 35000; // The first test is slow because the database needs to be built
async function allItems() {
let folders = await Folder.all();
@@ -459,7 +459,7 @@ describe('Synchronizer', function() {
let unconflictedNotes = await Note.unconflictedNotes();
expect(unconflictedNotes.length).toBe(0);
}));
}));
it('should handle conflict when remote folder is deleted then local folder is renamed', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@@ -489,7 +489,7 @@ describe('Synchronizer', function() {
let items = await allItems();
expect(items.length).toBe(1);
}));
}));
it('should allow duplicate folder titles', asyncTest(async () => {
let localF1 = await Folder.save({ title: "folder" });
@@ -575,10 +575,12 @@ describe('Synchronizer', function() {
}
it('should sync tags', asyncTest(async () => {
await shoudSyncTagTest(false); }));
await shoudSyncTagTest(false);
}));
it('should sync encrypted tags', asyncTest(async () => {
await shoudSyncTagTest(true); }));
await shoudSyncTagTest(true);
}));
it('should not sync notes with conflicts', asyncTest(async () => {
let f1 = await Folder.save({ title: "folder" });

View File

@@ -51,12 +51,12 @@ SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
//const syncTargetId_ = SyncTargetRegistry.nameToId('nextcloud');
const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
const syncTargetId_ = SyncTargetRegistry.nameToId('nextcloud');
//const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
//const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
const syncDir = __dirname + '/../tests/sync';
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 10;//400;
console.info('Testing with sync target: ' + SyncTargetRegistry.idToName(syncTargetId_));

View File

@@ -1,9 +1,11 @@
const { _ } = require('lib/locale.js');
const { BrowserWindow } = require('electron');
const { BrowserWindow, Menu, Tray } = require('electron');
const { shim } = require('lib/shim');
const url = require('url')
const path = require('path')
const urlUtils = require('lib/urlUtils.js');
const { dirname, basename } = require('lib/path-utils');
const fs = require('fs-extra');
class ElectronAppWrapper {
@@ -12,6 +14,8 @@ class ElectronAppWrapper {
this.env_ = env;
this.win_ = null;
this.willQuitApp_ = false;
this.tray_ = null;
this.buildDir_ = null;
}
electronApp() {
@@ -62,11 +66,27 @@ class ElectronAppWrapper {
if (this.env_ === 'dev') this.win_.webContents.openDevTools();
this.win_.on('close', (event) => {
if (this.willQuitApp_ || process.platform !== 'darwin') {
this.win_ = null;
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
// otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the
// user clicks on the icon in the task bar).
// On Windows and Linux, the app is closed when the window is closed *except* if the tray icon is used. In which
// case the app must be explicitely closed with Ctrl+Q or by right-clicking on the tray icon and selecting "Exit".
if (process.platform === 'darwin') {
if (this.willQuitApp_) {
this.win_ = null;
} else {
event.preventDefault();
this.win_.hide();
}
} else {
event.preventDefault();
this.win_.hide();
if (this.trayShown() && !this.willQuitApp_) {
event.preventDefault();
this.win_.hide();
} else {
this.win_ = null;
}
}
})
@@ -93,6 +113,43 @@ class ElectronAppWrapper {
this.electronApp_.quit();
}
trayShown() {
return !!this.tray_;
}
buildDir() {
if (this.buildDir_) return this.buildDir_;
let dir = __dirname + '/build';
if (!fs.pathExistsSync(dir)) {
dir = dirname(__dirname) + '/build';
if (!fs.pathExistsSync(dir)) throw new Error('Cannot find build dir');
}
this.buildDir_ = dir;
return dir;
}
// Note: this must be called only after the "ready" event of the app has been dispatched
createTray(contextMenu) {
try {
this.tray_ = new Tray(this.buildDir() + '/icons/16x16.png')
this.tray_.setToolTip(this.electronApp_.getName())
this.tray_.setContextMenu(contextMenu)
this.tray_.on('click', () => {
this.window().show();
});
} catch (error) {
console.error("Cannot create tray", error);
}
}
destroyTray() {
if (!this.tray_) return;
this.tray_.destroy();
this.tray_ = null;
}
async start() {
// Since we are doing other async things before creating the window, we might miss
// the "ready" event. So we use the function below to make sure that the app is ready.

View File

@@ -49,6 +49,10 @@ class Application extends BaseApplication {
return true;
}
checkForUpdateLoggerPath() {
return Setting.value('profileDir') + '/log-autoupdater.txt';
}
reducer(state = appDefaultState, action) {
let newState = state;
@@ -136,6 +140,10 @@ class Application extends BaseApplication {
this.refreshMenu();
}
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') {
this.updateTray();
}
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync();
}
@@ -294,6 +302,11 @@ class Application extends BaseApplication {
label: _('Website and documentation'),
accelerator: 'F1',
click () { bridge().openExternal('http://joplin.cozic.net') }
}, {
label: _('Check for updates...'),
click: () => {
bridge().checkForUpdates(false, this.checkForUpdateLoggerPath());
}
}, {
label: _('About Joplin'),
click: () => {
@@ -340,6 +353,28 @@ class Application extends BaseApplication {
this.lastMenuScreen_ = screen;
}
updateTray() {
// Tray icon (called AppIndicator) doesn't work in Ubuntu
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
// Might be fixed in Electron 18.x but no non-beta release yet.
if (!shim.isWindows() && !shim.isMac()) return;
const app = bridge().electronApp();
if (app.trayShown() === Setting.value('showTrayIcon')) return;
if (!Setting.value('showTrayIcon')) {
app.destroyTray();
} else {
const contextMenu = Menu.buildFromTemplate([
{ label: _('Open %s', app.electronApp().getName()), click: () => { app.window().show(); } },
{ type: 'separator' },
{ label: _('Exit'), click: () => { app.exit() } },
])
app.createTray(contextMenu);
}
}
async start(argv) {
argv = await super.start(argv);
@@ -385,9 +420,9 @@ class Application extends BaseApplication {
// Note: Auto-update currently doesn't work in Linux: it downloads the update
// but then doesn't install it on exit.
if (shim.isWindows() || shim.isMac()) {
const runAutoUpdateCheck = function() {
const runAutoUpdateCheck = () => {
if (Setting.value('autoUpdateEnabled')) {
bridge().checkForUpdatesAndNotify(Setting.value('profileDir') + '/log-autoupdater.txt');
bridge().checkForUpdates(true, this.checkForUpdateLoggerPath());
}
}
@@ -396,6 +431,8 @@ class Application extends BaseApplication {
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
}
this.updateTray();
setTimeout(() => {
AlarmService.garbageCollect();
}, 1000 * 60 * 60);

View File

@@ -125,6 +125,11 @@ class Bridge {
}
}
checkForUpdates(inBackground, logFilePath) {
const { checkForUpdates } = require('./checkForUpdates.js');
checkForUpdates(inBackground, logFilePath);
}
}
let bridge_ = null;

View File

@@ -0,0 +1,92 @@
const { dialog } = require('electron')
const { autoUpdater } = require('electron-updater')
const { Logger } = require('lib/logger.js');
const { _ } = require('lib/locale.js');
let autoUpdateLogger_ = new Logger();
let checkInBackground_ = false;
// Note: Electron Builder's autoUpdater is incredibly buggy so currently it's only used
// to detect if a new version is present. If it is, the download link is simply opened
// in a new browser window.
autoUpdater.autoDownload = false;
autoUpdater.on('error', (error) => {
autoUpdateLogger_.error(error);
if (checkInBackground_) return;
dialog.showErrorBox(_('Error'), error == null ? "unknown" : (error.stack || error).toString())
})
function htmlToText_(html) {
let output = html.replace(/\n/g, '');
output = output.replace(/<li>/g, '- ');
output = output.replace(/<\/li>/g, '\n');
output = output.replace(/<ul>/g, '');
output = output.replace(/<\/ul>/g, '');
output = output.replace(/<.*?>/g, '');
output = output.replace(/<\/.*?>/g, '');
return output;
}
autoUpdater.on('update-available', (info) => {
if (!info.version || !info.path) {
if (checkInBackground_) return;
dialog.showErrorBox(_('Error'), ('Could not get version info: ' + JSON.stringify(info)));
return;
}
const downloadUrl = 'https://github.com/laurent22/joplin/releases/download/v' + info.version + '/' + info.path;
let releaseNotes = info.releaseNotes + '';
if (releaseNotes) releaseNotes = '\n\n' + _('Release notes:\n\n%s', htmlToText_(releaseNotes));
dialog.showMessageBox({
type: 'info',
message: _('An update is available, do you want to download it now?' + releaseNotes),
buttons: [_('Yes'), _('No')]
}, (buttonIndex) => {
if (buttonIndex === 0) {
require('electron').shell.openExternal(downloadUrl);
}
})
})
autoUpdater.on('update-not-available', () => {
if (checkInBackground_) return;
dialog.showMessageBox({ message: _('Current version is up-to-date.') })
})
// autoUpdater.on('update-downloaded', () => {
// dialog.showMessageBox({ message: _('New version downloaded - application will quit now and update...') }, () => {
// setTimeout(() => {
// try {
// autoUpdater.quitAndInstall();
// } catch (error) {
// autoUpdateLogger_.error(error);
// dialog.showErrorBox(_('Error'), _('Could not install the update: %s', error.message));
// }
// }, 100);
// })
// })
function checkForUpdates(inBackground, logFilePath) {
if (logFilePath && !autoUpdateLogger_.targets().length) {
autoUpdateLogger_ = new Logger();
autoUpdateLogger_.addTarget('file', { path: logFilePath });
autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
autoUpdateLogger_.info('checkForUpdates: Initializing...');
autoUpdater.logger = autoUpdateLogger_;
}
checkInBackground_ = inBackground;
try {
autoUpdater.checkForUpdates()
} catch (error) {
autoUpdateLogger_.error(error);
if (!checkInBackground_) dialog.showErrorBox(_('Error'), error.message);
}
}
module.exports.checkForUpdates = checkForUpdates

View File

@@ -0,0 +1,3 @@
owner: laurent22
repo: joplin
provider: github

View File

@@ -92,10 +92,14 @@ class EncryptionConfigScreenComponent extends React.Component {
};
const mkComps = [];
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
@@ -149,6 +153,31 @@ class EncryptionConfigScreenComponent extends React.Component {
);
}
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(<tr key={id}><td style={theme.textStyle}>{id}</td></tr>);
}
nonExistingMasterKeySection = (
<div>
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
</tr>
{ rows }
</tbody>
</table>
</div>
);
}
return (
<div>
<Header style={headerStyle} />
@@ -169,6 +198,7 @@ class EncryptionConfigScreenComponent extends React.Component {
{decryptedItemsInfo}
{toggleButton}
{masterKeySection}
{nonExistingMasterKeySection}
</div>
</div>
);
@@ -183,6 +213,7 @@ const mapStateToProps = (state) => {
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
};

View File

@@ -274,20 +274,22 @@ class MainScreenComponent extends React.Component {
const messageBoxVisible = this.props.hasDisabledSyncItems || this.props.showMissingMasterKeyMessage;
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
const theme = themeStyle(this.props.theme);
const selectedFolderId = this.props.selectedFolderId;
const onConflictFolder = this.props.selectedFolderId === Folder.conflictFolderId();
const headerButtons = [];
headerButtons.push({
title: _('New note'),
iconName: 'fa-file-o',
enabled: !!folders.length,
enabled: !!folders.length && !onConflictFolder,
onClick: () => { this.doCommand({ name: 'newNote' }) },
});
headerButtons.push({
title: _('New to-do'),
iconName: 'fa-check-square-o',
enabled: !!folders.length,
enabled: !!folders.length && !onConflictFolder,
onClick: () => { this.doCommand({ name: 'newTodo' }) },
});
@@ -384,6 +386,7 @@ const mapStateToProps = (state) => {
notes: state.notes,
hasDisabledSyncItems: state.hasDisabledSyncItems,
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
selectedFolderId: state.selectedFolderId,
};
};

View File

@@ -190,13 +190,21 @@ class NoteTextComponent extends React.Component {
this.editorSetScrollTop(1);
this.restoreScrollTop_ = 0;
if (note) {
const focusSettingName = !!note.is_todo ? 'newTodoFocus' : 'newNoteFocus';
if (Setting.value(focusSettingName) === 'title') {
if (this.titleField_) this.titleField_.focus();
} else {
if (this.editor_) this.editor_.editor.focus();
}
}
if (this.editor_) {
const session = this.editor_.editor.getSession();
const undoManager = session.getUndoManager();
undoManager.reset();
session.setUndoManager(undoManager);
this.editor_.editor.focus();
this.editor_.editor.clearSelection();
this.editor_.editor.moveCursorTo(0,0);
}
@@ -576,6 +584,7 @@ class NoteTextComponent extends React.Component {
const titleEditor = <input
type="text"
ref={(elem) => { this.titleField_ = elem; } }
style={titleEditorStyle}
value={note && note.title ? note.title : ''}
onChange={(event) => { this.title_changeText(event); }}

View File

@@ -1,179 +1,205 @@
<style>
body {
overflow: hidden;
}
#content {
overflow-y: auto;
height: 100%;
padding-left: 10px;
padding-right: 10px;
}
</style>
<div id="hlScriptContainer"></div>
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
<script>
const { ipcRenderer } = require('electron');
const contentElement = document.getElementById('content');
// ----------------------------------------------------------------------
// Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
let hljsScriptAdded = false;
let hljsLoaded = false;
function loadHljs(callback) {
hljsScriptAdded = true;
const script = document.createElement('script');
script.onload = function () {
hljsLoaded = true;
applyHljs();
};
script.src = 'highlight/highlight.pack.js';
document.getElementById('hlScriptContainer').appendChild(script);
const link = document.createElement('link');
link.rel = 'stylesheet';
// https://ace.c9.io/build/kitchen-sink.html
// https://highlightjs.org/static/demo/
link.href = 'highlight/styles/atom-one-light.css';
document.getElementById('hlScriptContainer').appendChild(link);
}
function loadAndApplyHljs() {
var codeElements = document.getElementsByClassName('code');
if (!codeElements.length) return;
if (!hljsScriptAdded) {
this.loadHljs();
return;
}
// If HLJS is not loaded yet, no need to do anything. When it loads
// it will automatically apply the style to all the code elements.
if (hljsLoaded) applyHljs(codeElements);
}
function applyHljs(codeElements) {
if (typeof codeElements === 'undefined') codeElements = document.getElementsByClassName('code');
for (var i = 0; i < codeElements.length; i++) {
hljs.highlightBlock(codeElements[i]);
}
}
// ----------------------------------------------------------------------
// / Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore
// it at any time knowing that it's not going to be changed because the content height has changed.
// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during
// one second after the content has been updated.
//
// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result
// of programmatically changing scrollTop. We only want to respond to events initiated by the user.
let percentScroll_ = 0;
let checkScrollIID_ = null;
function setPercentScroll(percent) {
percentScroll_ = percent;
contentElement.scrollTop = percentScroll_ * maxScrollTop();
}
function percentScroll() {
return percentScroll_;
}
function restorePercentScroll() {
setPercentScroll(percentScroll_);
}
ipcRenderer.on('setHtml', (event, html) => {
contentElement.innerHTML = html;
loadAndApplyHljs();
// Remove the bullet from "ul" for checkbox lists and extra padding
const checkboxes = document.getElementsByClassName('checkbox');
for (let i = 0; i < checkboxes.length; i++) {
const cb = checkboxes[i];
const ul = cb.parentElement.parentElement;
if (!ul) {
console.warn('Unexpected layout for checkbox');
continue;
<!DOCTYPE html>
<html>
<head>
<style>
body {
overflow: hidden;
}
ul.style.listStyleType = 'none';
ul.style.paddingLeft = 0;
#content {
overflow-y: auto;
height: 100%;
padding-left: 10px;
padding-right: 10px;
}
.katex { font-size: 1.3em; } /* This controls the global Katex font size*/
</style>
</head>
<body id="body">
<div id="hlScriptContainer"></div>
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
<script>
const { ipcRenderer } = require('electron');
const contentElement = document.getElementById('content');
// ----------------------------------------------------------------------
// Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
let hljsScriptAdded = false;
let hljsLoaded = false;
function loadHljs(callback) {
hljsScriptAdded = true;
const script = document.createElement('script');
script.onload = function () {
hljsLoaded = true;
applyHljs();
};
script.src = 'highlight/highlight.pack.js';
document.getElementById('hlScriptContainer').appendChild(script);
const link = document.createElement('link');
link.rel = 'stylesheet';
// https://ace.c9.io/build/kitchen-sink.html
// https://highlightjs.org/static/demo/
link.href = 'highlight/styles/atom-one-light.css';
document.getElementById('hlScriptContainer').appendChild(link);
}
let previousContentHeight = contentElement.scrollHeight;
let startTime = Date.now();
ignoreNextScrollEvent = true;
restorePercentScroll();
function loadAndApplyHljs() {
var codeElements = document.getElementsByClassName('code');
if (!codeElements.length) return;
if (!checkScrollIID_) {
checkScrollIID_ = setInterval(() => {
const h = contentElement.scrollHeight;
if (h !== previousContentHeight) {
previousContentHeight = h;
ignoreNextScrollEvent = true;
restorePercentScroll();
if (!hljsScriptAdded) {
this.loadHljs();
return;
}
// If HLJS is not loaded yet, no need to do anything. When it loads
// it will automatically apply the style to all the code elements.
if (hljsLoaded) applyHljs(codeElements);
}
function applyHljs(codeElements) {
if (typeof codeElements === 'undefined') codeElements = document.getElementsByClassName('code');
for (var i = 0; i < codeElements.length; i++) {
try {
hljs.highlightBlock(codeElements[i]);
} catch (error) {
console.warn('Cannot highlight code', error);
}
if (Date.now() - startTime >= 1000) {
clearInterval(checkScrollIID_);
checkScrollIID_ = null;
}
}
// ----------------------------------------------------------------------
// / Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore
// it at any time knowing that it's not going to be changed because the content height has changed.
// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during
// one second after the content has been updated.
//
// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result
// of programmatically changing scrollTop. We only want to respond to events initiated by the user.
let percentScroll_ = 0;
let checkScrollIID_ = null;
function setPercentScroll(percent) {
percentScroll_ = percent;
contentElement.scrollTop = percentScroll_ * maxScrollTop();
}
function percentScroll() {
return percentScroll_;
}
function restorePercentScroll() {
setPercentScroll(percentScroll_);
}
ipcRenderer.on('setHtml', (event, html) => {
updateBodyHeight();
contentElement.innerHTML = html;
loadAndApplyHljs();
// Remove the bullet from "ul" for checkbox lists and extra padding
const checkboxes = document.getElementsByClassName('checkbox');
for (let i = 0; i < checkboxes.length; i++) {
const cb = checkboxes[i];
const ul = cb.parentElement.parentElement;
if (!ul) {
console.warn('Unexpected layout for checkbox');
continue;
}
}, 1);
}
});
ul.style.listStyleType = 'none';
ul.style.paddingLeft = 0;
}
let ignoreNextScrollEvent = false;
ipcRenderer.on('setPercentScroll', (event, percent) => {
if (checkScrollIID_) {
clearInterval(checkScrollIID_);
checkScrollIID_ = null;
let previousContentHeight = contentElement.scrollHeight;
let startTime = Date.now();
ignoreNextScrollEvent = true;
restorePercentScroll();
if (!checkScrollIID_) {
checkScrollIID_ = setInterval(() => {
const h = contentElement.scrollHeight;
if (h !== previousContentHeight) {
previousContentHeight = h;
ignoreNextScrollEvent = true;
restorePercentScroll();
}
if (Date.now() - startTime >= 1000) {
clearInterval(checkScrollIID_);
checkScrollIID_ = null;
}
}, 1);
}
});
let ignoreNextScrollEvent = false;
ipcRenderer.on('setPercentScroll', (event, percent) => {
if (checkScrollIID_) {
clearInterval(checkScrollIID_);
checkScrollIID_ = null;
}
ignoreNextScrollEvent = true;
setPercentScroll(percent);
});
function maxScrollTop() {
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
}
ignoreNextScrollEvent = true;
setPercentScroll(percent);
});
function maxScrollTop() {
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
}
contentElement.addEventListener('scroll', function(e) {
if (ignoreNextScrollEvent) {
ignoreNextScrollEvent = false;
return;
// The body element needs to have a fixed height for the content to be scrollable
function updateBodyHeight() {
document.getElementById('body').style.height = window.innerHeight + 'px';
}
const m = maxScrollTop();
const percent = m ? contentElement.scrollTop / m : 0;
setPercentScroll(percent);
ipcRenderer.sendToHost('percentScroll', percent);
});
// Disable drag and drop otherwise it's possible to drop a URL
// on it and it will open in the view as a website.
document.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('dragover', function(e) {
e.preventDefault();
});
</script>
contentElement.addEventListener('scroll', function(e) {
if (ignoreNextScrollEvent) {
ignoreNextScrollEvent = false;
return;
}
const m = maxScrollTop();
const percent = m ? contentElement.scrollTop / m : 0;
setPercentScroll(percent);
ipcRenderer.sendToHost('percentScroll', percent);
});
// Disable drag and drop otherwise it's possible to drop a URL
// on it and it will open in the view as a website.
document.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('dragover', function(e) {
e.preventDefault();
});
window.addEventListener('resize', function() {
updateBodyHeight();
});
updateBodyHeight();
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.51",
"version": "0.10.60",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -10,17 +10,9 @@
"integrity": "sha512-+rr4OgeTNrLuJAf09o3USdttEYiXvZshWMkhD6wR9v1ieXH0JM1Q2yT41/cJuJcqiPpSXlM/g3aR+Y5MWQdr0Q==",
"dev": true,
"requires": {
"7zip-bin-linux": "1.3.1",
"7zip-bin-win": "2.1.1"
},
"dependencies": {
"7zip-bin-linux": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/7zip-bin-linux/-/7zip-bin-linux-1.3.1.tgz",
"integrity": "sha512-Wv1uEEeHbTiS1+ycpwUxYNuIcyohU6Y6vEqY3NquBkeqy0YhVdsNUGsj0XKSRciHR6LoJSEUuqYUexmws3zH7Q==",
"dev": true,
"optional": true
},
"7zip-bin-win": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/7zip-bin-win/-/7zip-bin-win-2.1.1.tgz",
@@ -571,8 +563,7 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base-64": {
"version": "0.1.0",
@@ -617,6 +608,14 @@
"readable-stream": "2.3.3"
}
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
"integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
"requires": {
"inherits": "2.0.3"
}
},
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
@@ -734,7 +733,6 @@
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
"dev": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
@@ -879,8 +877,7 @@
"capture-stack-trace": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz",
"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=",
"dev": true
"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0="
},
"caseless": {
"version": "0.12.0",
@@ -937,7 +934,8 @@
"chownr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz",
"integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE="
"integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=",
"dev": true
},
"chromium-pickle-js": {
"version": "0.2.0",
@@ -981,9 +979,9 @@
"dev": true
},
"color": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color/-/color-2.0.1.tgz",
"integrity": "sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/color/-/color-1.0.3.tgz",
"integrity": "sha1-5I6DLYXxTvaU+0aIEcLVz+cptV0=",
"requires": {
"color-convert": "1.9.1",
"color-string": "1.5.2"
@@ -1034,8 +1032,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
"version": "1.6.0",
@@ -1097,7 +1094,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
"integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=",
"dev": true,
"requires": {
"capture-stack-trace": "1.0.0"
}
@@ -1196,14 +1192,6 @@
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"decompress-response": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
"requires": {
"mimic-response": "1.0.0"
}
},
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
@@ -1240,11 +1228,6 @@
"repeating": "2.0.1"
}
},
"detect-libc": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-0.2.0.tgz",
"integrity": "sha1-R/31ZzSKF+wl/L8LnkRjSKdvn7U="
},
"dmg-builder": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-2.1.6.tgz",
@@ -1295,8 +1278,7 @@
"duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
"dev": true
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
},
"ecc-jsbn": {
"version": "0.1.1",
@@ -2024,8 +2006,18 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fstream": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
"integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
"requires": {
"graceful-fs": "4.1.11",
"inherits": "2.0.3",
"mkdirp": "0.5.1",
"rimraf": "2.6.2"
}
},
"fullstore": {
"version": "1.1.0",
@@ -2071,8 +2063,7 @@
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"getpass": {
"version": "0.1.7",
@@ -2093,7 +2084,6 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
@@ -2142,7 +2132,6 @@
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
"dev": true,
"requires": {
"create-error-class": "3.0.2",
"duplexer3": "0.1.4",
@@ -2160,8 +2149,7 @@
"unzip-response": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",
"integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=",
"dev": true
"integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c="
}
}
},
@@ -2321,7 +2309,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
@@ -2520,14 +2507,12 @@
"is-redirect": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=",
"dev": true
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ="
},
"is-retry-allowed": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=",
"dev": true
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ="
},
"is-stream": {
"version": "1.1.0",
@@ -2680,6 +2665,14 @@
"resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz",
"integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po="
},
"katex": {
"version": "0.9.0-beta1",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.9.0-beta1.tgz",
"integrity": "sha512-M7c7Eihp665Bh9wDR0xg/PdE1OuCa15PsiDQSBYyr+xJR8WrFP8nxdNF1lNUCBPzEup4zECG2jFUIZnU66xBRQ==",
"requires": {
"match-at": "0.1.1"
}
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@@ -2812,8 +2805,7 @@
"lowercase-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz",
"integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=",
"dev": true
"integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY="
},
"lru-cache": {
"version": "4.1.1",
@@ -2860,6 +2852,11 @@
"uc.micro": "1.0.3"
}
},
"match-at": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz",
"integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q=="
},
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
@@ -2963,16 +2960,10 @@
"integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=",
"dev": true
},
"mimic-response": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz",
"integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "1.1.8"
}
@@ -2982,22 +2973,6 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minipass": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.1.tgz",
"integrity": "sha512-u1aUllxPJUI07cOqzR7reGmQxmCqlH88uIIsf6XZFEWgw7gXKpJdR+5R9Y3KEDmWYkdIz9wXZs3C0jOPxejk/Q==",
"requires": {
"yallist": "3.0.2"
}
},
"minizlib": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.0.4.tgz",
"integrity": "sha512-sN4U9tIJtBRwKbwgFh9qJfrPIQ/GGTRr1MGqkgOeMTLy8/lM0FcWU//FqlnZ3Vb7gJ+Mxh3FOg1EklibdajbaQ==",
"requires": {
"minipass": "2.2.1"
}
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
@@ -3300,8 +3275,7 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-is-inside": {
"version": "1.0.2",
@@ -3427,8 +3401,7 @@
"prepend-http": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
},
"preserve": {
"version": "0.2.0",
@@ -3889,7 +3862,6 @@
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"dev": true,
"requires": {
"glob": "7.1.2"
}
@@ -3946,17 +3918,16 @@
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"sharp": {
"version": "0.18.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.18.4.tgz",
"integrity": "sha1-/jKcDwaJbCiqJDdt8f/wKuV/LTQ=",
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.17.3.tgz",
"integrity": "sha1-SEzSpwyQA3CUjcxD4WX3gwa/9Io=",
"requires": {
"caw": "2.0.1",
"color": "2.0.1",
"detect-libc": "0.2.0",
"color": "1.0.3",
"got": "6.7.1",
"nan": "2.7.0",
"semver": "5.4.1",
"simple-get": "2.7.0",
"tar": "3.2.1"
"tar": "2.2.1"
}
},
"shebang-command": {
@@ -3985,21 +3956,6 @@
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true
},
"simple-concat": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
},
"simple-get": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.7.0.tgz",
"integrity": "sha512-RkE9rGPHcxYZ/baYmgJtOSM63vH0Vyq+ma5TijBcLla41SWlh8t6XYIGMR/oeZcmr+/G8k+zrClkkVrtnQ0esg==",
"requires": {
"decompress-response": "3.3.0",
"once": "1.4.0",
"simple-concat": "1.0.0"
}
},
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -4944,15 +4900,13 @@
"integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0="
},
"tar": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-3.2.1.tgz",
"integrity": "sha512-ZSzds1E0IqutvMU8HxjMaU8eB7urw2fGwTq88ukDOVuUIh0656l7/P7LiVPxhO5kS4flcRJQk8USG+cghQbTUQ==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
"integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
"requires": {
"chownr": "1.0.1",
"minipass": "2.2.1",
"minizlib": "1.0.4",
"mkdirp": "0.5.1",
"yallist": "3.0.2"
"block-stream": "0.0.9",
"fstream": "1.0.11",
"inherits": "2.0.3"
}
},
"tar-fs": {
@@ -5070,8 +5024,7 @@
"timed-out": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=",
"dev": true
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8="
},
"to-fast-properties": {
"version": "1.0.3",
@@ -5230,7 +5183,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
"integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=",
"dev": true,
"requires": {
"prepend-http": "1.0.4"
}
@@ -5409,11 +5361,6 @@
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"dev": true
},
"yallist": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k="
},
"yargs": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-10.0.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.51",
"version": "0.10.60",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {
@@ -8,8 +8,8 @@
"pack": "node_modules/.bin/electron-builder --dir",
"dist": "node_modules/.bin/electron-builder",
"publish": "build -p always",
"postinstall": "node compile-jsx.js && node compile-package-info.js",
"compile": "node compile-jsx.js && node compile-package-info.js"
"postinstall": "node compile-jsx.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts",
"compile": "node compile-jsx.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts"
},
"repository": {
"type": "git",
@@ -22,6 +22,9 @@
},
"build": {
"appId": "net.cozic.joplin-desktop",
"extraResources": [
"build/icons/*"
],
"win": {
"icon": "../../Assets/Joplin.ico"
},
@@ -34,7 +37,8 @@
"asar": false
},
"linux": {
"asar": false
"asar": false,
"category": "Office"
}
},
"homepage": "https://github.com/laurent22/joplin#readme",
@@ -62,6 +66,7 @@
"highlight.js": "^9.12.0",
"html-entities": "^1.2.1",
"jssha": "^2.3.1",
"katex": "^0.9.0-beta1",
"levenshtein": "^1.0.5",
"lodash": "^4.17.4",
"markdown-it": "^8.4.0",
@@ -78,7 +83,7 @@
"react-dom": "^16.0.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"sharp": "^0.18.4",
"sharp": "^0.17.3",
"smalltalk": "^2.5.1",
"sprintf-js": "^1.1.1",
"sqlite3": "^3.1.13",

View File

@@ -18,15 +18,15 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
Operating System | Download
-----------------|--------
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-Setup-0.10.43.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-0.10.43.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-0.10.43-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.59/Joplin-Setup-0.10.59.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.59/Joplin-0.10.59.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.59/Joplin-0.10.59-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
## 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://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin/releases/download/android-v0.10.79/joplin-v0.10.79.apk)
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin/releases/download/android-v0.10.86/joplin-v0.10.86.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a> | -
## Terminal application
@@ -104,6 +104,12 @@ On the **terminal application**, you will need to set the `sync.target` config v
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.
## WebDAV synchronisation
**Important: This is a beta feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain. If you wish to you use it, it is recommended that you keep a backup of your data. The simplest way is to regularly backup the profile directory of the desktop or terminal application.**
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
## OneDrive synchronisation
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.
@@ -138,20 +144,75 @@ On mobile, the alarms will be displayed using the built-in notification system.
If for any reason the notifications do not work, please [open an issue](https://github.com/laurent22/joplin/issues).
# Markdown
Joplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions. In particular:
## Math notation
Math expressions can be added using the [Katex notation](https://khan.github.io/KaTeX/). To add an inline equation, wrap the expression in `` `{.katex}EXPRESSION` ``, eg. `` `{.katex}\sqrt{3x-1}+(1+x)^2` ``. To create an expression block, wrap it as follow:
```katex
EXPRESSION
```
For example:
```katex
f(x) = \int_{-\infty}^\infty
\hat f(\xi)\,e^{2 \pi i \xi x}
\,d\xi
```
Here is an example with the Markdown and rendered result side by side:
<img src="https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/Katex.png" style="max-width: 100%; max-height: 35em;">
## Checkboxes
Checkboxes can be added like so:
-[ ] Milk
-[ ] Rice
-[ ] Eggs
The checkboxes can then be ticked in the mobile and desktop applications.
# Contributing
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md
# Localisation
Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian, Dutch and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:
Joplin is currently available in the languages below. If you would like to contribute a **new translation**, it is quite straightforward, please follow these steps:
- [Download Poedit](https://poedit.net/), the translation editor, and install it.
- [Download the file to be translated](https://raw.githubusercontent.com/laurent22/joplin/master/CliClient/locales/joplin.pot).
- In Poedit, open this .pot file, go into the Catalog menu and click Configuration. Change "Country" and "Language" to your own country and language.
- From then you can translate the file. Once it is done, please either [open a pull request](https://github.com/laurent22/joplin/pulls) or send the file to [this address](https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png).
To **update a translation**, follow the same steps as above but instead of getting the .pot file, get the .po file for your language from there: https://github.com/laurent22/joplin/tree/master/CliClient/locales
This translation will apply to the three applications - desktop, mobile and terminal.
# Contributing
Current translations:
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Code | Last translator | Percent done
---|---|---|---|---
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png) | Croatian | hr_HR | Hrvoje Mandić <trbuhom@net.hr> | 72%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png) | Deutsch | de_DE | Tobias Strobel <git@strobeltobias.de> | 92%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/gb.png) | English | en_GB | | 100%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png) | Español | es_ES | Lucas Vieites | 80%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cr.png) | Español (Costa Rica) | es_CR | | 68%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/fr.png) | Français | fr_FR | Laurent Cozic | 100%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/it.png) | Italiano | it_IT | | 76%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/be.png) | Nederlands | nl_BE | | 90%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png) | Português (Brasil) | pt_BR | | 74%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png) | Русский | ru_RU | Artyom Karlov <artyom.karlov@gmail.com> | 96%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png) | 中文 (简体) | zh_CN | RCJacH <RCJacH@outlook.com> | 76%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/jp.png) | 日本語 | ja_JP | | 74%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Coming features

View File

@@ -126,6 +126,10 @@ You will need to set the `sync.target` config variable and all the `sync.5.path`
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.
## WebDAV synchronisation
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
## OneDrive synchronisation
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.

View File

@@ -90,8 +90,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion 16
targetSdkVersion 22
versionCode 2097257
versionName "0.10.79"
versionCode 2097264
versionName "0.10.86"
ndk {
abiFilters "armeabi-v7a", "x86"
}

View File

@@ -13,4 +13,28 @@ ArrayUtils.removeElement = function(array, element) {
return array;
}
// https://stackoverflow.com/a/10264318/561309
ArrayUtils.binarySearch = function(items, value) {
var startIndex = 0,
stopIndex = items.length - 1,
middle = Math.floor((stopIndex + startIndex)/2);
while(items[middle] != value && startIndex < stopIndex){
//adjust search area
if (value < items[middle]){
stopIndex = middle - 1;
} else if (value > items[middle]){
startIndex = middle + 1;
}
//recalculate middle
middle = Math.floor((stopIndex + startIndex)/2);
}
//make sure it's the right value
return (items[middle] != value) ? -1 : middle;
}
module.exports = ArrayUtils;

View File

@@ -27,6 +27,7 @@ const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
const EncryptionService = require('lib/services/EncryptionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
@@ -34,6 +35,7 @@ SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
SyncTargetRegistry.addClass(SyncTargetWebDAV);
class BaseApplication {

View File

@@ -5,6 +5,7 @@ const Resource = require('lib/models/Resource.js');
const ModelCache = require('lib/ModelCache');
const { shim } = require('lib/shim.js');
const md5 = require('md5');
const MdToHtml_Katex = require('lib/MdToHtml_Katex');
class MdToHtml {
@@ -28,7 +29,7 @@ class MdToHtml {
const r = resources[n];
k.push(r.id);
}
k.push(md5(body));
k.push(md5(escape(body))); // https://github.com/pvorb/node-md5/issues/41
k.push(md5(JSON.stringify(style)));
k.push(md5(JSON.stringify(options)));
return k.join('_');
@@ -156,27 +157,56 @@ class MdToHtml {
}
}
renderTokens_(tokens, options) {
customCodeHandler_(language) {
if (!language) return null;
const handlers = {};
handlers['katex'] = new MdToHtml_Katex();
return language in handlers ? handlers[language] : null;
}
parseInlineCodeLanguage_(content) {
const m = content.match(/^\{\.([a-zA-Z0-9]+)\}/);
if (m && m.length >= 2) {
const language = m[1];
return {
language: language,
newContent: content.substr(language.length + 3),
};
}
return null;
}
renderTokens_(markdownIt, tokens, options) {
let output = [];
let previousToken = null;
let anchorAttrs = [];
let extraCssBlocks = {};
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i];
let t = tokens[i];
const nextToken = i < tokens.length ? tokens[i+1] : null;
let tag = t.tag;
let openTag = null;
let closeTag = null;
let attrs = t.attrs ? t.attrs : [];
let tokenContent = t.content ? t.content : null;
const isCodeBlock = tag === 'code' && t.block;
const isInlineCode = t.type === 'code_inline';
const codeBlockLanguage = t && t.info ? t.info : null;
let codeBlockHandler = null;
// if (t.map) attrs.push(['data-map', t.map.join(':')]);
if (isCodeBlock) codeBlockHandler = this.customCodeHandler_(codeBlockLanguage);
if (previousToken && previousToken.tag === 'li' && tag === 'p') {
// Markdown-it render list items as <li><p>Text<p></li> which makes it
// complicated to style and layout the HTML, so we remove this extra
// <p> here and below in closeTag.
openTag = null;
} else if (isInlineCode) {
openTag = null;
} else if (tag && t.type.indexOf('_open') >= 0) {
openTag = tag;
} else if (tag && t.type.indexOf('_close') >= 0) {
@@ -186,7 +216,11 @@ class MdToHtml {
} else if (t.type === 'link_open') {
openTag = 'a';
} else if (isCodeBlock) {
openTag = 'pre';
if (codeBlockHandler) {
openTag = null;
} else {
openTag = 'pre';
}
}
if (openTag) {
@@ -201,12 +235,30 @@ class MdToHtml {
if (isCodeBlock) {
const codeAttrs = ['code'];
if (t.info) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock
output.push('<code class="' + codeAttrs.join(' ') + '">');
if (!codeBlockHandler) {
if (codeBlockLanguage) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock
output.push('<code class="' + codeAttrs.join(' ') + '">');
}
} else if (isInlineCode) {
const result = this.parseInlineCodeLanguage_(tokenContent);
if (result) {
codeBlockHandler = this.customCodeHandler_(result.language);
tokenContent = result.newContent;
}
if (!codeBlockHandler) {
output.push('<code>');
}
}
if (codeBlockHandler) {
codeBlockHandler.loadAssets().catch((error) => {
console.warn('MdToHtml: Error loading assets for ' + codeBlockHandler.name() + ': ', error.message);
});
}
if (t.type === 'image') {
if (t.content) attrs.push(['title', t.content]);
if (tokenContent) attrs.push(['title', tokenContent]);
output.push(this.renderImage_(attrs, options));
} else if (t.type === 'softbreak') {
output.push('<br/>');
@@ -214,11 +266,15 @@ class MdToHtml {
output.push('<hr/>');
} else {
if (t.children) {
const parsedChildren = this.renderTokens_(t.children, options);
const parsedChildren = this.renderTokens_(markdownIt, t.children, options);
output = output.concat(parsedChildren);
} else {
if (t.content) {
output.push(htmlentities(t.content));
if (tokenContent) {
if ((isCodeBlock || isInlineCode) && codeBlockHandler) {
output = codeBlockHandler.processContent(output, tokenContent, isCodeBlock ? 'block' : 'inline');
} else {
output.push(htmlentities(tokenContent));
}
}
}
}
@@ -230,10 +286,18 @@ class MdToHtml {
} else if (tag && t.type.indexOf('inline') >= 0) {
closeTag = openTag;
} else if (isCodeBlock) {
closeTag = openTag;
if (!codeBlockHandler) closeTag = openTag;
}
if (isCodeBlock) output.push('</code>');
if (isCodeBlock) {
if (!codeBlockHandler) {
output.push('</code>');
}
} else if (isInlineCode) {
if (!codeBlockHandler) {
output.push('</code>');
}
}
if (closeTag) {
if (closeTag === 'a') {
@@ -243,8 +307,28 @@ class MdToHtml {
}
}
if (codeBlockHandler) {
const extraCss = codeBlockHandler.extraCss();
const name = codeBlockHandler.name();
if (extraCss && !(name in extraCssBlocks)) {
extraCssBlocks[name] = extraCss;
}
}
previousToken = t;
}
// Insert the extra CSS at the top of the HTML
const temp = ['<style>'];
for (let n in extraCssBlocks) {
if (!extraCssBlocks.hasOwnProperty(n)) continue;
temp.push(extraCssBlocks[n]);
}
temp.push('</style>');
output = temp.concat(output);
return output.join('');
}
@@ -260,7 +344,6 @@ class MdToHtml {
breaks: true,
linkify: true,
});
const env = {};
// Hack to make checkboxes clickable. Ideally, checkboxes should be parsed properly in
// renderTokens_(), but for now this hack works. Marking it with HORRIBLE_HACK so
@@ -278,12 +361,14 @@ class MdToHtml {
}
}
const env = {};
const tokens = md.parse(body, env);
let renderedBody = this.renderTokens_(md, tokens, options);
// console.info(body);
// console.info(tokens);
let renderedBody = this.renderTokens_(tokens, options);
// console.info(renderedBody);
if (HORRIBLE_HACK) {
let loopCount = 0;
@@ -374,9 +459,14 @@ class MdToHtml {
width: auto;
max-width: 100%;
}
.katex .mfrac .frac-line:before {
/* top: 50%; */
/* padding-bottom: .7em; */
}
`;
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>'; //+ '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css">';
const output = styleHtml + renderedBody;

View File

@@ -0,0 +1,48 @@
const { shim } = require('lib/shim');
const katex = require('katex');
const katexCss = require('lib/csstojs/katex.css.js');
const Setting = require('lib/models/Setting');
class MdToHtml_Katex {
name() {
return 'katex';
}
processContent(renderedTokens, content, tagType) {
try {
let renderered = katex.renderToString(content);
if (tagType === 'block') renderered = '<p>' + renderered + '</p>';
renderedTokens.push(renderered);
} catch (error) {
renderedTokens.push('Cannot render Katex content: ' + error.message);
}
return renderedTokens;
}
extraCss() {
return katexCss;
}
async loadAssets() {
// In node, the fonts are simply copied using copycss to where Katex expects to find them, which is under app/gui/note-viewer/fonts
// In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed
// them as an asset and copy them from there (or load them from there by modifying Katex CSS), but for now that will do.
if (shim.isReactNative()) {
// Fonts must go under the resourceDir directory because this is the baseUrl of NoteBodyViewer
const baseDir = Setting.value('resourceDir');
await shim.fsDriver().mkdir(baseDir + '/fonts');
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Main-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Main-Regular.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Math-Italic.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Math-Italic.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Size1-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Size1-Regular.woff2' });
}
}
}
module.exports = MdToHtml_Katex;

View File

@@ -0,0 +1,52 @@
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const { FileApi } = require('lib/file-api.js');
const { Synchronizer } = require('lib/synchronizer.js');
const WebDavApi = require('lib/WebDavApi');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
class SyncTargetWebDAV extends BaseSyncTarget {
static id() {
return 6;
}
constructor(db, options = null) {
super(db, options);
}
static targetName() {
return 'webdav';
}
static label() {
return _('WebDAV (Beta)');
}
isAuthenticated() {
return true;
}
async initFileApi() {
const options = {
baseUrl: () => Setting.value('sync.6.path'),
username: () => Setting.value('sync.6.username'),
password: () => Setting.value('sync.6.password'),
};
const api = new WebDavApi(options);
const driver = new FileApiDriverWebDav(api);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(SyncTargetWebDAV.id());
fileApi.setLogger(this.logger());
return fileApi;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetWebDAV;

View File

@@ -42,18 +42,34 @@ class WebDavApi {
}
async xmlToJson(xml) {
let davNamespaces = []; // Yes, there can be more than one... xmlns:a="DAV:" xmlns:D="DAV:"
const nameProcessor = (name) => {
// const idx = name.indexOf(':');
// if (idx >= 0) {
// if (name.indexOf('xmlns:') !== 0) name = name.substr(idx + 1);
// }
if (name.indexOf('xmlns:') !== 0) {
// Check if the current name is within the DAV namespace. If it is, normalise it
// by moving it to the "d:" namespace, which is what all the functions are using.
const p = name.split(':');
if (p.length == 2) {
const ns = p[0];
if (davNamespaces.indexOf(ns) >= 0) {
name = 'd:' + p[1];
}
}
}
return name.toLowerCase();
};
const attrValueProcessor = (value, name) => {
if (value.toLowerCase() === 'dav:') {
const p = name.split(':');
davNamespaces.push(p[p.length - 1]);
}
}
const options = {
tagNameProcessors: [nameProcessor],
attrNameProcessors: [nameProcessor],
attrValueProcessors: [attrValueProcessor]
}
return new Promise((resolve, reject) => {
@@ -81,6 +97,14 @@ class WebDavApi {
}
if (type === 'string') {
// If the XML has not attribute the value is directly a string
// If the XML node has attributes, the value is under "_".
// Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}:
// <a:getlastmodified b:dt="dateTime.rfc1123">Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
// For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT"
// <a:getlastmodified>Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
if (typeof output === 'object' && '_' in output) output = output['_'];
if (typeof output !== 'string') return null;
return output;
}
@@ -109,7 +133,7 @@ class WebDavApi {
return this.valueFromJson(json, keys, 'array');
}
async execPropFind(path, depth, fields = null) {
async execPropFind(path, depth, fields = null, options = null) {
if (fields === null) fields = ['d:getlastmodified'];
let fieldsXml = '';
@@ -131,7 +155,7 @@ class WebDavApi {
</d:prop>
</d:propfind>`;
return this.exec('PROPFIND', path, body, { 'Depth': depth });
return this.exec('PROPFIND', path, body, { 'Depth': depth }, options);
}
// curl -u admin:123456 'http://nextcloud.local/remote.php/dav/files/admin/' -X PROPFIND --data '<?xml version="1.0" encoding="UTF-8"?>
@@ -151,6 +175,8 @@ class WebDavApi {
if (authToken) headers['Authorization'] = 'Basic ' + authToken;
if (typeof body === 'string') headers['Content-Length'] = body.length;
const fetchOptions = {};
fetchOptions.headers = headers;
fetchOptions.method = method;
@@ -161,6 +187,8 @@ class WebDavApi {
let response = null;
// console.info('WebDAV', method + ' ' + url, headers, options);
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
response = await shim.uploadBlob(url, fetchOptions);
} else if (options.target == 'string') {
@@ -198,7 +226,7 @@ class WebDavApi {
throw new JoplinError(method + ' ' + path + ': ' + message + ' (' + code + ')', response.status);
}
throw new JoplinError(shortResponseText(), response.status);
throw new JoplinError(method + ' ' + path + ': ' + shortResponseText(), response.status);
}
if (options.responseFormat === 'text') return responseText;

View File

@@ -2,7 +2,7 @@ const React = require('react'); const Component = React.Component;
const { connect } = require('react-redux');
const { NotesScreen } = require('lib/components/screens/notes.js');
const { SearchScreen } = require('lib/components/screens/search.js');
const { View } = require('react-native');
const { KeyboardAvoidingView, Keyboard, Platform, View } = require('react-native');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/components/global-style.js');
@@ -11,6 +11,31 @@ class AppNavComponent extends Component {
constructor() {
super();
this.previousRouteName_ = null;
this.state = {
autoCompletionBarExtraHeight: 0, // Extra padding for the auto completion bar at the top of the keyboard
}
}
componentWillMount() {
if (Platform.OS === 'ios') {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide.bind(this));
}
}
componentWillUnmount() {
if (this.keyboardDidShowListener) this.keyboardDidShowListener.remove();
if (this.keyboardDidHideListener) this.keyboardDidHideListener.remove();
this.keyboardDidShowListener = null;
this.keyboardDidHideListener = null;
}
keyboardDidShow () {
this.setState({ autoCompletionBarExtraHeight: 30 })
}
keyboardDidHide () {
this.setState({ autoCompletionBarExtraHeight:0 })
}
render() {
@@ -44,11 +69,12 @@ class AppNavComponent extends Component {
const style = { flex: 1, backgroundColor: theme.backgroundColor }
return (
<View style={style}>
<KeyboardAvoidingView behavior={ Platform.OS === 'ios' ? "padding" : null } style={style}>
<NotesScreen visible={notesScreenVisible} navigation={{ state: route }} />
{ searchScreenLoaded && <SearchScreen visible={searchScreenVisible} navigation={{ state: route }} /> }
{ (!notesScreenVisible && !searchScreenVisible) && <Screen navigation={{ state: route }} /> }
</View>
<View style={{ height: this.state.autoCompletionBarExtraHeight }} />
</KeyboardAvoidingView>
);
}

View File

@@ -51,7 +51,19 @@ class NoteBodyViewer extends Component {
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
};
const html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
let html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
html = `
<!DOCTYPE html>
<html>
<head>
</head>
<body>
` + html + `
</body>
</html>
`;
let webViewStyle = {}
// On iOS, the onLoadEnd() event is never fired so always

View File

@@ -372,7 +372,7 @@ class ScreenHeaderComponent extends Component {
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
for (let i = 0; i < this.props.folders.length; i++) {
let f = this.props.folders[i];
output.push({ label: f.title, value: f.id });
output.push({ label: Folder.displayTitle(f), value: f.id });
}
output.sort((a, b) => {
if (a.value === null) return -1;

View File

@@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView, TextInput } = require('react-native');
const { Platform, TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView, TextInput } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('lib/components/screen-header.js');
const { _, setLocale } = require('lib/locale.js');
@@ -72,6 +72,11 @@ class ConfigScreenComponent extends BaseScreenComponent {
},
}
if (Platform.OS === 'ios') {
styles.settingControl.borderBottomWidth = 1;
styles.settingControl.borderBottomColor = theme.dividerColor;
}
styles.switchSettingText = Object.assign({}, styles.settingText);
styles.switchSettingText.width = '80%';
@@ -106,8 +111,6 @@ class ConfigScreenComponent extends BaseScreenComponent {
settings: settings,
settingsChanged: true,
});
console.info(settings['sync.5.path']);
}
const md = Setting.settingMetadata(key);
@@ -163,7 +166,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
return (
<View key={key} style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>{md.label()}</Text>
<TextInput key="control" style={this.styles().settingControl} value={value} onChangeText={(value) => updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
<TextInput autoCapitalize="none" key="control" style={this.styles().settingControl} value={value} onChangeText={(value) => updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
</View>
);
} else {

View File

@@ -159,9 +159,15 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
const mkComps = [];
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(i+1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
@@ -183,6 +189,24 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
}
};
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(<Text style={this.styles().normalText} key={id}>{id}</Text>);
}
nonExistingMasterKeySection = (
<View>
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
<Text style={this.styles().normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
<View style={{marginTop: 10}}>{rows}</View>
</View>
);
}
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
const toggleButton = !this.state.passwordPromptShow ? <View style={{marginTop: 10}}><Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button></View> : null;
@@ -204,6 +228,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
{toggleButton}
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{flex:1, height: 20}}></View>
</ScrollView>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
@@ -221,6 +246,7 @@ const EncryptionConfigScreen = connect(
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
}
)(EncryptionConfigScreenComponent)

View File

@@ -23,6 +23,7 @@ class LogScreenComponent extends BaseScreenComponent {
});
this.state = {
dataSource: ds,
showErrorsOnly: false,
};
this.styles_ = {};
}
@@ -62,13 +63,24 @@ class LogScreenComponent extends BaseScreenComponent {
this.resfreshLogEntries();
}
resfreshLogEntries() {
reg.logger().lastEntries(1000).then((entries) => {
resfreshLogEntries(showErrorsOnly = null) {
if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR]
reg.logger().lastEntries(1000, { levels: levels }).then((entries) => {
const newDataSource = this.state.dataSource.cloneWithRows(entries);
this.setState({ dataSource: newDataSource });
});
}
toggleErrorsOnly() {
const showErrorsOnly = !this.state.showErrorsOnly;
this.setState({ showErrorsOnly: showErrorsOnly });
this.resfreshLogEntries(showErrorsOnly);
}
render() {
let renderRow = (item) => {
let textStyle = this.styles().rowText;
@@ -91,7 +103,14 @@ class LogScreenComponent extends BaseScreenComponent {
renderRow={renderRow}
enableEmptySections={true}
/>
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
<View style={{flexDirection: 'row'}}>
<View style={{flex:1, marginRight: 5 }}>
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
</View>
<View style={{flex:1}}>
<Button title={this.state.showErrorsOnly ? _("Show all") : _("Errors only")} onPress={() => { this.toggleErrorsOnly(); }}/>
</View>
</View>
</View>
);
}

View File

@@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image, KeyboardAvoidingView } = require('react-native');
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native');
const { connect } = require('react-redux');
const { uuid } = require('lib/uuid.js');
const { Log } = require('lib/log.js');
@@ -149,12 +149,6 @@ class NoteScreenComponent extends BaseScreenComponent {
await shared.initState(this);
this.refreshNoteMetadata();
if (Platform.OS === 'ios') {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow.bind(this));
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide.bind(this));
}
}
refreshNoteMetadata(force = null) {
@@ -163,19 +157,6 @@ class NoteScreenComponent extends BaseScreenComponent {
componentWillUnmount() {
BackButtonService.removeHandler(this.backHandler);
if (Platform.OS === 'ios'){
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
}
_keyboardDidShow () {
this.setState({ heightBumpView:30 })
}
_keyboardDidHide () {
this.setState({ heightBumpView:0 })
}
title_changeText(text) {
@@ -542,7 +523,7 @@ class NoteScreenComponent extends BaseScreenComponent {
);
return (
<KeyboardAvoidingView behavior= {(Platform.OS === 'ios')? "padding" : null} style={this.rootStyle(this.props.theme).root}>
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader
folderPickerOptions={{
enabled: true,
@@ -578,8 +559,7 @@ class NoteScreenComponent extends BaseScreenComponent {
/>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
<View style={{ height: this.state.heightBumpView }} />
</KeyboardAvoidingView>
</View>
);
}

View File

@@ -30,6 +30,7 @@ shared.saveNoteButton_press = async function(comp) {
options.fields = BaseModel.diffObjectsFields(comp.state.lastSavedNote, note);
}
const hasAutoTitle = comp.state.newAndNoTitleChangeNoteId || (isNew && !note.title);
if (hasAutoTitle) {
note.title = Note.defaultTitle(note);
@@ -63,11 +64,33 @@ shared.saveNoteButton_press = async function(comp) {
note: note,
};
if (isNew) newState.newAndNoTitleChangeNoteId = note.id;
if (isNew && hasAutoTitle) newState.newAndNoTitleChangeNoteId = note.id;
comp.setState(newState);
if (isNew) Note.updateGeolocation(note.id);
if (isNew) {
Note.updateGeolocation(note.id).then((geoNote) => {
const stateNote = comp.state.note;
if (!stateNote || !geoNote) return;
if (stateNote.id !== geoNote.id) return; // Another note has been loaded while geoloc was being retrieved
// Geo-location for this note has been saved to the database however the properties
// are is not in the state so set them now.
const geoInfo = {
longitude: geoNote.longitude,
latitude: geoNote.latitude,
altitude: geoNote.altitude,
}
const modNote = Object.assign({}, stateNote, geoInfo);
const modLastSavedNote = Object.assign({}, comp.state.lastSavedNote, geoInfo);
comp.setState({ note: modNote, lastSavedNote: modLastSavedNote });
comp.refreshNoteMetadata();
});
}
comp.refreshNoteMetadata();
if (isNew) {

View File

@@ -2,6 +2,11 @@ const BaseItem = require('lib/models/BaseItem.js');
const { time } = require('lib/time-utils.js');
const { basicDelta } = require('lib/file-api');
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js');
const Entities = require('html-entities').AllHtmlEntities;
const html_entity_decode = (new Entities()).decode;
const { shim } = require('lib/shim');
const { basename } = require('lib/path-utils');
const JoplinError = require('lib/JoplinError');
class FileApiDriverWebDav {
@@ -18,6 +23,7 @@ class FileApiDriverWebDav {
const result = await this.api().execPropFind(path, 0, [
'd:getlastmodified',
'd:resourcetype',
'd:getcontentlength', // Remove this once PUT call issue is sorted out
]);
const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]);
@@ -32,6 +38,9 @@ class FileApiDriverWebDav {
const isCollection = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:resourcetype', 0, 'd:collection', 0]);
const lastModifiedString = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getlastmodified', 0]);
const sizeDONOTUSE = Number(this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getcontentlength', 0]));
if (isNaN(sizeDONOTUSE)) throw new Error('Cannot get content size: ' + JSON.stringify(resource));
if (!lastModifiedString) throw new Error('Could not get lastModified date: ' + JSON.stringify(resource));
const lastModifiedDate = new Date(lastModifiedString);
@@ -42,24 +51,10 @@ class FileApiDriverWebDav {
created_time: lastModifiedDate.getTime(),
updated_time: lastModifiedDate.getTime(),
isDir: isCollection === '',
sizeDONOTUSE: sizeDONOTUSE, // This property is used only for the WebDAV PUT hack (see below) so mark it as such so that it can be removed with the hack later on.
};
}
statsFromResources_(resources) {
const relativeBaseUrl = this.api().relativeBaseUrl();
let output = [];
for (let i = 0; i < resources.length; i++) {
const resource = resources[i];
const href = this.api().stringFromJson(resource, ['d:href', 0]);
if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
if (path === '') continue; // The list of resources includes the root dir too, which we don't want
const stat = this.statFromResource_(resources[i], path);
output.push(stat);
}
return output;
}
async setTimestamp(path, timestampMs) {
throw new Error('Not implemented'); // Not needed anymore
}
@@ -73,16 +68,198 @@ class FileApiDriverWebDav {
return await basicDelta(path, getDirStats, options);
}
// A file href, as found in the result of a PROPFIND, can be either an absolute URL or a
// relative URL (an absolute URL minus the protocol and domain), while the sync algorithm
// works with paths relative to the base URL.
hrefToRelativePath_(href, baseUrl, relativeBaseUrl) {
let output = '';
if (href.indexOf(baseUrl) === 0) {
output = href.substr(baseUrl.length);
} else if (href.indexOf(relativeBaseUrl) === 0) {
output = href.substr(relativeBaseUrl.length);
} else {
throw new Error('href ' + href + ' not in baseUrl ' + baseUrl + ' nor relativeBaseUrl ' + relativeBaseUrl);
}
return rtrimSlashes(ltrimSlashes(output));
}
statsFromResources_(resources) {
const relativeBaseUrl = this.api().relativeBaseUrl();
const baseUrl = this.api().baseUrl();
let output = [];
for (let i = 0; i < resources.length; i++) {
const resource = resources[i];
const href = this.api().stringFromJson(resource, ['d:href', 0]);
const path = this.hrefToRelativePath_(href, baseUrl, relativeBaseUrl);
// if (href.indexOf(relativeBaseUrl) !== 0) throw new Error('Path "' + href + '" not inside base URL: ' + relativeBaseUrl);
// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
if (path === '') continue; // The list of resources includes the root dir too, which we don't want
const stat = this.statFromResource_(resources[i], path);
output.push(stat);
}
return output;
}
async list(path, options) {
// const relativeBaseUrl = this.api().relativeBaseUrl();
// function parsePropFindXml(xmlString) {
// return new Promise(async (resolve, reject) => {
// const saxOptions = {};
// const saxParser = require('sax').parser(false, { position: false });
// let stats = [];
// let currentStat = null;
// let currentText = '';
// // When this is on, the tags from the bloated XML string are replaced by shorter ones,
// // which makes parsing about 25% faster. However it's a bit of a hack so keep it as
// // an option so that it can be disabled if it causes problems.
// const optimizeXml = true;
// const tagResponse = optimizeXml ? 'd:r' : 'd:response';
// const tagGetLastModified = optimizeXml ? 'd:glm' : 'd:getlastmodified';
// const tagPropStat = optimizeXml ? 'd:ps' : 'd:propstat';
// const replaceUrls = optimizeXml;
// saxParser.onerror = function (error) {
// reject(new Error(e.toString()));
// };
// saxParser.ontext = function (t) {
// currentText += t;
// };
// saxParser.onopentag = function (node) {
// const tagName = node.name.toLowerCase();
// currentText = '';
// if (tagName === tagResponse) {
// currentStat = { isDir: false };
// }
// };
// saxParser.onclosetag = function(tagName) {
// tagName = tagName.toLowerCase();
// if (tagName === tagResponse) {
// if (currentStat.path) { // The list of resources includes the root dir too, which we don't want
// if (!currentStat.updated_time) throw new Error('Resource does not have a getlastmodified prop');
// stats.push(currentStat);
// }
// currentStat = null;
// }
// if (tagName === 'd:href') {
// const href = currentText;
// if (replaceUrls) {
// currentStat.path = rtrimSlashes(ltrimSlashes(href));
// } else {
// if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
// currentStat.path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
// }
// }
// if (tagName === tagGetLastModified) {
// const lastModifiedDate = new Date(currentText);
// if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + currentText);
// currentStat.updated_time = lastModifiedDate.getTime();
// currentStat.created_time = currentStat.updated_time;
// }
// if (tagName === 'd:collection') {
// currentStat.isDir = true;
// }
// currentText = '';
// }
// saxParser.onend = function () {
// resolve(stats);
// };
// if (optimizeXml) {
// xmlString = xmlString.replace(/<d:status>HTTP\/1\.1 200 OK<\/d:status>/ig, '');
// xmlString = xmlString.replace(/<d:resourcetype\/>/ig, '');
// xmlString = xmlString.replace(/d:getlastmodified/ig, tagGetLastModified);
// xmlString = xmlString.replace(/d:response/ig, tagResponse);
// xmlString = xmlString.replace(/d:propstat/ig, tagPropStat);
// if (replaceUrls) xmlString = xmlString.replace(new RegExp(relativeBaseUrl, 'gi'), '');
// }
// let idx = 0;
// let size = 1024 * 100;
// while (true) {
// sub = xmlString.substr(idx, size);
// if (!sub.length) break;
// saxParser.write(sub);
// idx += size;
// //await time.msleep(500);
// }
// saxParser.close();
// //saxParser.write(xmlString).close();
// });
// }
// For performance reasons, the response of the PROPFIND call is manually parsed with a regex below
// instead of being processed by xml2json like the other WebDAV responses. This is over 2 times faster
// and it means the mobile app does not freeze during sync.
// async function parsePropFindXml2(xmlString) {
// const regex = /<d:response>[\S\s]*?<d:href>([\S\s]*?)<\/d:href>[\S\s]*?<d:getlastmodified>(.*?)<\/d:getlastmodified>/g;
// let output = [];
// let match = null;
// while (match = regex.exec(xmlString)) {
// const href = html_entity_decode(match[1]);
// if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
// if (!path) continue; // The list of resources includes the root dir too, which we don't want
// const lastModifiedDate = new Date(match[2]);
// if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + match[2]);
// output.push({
// path: path,
// updated_time: lastModifiedDate.getTime(),
// created_time: lastModifiedDate.getTime(),
// isDir: !BaseItem.isSystemPath(path),
// });
// }
// return output;
// }
// const resultXml = await this.api().execPropFind(path, 1, [
// 'd:getlastmodified',
// //'d:resourcetype', // Include this to use parsePropFindXml()
// ], { responseFormat: 'text' });
// const stats = await parsePropFindXml2(resultXml);
// return {
// items: stats,
// hasMore: false,
// context: null,
// };
const result = await this.api().execPropFind(path, 1, [
'd:getlastmodified',
'd:resourcetype',
]);
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
const stats = this.statsFromResources_(resources)
return {
items: this.statsFromResources_(resources),
items: stats,
hasMore: false,
context: null,
};
@@ -92,7 +269,13 @@ class FileApiDriverWebDav {
if (!options) options = {};
if (!options.responseFormat) options.responseFormat = 'text';
try {
return await this.api().exec('GET', path, null, null, options);
const response = await this.api().exec('GET', path, null, null, options);
// This is awful but instead of a 404 Not Found, Microsoft IIS returns an HTTP code 200
// with a response body "The specified file doesn't exist." for non-existing files,
// so we need to check for this.
if (response === "The specified file doesn't exist.") throw new JoplinError(response, 404);
return response;
} catch (error) {
if (error.code !== 404) throw error;
}
@@ -102,12 +285,47 @@ class FileApiDriverWebDav {
try {
await this.api().exec('MKCOL', path);
} catch (error) {
if (error.code !== 405) throw error; // 405 means that the collection already exists (Method Not Allowed)
if (error.code === 405) return; // 405 means that the collection already exists (Method Not Allowed)
// 409 should only be returned if a parent path does not exists (eg. when trying to create a/b/c when a/b does not exist)
// however non-compliant servers (eg. Microsoft IIS) also return this code when the directory already exists. So here, if
// we get this code, verify that indeed the directory already exists and exit if it does.
if (error.code === 409) {
const stat = await this.stat(path);
if (stat) return;
}
throw error;
}
}
async put(path, content, options = null) {
await this.api().exec('PUT', path, content, null, options);
// In theory, if a client doesn't complete an upload, the file will not appear in the Nextcloud app. Likewise if
// the server interrupts the upload midway, the client should receive some kind of error and try uploading the
// file again next time. At the very least the file should not appear half-uploaded on the server. In practice
// however it seems some files might end up half uploaded on the server (at least on ocloud.de) so, for now,
// instead of doing a simple PUT, we do it to a temp file on Nextcloud, then check the file size and, if it
// matches, move it its actual place (hoping the server won't mess up and only copy half of the file).
// This is innefficient so once the bug is better understood it should hopefully be possible to go back to
// using a single PUT call.
let contentSize = 0;
if (content) contentSize = content.length;
if (options && options.path) {
const stat = await shim.fsDriver().stat(options.path);
contentSize = stat.size;
}
const tempPath = this.fileApi_.tempDirName() + '/' + basename(path) + '_' + Date.now();
await this.api().exec('PUT', tempPath, content, null, options);
const stat = await this.stat(tempPath);
if (stat.sizeDONOTUSE != contentSize) {
// await this.delete(tempPath);
throw new Error('WebDAV PUT - Size check failed for ' + tempPath + ' Expected: ' + contentSize + '. Found: ' + stat.sizeDONOTUSE);
}
await this.move(tempPath, path);
}
async delete(path) {
@@ -119,7 +337,10 @@ class FileApiDriverWebDav {
}
async move(oldPath, newPath) {
throw new Error('Not implemented');
await this.api().exec('MOVE', oldPath, null, {
'Destination': this.api().baseUrl() + '/' + newPath,
'Overwrite': 'T',
});
}
format() {

View File

@@ -3,6 +3,7 @@ const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim');
const BaseItem = require('lib/models/BaseItem.js');
const JoplinError = require('lib/JoplinError');
const ArrayUtils = require('lib/ArrayUtils');
class FileApi {
@@ -11,6 +12,17 @@ class FileApi {
this.driver_ = driver;
this.logger_ = new Logger();
this.syncTargetId_ = null;
this.tempDirName_ = null;
this.driver_.fileApi_ = this;
}
tempDirName() {
if (this.tempDirName_ === null) throw Error('Temp dir not set!');
return this.tempDirName_;
}
setTempDirName(v) {
this.tempDirName_ = v;
}
fsDriver() {
@@ -39,9 +51,10 @@ class FileApi {
}
fullPath_(path) {
let output = this.baseDir_;
if (path != '') output += '/' + path;
return output;
let output = [];
if (this.baseDir_) output.push(this.baseDir_);
if (path) output.push(path);
return output.join('/');
}
// DRIVER MUST RETURN PATHS RELATIVE TO `path`
@@ -133,14 +146,19 @@ function basicDeltaContextFromOptions_(options) {
timestamp: 0,
filesAtTimestamp: [],
statsCache: null,
statIdsCache: null,
deletedItemsProcessed: false,
};
if (!options || !options.context) return output;
const d = new Date(options.context.timestamp);
output.timestamp = isNaN(d.getTime()) ? 0 : options.context.timestamp;
output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : [];
output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null;
output.statIdsCache = options.context && options.context.statIdsCache ? options.context.statIdsCache : null;
output.deletedItemsProcessed = options.context && ('deletedItemsProcessed' in options.context) ? options.context.deletedItemsProcessed : false;
return output;
}
@@ -159,6 +177,8 @@ async function basicDelta(path, getDirStatFn, options) {
timestamp: context.timestamp,
filesAtTimestamp: context.filesAtTimestamp.slice(),
statsCache: context.statsCache,
statIdsCache: context.statIdsCache,
deletedItemsProcessed: context.deletedItemsProcessed,
};
// Stats are cached until all items have been processed (until hasMore is false)
@@ -167,6 +187,8 @@ async function basicDelta(path, getDirStatFn, options) {
newContext.statsCache.sort(function(a, b) {
return a.updated_time - b.updated_time;
});
newContext.statIdsCache = newContext.statsCache.map((item) => BaseItem.pathToId(item.path));
newContext.statIdsCache.sort(); // Items must be sorted to use binary search below
}
let output = [];
@@ -203,31 +225,27 @@ async function basicDelta(path, getDirStatFn, options) {
if (output.length >= outputLimit) break;
}
// Find out which items have been deleted on the sync target by comparing the items
// we have to the items on the target.
let deletedItems = [];
for (let i = 0; i < itemIds.length; i++) {
if (output.length + deletedItems.length >= outputLimit) break;
if (!newContext.deletedItemsProcessed) {
// Find out which items have been deleted on the sync target by comparing the items
// we have to the items on the target.
// Note that when deleted items are processed it might result in the output having
// more items than outputLimit. This is acceptable since delete operations are cheap.
let deletedItems = [];
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
const itemId = itemIds[i];
let found = false;
for (let j = 0; j < newContext.statsCache.length; j++) {
const item = newContext.statsCache[j];
if (BaseItem.pathToId(item.path) == itemId) {
found = true;
break;
if (ArrayUtils.binarySearch(newContext.statIdsCache, itemId) < 0) {
deletedItems.push({
path: BaseItem.systemPath(itemId),
isDeleted: true,
});
}
}
if (!found) {
deletedItems.push({
path: BaseItem.systemPath(itemId),
isDeleted: true,
});
}
output = output.concat(deletedItems);
}
output = output.concat(deletedItems);
newContext.deletedItemsProcessed = true;
const hasMore = output.length >= outputLimit;
if (!hasMore) newContext.statsCache = null;

View File

@@ -297,4 +297,4 @@ function _(s, ...args) {
return sprintf(result, ...args);
}
module.exports = { _, supportedLocales, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode };
module.exports = { _, supportedLocales, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };

View File

@@ -25,6 +25,10 @@ class Logger {
return this.level_;
}
targets() {
return this.targets_;
}
clearTargets() {
this.targets_.clear();
}
@@ -81,11 +85,15 @@ class Logger {
}
// Only for database at the moment
async lastEntries(limit = 100) {
async lastEntries(limit = 100, options = null) {
if (options === null) options = {};
if (!options.levels) options.levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
if (!options.levels.length) return [];
for (let i = 0; i < this.targets_.length; i++) {
const target = this.targets_[i];
if (target.type == 'database') {
let sql = 'SELECT * FROM logs ORDER BY timestamp DESC';
let sql = 'SELECT * FROM logs WHERE level IN (' + options.levels.join(',') + ') ORDER BY timestamp DESC';
if (limit !== null) sql += ' LIMIT ' + limit
return await target.database.selectAll(sql);
}

View File

@@ -60,6 +60,19 @@ class Setting extends BaseModel {
// })},
'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted todos on top of the lists') },
'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save geo-location with notes') },
'newTodoFocus': { value: 'title', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new to-do:'), options: () => {
return {
'title': _('Focus title'),
'body': _('Focus body'),
};
}},
'newNoteFocus': { value: 'body', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new note:'), options: () => {
return {
'title': _('Focus title'),
'body': _('Focus body'),
};
}},
'showTrayIcon': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Show tray icon') },
'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false },
'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false },
'encryption.passwordCache': { value: {}, type: Setting.TYPE_OBJECT, public: false },
@@ -93,6 +106,11 @@ class Setting extends BaseModel {
'sync.5.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud WebDAV URL') },
'sync.5.username': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud username') },
'sync.5.password': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud password'), secure: true },
'sync.6.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV URL') },
'sync.6.username': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV username') },
'sync.6.password': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV password'), secure: true },
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },

View File

@@ -72,8 +72,15 @@ class DecryptionWorker {
for (let i = 0; i < items.length; i++) {
const item = items[i];
// Temp hack
if (['edf44b7a0e4f8cbf248e206cd8dfa800', '2ccb3c9af0b1adac2ec6b66a5961fbb1'].indexOf(item.id) >= 0) {
excludedIds.push(item.id);
continue;
}
const ItemClass = BaseItem.itemClass(item);
this.logger().debug('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
this.logger().info('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
try {
await ItemClass.decrypt(item);
} catch (error) {
@@ -88,8 +95,7 @@ class DecryptionWorker {
}
continue;
}
this.logger().warn('DecryptionWorker: error for: ' + item.id + ' (' + ItemClass.tableName() + ')');
throw error;
this.logger().warn('DecryptionWorker: error for: ' + item.id + ' (' + ItemClass.tableName() + ')', error);
}
}

View File

@@ -4,6 +4,7 @@ const { PoorManIntervals } = require('lib/poor-man-intervals.js');
const RNFetchBlob = require('react-native-fetch-blob').default;
const { generateSecureRandom } = require('react-native-securerandom');
const FsDriverRN = require('lib/fs-driver-rn.js').FsDriverRN;
const urlValidator = require('valid-url');
function shimInit() {
shim.Geolocation = GeolocationReact;
@@ -27,20 +28,15 @@ function shimInit() {
}
shim.fetch = async function(url, options = null) {
// The native fetch() throws an uncatable error that crashes the app if calling it with an
// invalid URL such as '//.resource' or "http://ocloud. de" so detect if the URL is valid beforehand
// and throw a catchable error.
// Bug: https://github.com/facebook/react-native/issues/7436
const validatedUrl = urlValidator.isUri(url);
if (!validatedUrl) throw new Error('Not a valid URL: ' + url);
return shim.fetchWithRetry(() => {
// The native fetch() throws an uncatable error if calling it with an invalid URL
// such as '//.resource' so detect if the URL is valid beforehand and throw
// a catchable error.
if (typeof url !== 'string') {
console.info('NOT A STRING: ', url);
throw new Error('shim.fetch: URL is not a string');
}
const lcUrl = url.toLowerCase();
if (lcUrl.indexOf('http:') !== 0 && lcUrl.indexOf('https:') !== 0) throw new Error('shim.fetch: Invalid URL: ' + lcUrl);
return shim.nativeFetch_(url, options);
return fetch(validatedUrl, options);
}, options);
}
@@ -49,12 +45,20 @@ function shimInit() {
let headers = options.headers ? options.headers : {};
let method = options.method ? options.method : 'GET';
const overwrite = 'overwrite' in options ? options.overwrite : true;
let dirs = RNFetchBlob.fs.dirs;
let localFilePath = options.path;
if (localFilePath.indexOf('/') !== 0) localFilePath = dirs.DocumentDir + '/' + localFilePath;
if (!overwrite) {
if (await shim.fsDriver().exists(localFilePath)) {
return { ok: true };
}
}
delete options.path;
delete options.overwrite;
const doFetchBlob = () => {
return RNFetchBlob.config({

View File

@@ -104,7 +104,6 @@ shim.fetchWithRetry = async function(fetchFn, options = null) {
}
}
shim.nativeFetch_ = typeof fetch !== 'undefined' ? fetch : null;
shim.fetch = () => { throw new Error('Not implemented'); }
shim.FormData = typeof FormData !== 'undefined' ? FormData : null;
shim.fsDriver = () => { throw new Error('Not implemented') }

View File

@@ -18,7 +18,7 @@ class Synchronizer {
this.state_ = 'idle';
this.db_ = db;
this.api_ = api;
//this.syncDirName_ = '.sync';
this.syncDirName_ = '.sync';
this.resourceDirName_ = '.resource';
this.logger_ = new Logger();
this.appType_ = appType;
@@ -195,7 +195,8 @@ class Synchronizer {
this.logSyncOperation('starting', null, null, 'Starting synchronisation to target ' + syncTargetId + '... [' + synchronizationId + ']');
try {
//await this.api().mkdir(this.syncDirName_);
await this.api().mkdir(this.syncDirName_);
this.api().setTempDirName(this.syncDirName_);
await this.api().mkdir(this.resourceDirName_);
let donePaths = [];
@@ -449,7 +450,7 @@ class Synchronizer {
if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder
const loadContent = async () => {
content = await this.api().get(path);
let content = await this.api().get(path);
if (!content) return null;
return await BaseItem.unserialize(content);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3397,6 +3397,14 @@
"verror": "1.10.0"
}
},
"katex": {
"version": "0.9.0-beta1",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.9.0-beta1.tgz",
"integrity": "sha512-M7c7Eihp665Bh9wDR0xg/PdE1OuCa15PsiDQSBYyr+xJR8WrFP8nxdNF1lNUCBPzEup4zECG2jFUIZnU66xBRQ==",
"requires": {
"match-at": "0.1.1"
}
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@@ -3658,6 +3666,11 @@
"uc.micro": "1.0.3"
}
},
"match-at": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz",
"integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q=="
},
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
@@ -5390,9 +5403,9 @@
}
},
"sax": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.1.6.tgz",
"integrity": "sha1-XWFr6KXmB9VOEUr65Vt+ry/MMkA="
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "5.4.1",
@@ -6030,6 +6043,11 @@
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
},
"valid-url": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz",
"integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA="
},
"validate-npm-package-license": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
@@ -6255,7 +6273,7 @@
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": "1.1.6",
"sax": "1.2.4",
"xmlbuilder": "9.0.4"
},
"dependencies": {
@@ -6287,6 +6305,13 @@
"integrity": "sha1-0lciS+g5PqrL+DfvIn/Y7CWzaIg=",
"requires": {
"sax": "1.1.6"
},
"dependencies": {
"sax": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.1.6.tgz",
"integrity": "sha1-XWFr6KXmB9VOEUr65Vt+ry/MMkA="
}
}
},
"xmldom": {

View File

@@ -6,7 +6,8 @@
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest"
"test": "jest",
"postinstall": "node ../Tools/copycss.js"
},
"dependencies": {
"base-64": "^0.1.0",
@@ -14,6 +15,7 @@
"events": "^1.1.1",
"form-data": "^2.1.4",
"html-entities": "^1.2.1",
"katex": "^0.9.0-beta1",
"markdown-it": "^8.4.0",
"md5": "^2.2.1",
"moment": "^2.18.1",
@@ -45,6 +47,7 @@
"timers": "^0.1.1",
"url-parse": "^1.2.0",
"uuid": "^3.0.1",
"valid-url": "^1.0.9",
"word-wrap": "^1.2.3",
"xml2js": "^0.4.19"
},

View File

@@ -52,9 +52,11 @@ const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
SyncTargetRegistry.addClass(SyncTargetWebDAV);
// Disabled because not fully working
//SyncTargetRegistry.addClass(SyncTargetFilesystem);

View File

@@ -1,18 +0,0 @@
const fs = require('fs-extra');
const toolUtils = require('./tool-utils.js');
const rnDir = __dirname + '/../ReactNativeClient';
function androidVersionNumber() {
let content = fs.readFileSync(rnDir + '/android/app/build.gradle', 'utf8');
const r = content.match(/versionName\s+"(\d+?\.\d+?\.\d+)"/)
if (r.length !== 2) throw new Error('Could not get version number');
return r[1];
}
// const
// '/android/app/build/outputs/apk/app-armeabi-v7a-release.apk'
console.info(androidVersionNumber());

View File

@@ -1,5 +1,7 @@
"use strict"
require('app-module-path').addPath(__dirname + '/../ReactNativeClient');
const rootDir = __dirname + '/..';
const processArgs = process.argv.splice(2, process.argv.length);
@@ -16,6 +18,7 @@ const rnDir = rootDir + '/ReactNativeClient';
const electronDir = rootDir + '/ElectronClient/app';
const { execCommand } = require('./tool-utils.js');
const { countryDisplayName, countryCodeOnly } = require('lib/locale.js');
function parsePoFile(filePath) {
const content = fs.readFileSync(filePath);
@@ -105,6 +108,61 @@ function availableLocales(defaultLocale) {
return output;
}
async function translationStatus(isDefault, poFile) {
// "apt install translate-toolkit" to have pocount
const command = 'pocount "' + poFile + '"';
const result = await execCommand(command);
const matches = result.match(/translated:\s*?(\d+)\s*\((.+?)%\)/);
if (matches.length < 3) throw new Error('Cannot extract status: ' + command + ':\n' + result);
const percentDone = Number(matches[2]);
if (isNaN(percentDone)) throw new Error('Cannot extract percent translated: ' + command + ':\n' + result);
let translatorName = '';
const content = await fs.readFile(poFile, 'utf-8');
// "Last-Translator: Hrvoje Mandić <trbuhom@net.hr>\n"
const translatorMatch = content.match(/Last-Translator:\s*?(.*)/);
if (translatorMatch.length >= 1) {
translatorName = translatorMatch[1];
translatorName = translatorName.replace(/["\s]+$/, '');
translatorName = translatorName.replace(/\\n$/, '');
translatorName = translatorName.replace(/^\s*/, '');
}
if (translatorName.indexOf('FULL NAME') >= 0) translatorName = '';
return {
percentDone: isDefault ? 100 : percentDone,
translatorName: translatorName,
};
}
function translationStatusToMdTable(status) {
let output = [];
output.push(['&nbsp;', 'Language', 'Code', 'Last translator', 'Percent done'].join(' | '));
output.push(['---', '---', '---', '---', '---'].join('|'));
for (let i = 0; i < status.length; i++) {
const stat = status[i];
const flagUrl = 'https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/' + countryCodeOnly(stat.locale).toLowerCase() + '.png';
output.push(['![](' + flagUrl + ')', stat.languageName, stat.locale, stat.translatorName, stat.percentDone + '%'].join(' | '));
}
return output.join('\n');
}
async function updateReadmeWithStats(stats) {
const mdTableMarkerOpen = '<!-- LOCALE-TABLE-AUTO-GENERATED -->\n';
const mdTableMarkerClose = '\n<!-- LOCALE-TABLE-AUTO-GENERATED -->';
let mdTable = translationStatusToMdTable(stats);
mdTable = mdTableMarkerOpen + mdTable + mdTableMarkerClose;
let content = await fs.readFile(rootDir + '/README.md', 'utf-8');
// [^]* matches any character including new lines
const regex = new RegExp(mdTableMarkerOpen + '[^]*?' + mdTableMarkerClose);
content = content.replace(regex, mdTable);
await fs.writeFile(rootDir + '/README.md', content);
}
async function main() {
let potFilePath = cliLocalesDir + '/joplin.pot';
let jsonLocalesDir = cliDir + '/build/locales';
@@ -126,6 +184,8 @@ async function main() {
fs.mkdirpSync(jsonLocalesDir, 0o755);
let stats = [];
let locales = availableLocales(defaultLocale);
for (let i = 0; i < locales.length; i++) {
const locale = locales[i];
@@ -133,8 +193,15 @@ async function main() {
const jsonFilePath = jsonLocalesDir + '/' + locale + '.json';
if (locale != defaultLocale) await mergePotToPo(potFilePath, poFilePäth);
buildLocale(poFilePäth, jsonFilePath);
const stat = await translationStatus(defaultLocale === locale, poFilePäth);
stat.locale = locale;
stat.languageName = countryDisplayName(locale);
stats.push(stat);
}
stats.sort((a, b) => a.languageName < b.languageName ? -1 : +1);
saveToFile(jsonLocalesDir + '/index.js', buildIndex(locales));
const rnJsonLocaleDir = rnDir + '/locales';
@@ -142,6 +209,8 @@ async function main() {
const electronJsonLocaleDir = electronDir + '/locales';
await execCommand('rsync -a "' + jsonLocalesDir + '/" "' + electronJsonLocaleDir + '"');
await updateReadmeWithStats(stats);
}
main().catch((error) => {

27
Tools/copycss.js Normal file
View File

@@ -0,0 +1,27 @@
const fs = require('fs-extra');
const cwd = process.cwd();
const outputDir = cwd + '/lib/csstojs';
async function createJsFromCss(name, filePath) {
let css = await fs.readFile(filePath, 'utf-8');
css = css.replace(/\`/g, '\\`');
const js = 'module.exports = `' + css + '`;';
const outputPath = outputDir + '/' + name + '.css.js';
await fs.writeFile(outputPath, js);
}
async function main(argv) {
await fs.mkdirp(outputDir);
await createJsFromCss('katex', cwd + '/node_modules/katex/dist/katex.min.css');
if (argv.indexOf('--copy-fonts') >= 0) {
await fs.copy(cwd + '/node_modules/katex/dist/fonts', cwd + '/gui/note-viewer/fonts');
}
}
main(process.argv).catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -4,6 +4,11 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"app-module-path": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz",
"integrity": "sha1-ZBqlXft9am8KgUHEucCqULbCTdU="
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",

View File

@@ -9,6 +9,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"app-module-path": "^2.2.0",
"fs-extra": "^4.0.2",
"gettext-parser": "^1.3.0",
"marked": "^0.3.7",

View File

@@ -1,36 +0,0 @@
const fs = require('fs-extra');
const rnDir = __dirname + '/../ReactNativeClient';
function increaseGradleVersionCode(content) {
const newContent = content.replace(/versionCode\s+(\d+)/, function(a, versionCode, c) {
const n = Number(versionCode);
if (isNaN(n) || !n) throw new Error('Invalid version code: ' + versionCode);
return 'versionCode ' + (n + 1);
});
if (newContent === content) throw new Error('Could not update version code');
return newContent;
}
function increaseGradleVersionName(content) {
const newContent = content.replace(/(versionName\s+"\d+?\.\d+?\.)(\d+)"/, function(match, prefix, buildNum) {
const n = Number(buildNum);
if (isNaN(n) || !n) throw new Error('Invalid version code: ' + versionCode);
return prefix + (n + 1) + '"';
});
if (newContent === content) throw new Error('Could not update version name');
return newContent;
}
function updateGradleConfig() {
let content = fs.readFileSync(rnDir + '/android/app/build.gradle', 'utf8');
content = increaseGradleVersionCode(content);
content = increaseGradleVersionName(content);
fs.writeFileSync(rnDir + '/android/app/build.gradle', content);
}
updateGradleConfig();

View File

@@ -1,5 +1,5 @@
const fs = require('fs-extra');
const { execCommand } = require('./tool-utils.js');
const { execCommand, githubRelease, githubOauthToken } = require('./tool-utils.js');
const path = require('path');
const fetch = require('node-fetch');
const uriTemplate = require('uri-template');
@@ -46,14 +46,7 @@ function gradleVersionName(content) {
return matches[1];
}
async function githubOauthToken() {
const r = await fs.readFile(rootDir + '/Tools/github_oauth_token.txt');
return r.toString();
}
async function main() {
const oauthToken = await githubOauthToken();
const newContent = updateGradleConfig();
const version = gradleVersionName(newContent);
const tagName = 'android-v' + version;
@@ -88,28 +81,14 @@ async function main() {
console.info('Creating GitHub release ' + tagName + '...');
const response = await fetch('https://api.github.com/repos/laurent22/joplin/releases', {
method: 'POST',
body: JSON.stringify({
tag_name: tagName,
name: tagName,
draft: false,
}),
headers: {
'Content-Type': 'application/json',
'Authorization': 'token ' + oauthToken,
},
});
const responseText = await response.text();
const responseJson = JSON.parse(responseText);
if (!responseJson.upload_url) throw new Error('No upload URL for release: ' + responseText);
const uploadUrlTemplate = uriTemplate.parse(responseJson.upload_url);
const release = await githubRelease(tagName, false);
const uploadUrlTemplate = uriTemplate.parse(release.upload_url);
const uploadUrl = uploadUrlTemplate.expand({ name: apkFilename });
const binaryBody = await fs.readFile(apkFilePath);
const oauthToken = await githubOauthToken();
console.info('Uploading ' + apkFilename + ' to ' + uploadUrl);
const uploadResponse = await fetch(uploadUrl, {

31
Tools/release-electron.js Normal file
View File

@@ -0,0 +1,31 @@
const { execCommand, githubRelease, handleCommitHook, githubOauthToken } = require('./tool-utils.js');
const path = require('path');
const rootDir = path.dirname(__dirname);
const appDir = rootDir + '/ElectronClient/app';
async function main() {
const oauthToken = await githubOauthToken();
process.chdir(appDir);
console.info('Running from: ' + process.cwd());
const version = (await execCommand('npm version patch')).trim();
const tagName = version;
console.info('New version number: ' + version);
console.info(await execCommand('git add -A'));
console.info(await execCommand('git commit -m "Electron release ' + version + '"'));
console.info(await execCommand('git tag ' + tagName));
console.info(await execCommand('git push && git push --tags'));
const release = await githubRelease(tagName, true);
console.info('Created GitHub release: ' + release.html_url);
}
main().catch((error) => {
console.error('Fatal error');
console.error(error);
});

View File

@@ -81,4 +81,38 @@ toolUtils.fileExists = async function(filePath) {
});
}
toolUtils.githubOauthToken = async function() {
const fs = require('fs-extra');
const r = await fs.readFile(__dirname + '/github_oauth_token.txt');
return r.toString();
}
toolUtils.githubRelease = async function(tagName, isDraft) {
const fetch = require('node-fetch');
const oauthToken = await toolUtils.githubOauthToken();
const response = await fetch('https://api.github.com/repos/laurent22/joplin/releases', {
method: 'POST',
body: JSON.stringify({
tag_name: tagName,
name: tagName,
draft: isDraft,
}),
headers: {
'Content-Type': 'application/json',
'Authorization': 'token ' + oauthToken,
},
});
const responseText = await response.text();
if (!response.ok) throw new Error('Cannot create GitHub release: ' + responseText);
const responseJson = JSON.parse(responseText);
if (!responseJson.url) throw new Error('No URL for release: ' + responseText);
return responseJson;
}
module.exports = toolUtils;

View File

@@ -13,7 +13,9 @@ install:
- yarn
build_script:
- ps: cd ElectronClient\app
- ps: cd Tools
- npm install
- ps: cd ..\ElectronClient\app
- ps: xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
- npm install
- yarn dist

BIN
docs/images/Katex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -218,15 +218,15 @@
<tbody>
<tr>
<td>Windows</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-Setup-0.10.43.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a></td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.59/Joplin-Setup-0.10.59.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a></td>
</tr>
<tr>
<td>macOS</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-0.10.43.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a></td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.59/Joplin-0.10.59.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a></td>
</tr>
<tr>
<td>Linux</td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-0.10.43-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a></td>
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.59/Joplin-0.10.59-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a></td>
</tr>
</tbody>
</table>
@@ -243,7 +243,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://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a></td>
<td>or <a href="https://github.com/laurent22/joplin/releases/download/android-v0.10.79/joplin-v0.10.79.apk">Download APK File</a></td>
<td>or <a href="https://github.com/laurent22/joplin/releases/download/android-v0.10.86/joplin-v0.10.86.apk">Download APK File</a></td>
</tr>
<tr>
<td>iOS</td>
@@ -305,6 +305,9 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
: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="webdav-synchronisation">WebDAV synchronisation</h2>
<p><strong>Important: This is a beta feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain. If you wish to you use it, it is recommended that you keep a backup of your data. The simplest way is to regularly backup the profile directory of the desktop or terminal application.</strong></p>
<p>Select the &quot;WebDAV&quot; synchronisation target and follow the same instructions as for Nextcloud above.</p>
<h2 id="onedrive-synchronisation">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 &quot;OneDrive&quot; as the synchronisation target in the config screen (it is selected by default). Then, to initiate the synchronisation process, click on the &quot;Synchronise&quot; button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive).</p>
@@ -325,17 +328,139 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
<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="markdown">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="math-notation">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>`{.katex}EXPRESSION` </code>, eg. <code>`{.katex}\sqrt{3x-1}+(1+x)^2` </code>. To create an expression block, wrap it as follow:</p>
<pre><code>```katex
EXPRESSION
```
</code></pre><p>For example:</p>
<pre><code>```katex
f(x) = \int_{-\infty}^\infty
\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://raw.githubusercontent.com/laurent22/joplin/master/docs/images/Katex.png" style="max-width: 100%; max-height: 35em;"></p>
<h2 id="checkboxes">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>
<h1 id="contributing">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>
<p>Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian, Dutch and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:</p>
<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>
<li><a href="https://raw.githubusercontent.com/laurent22/joplin/master/CliClient/locales/joplin.pot">Download the file to be translated</a>.</li>
<li>In Poedit, open this .pot file, go into the Catalog menu and click Configuration. Change &quot;Country&quot; and &quot;Language&quot; to your own country and language.</li>
<li>From then you can translate the file. Once it is done, please either <a href="https://github.com/laurent22/joplin/pulls">open a pull request</a> or send the file to <a href="https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png">this address</a>.</li>
</ul>
<p>To <strong>update a translation</strong>, follow the same steps as above but instead of getting the .pot file, get the .po file for your language from there: <a href="https://github.com/laurent22/joplin/tree/master/CliClient/locales">https://github.com/laurent22/joplin/tree/master/CliClient/locales</a></p>
<p>This translation will apply to the three applications - desktop, mobile and terminal.</p>
<h1 id="contributing">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>
<p>Current translations:</p>
<!-- LOCALE-TABLE-AUTO-GENERATED -->
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>Language</th>
<th>Code</th>
<th>Last translator</th>
<th>Percent done</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png" alt=""></td>
<td>Croatian</td>
<td>hr_HR</td>
<td>Hrvoje Mandić <a href="&#109;&#97;&#x69;&#x6c;&#116;&#111;&#x3a;&#x74;&#114;&#98;&#117;&#104;&#x6f;&#x6d;&#x40;&#110;&#101;&#x74;&#46;&#104;&#114;">&#x74;&#114;&#98;&#117;&#104;&#x6f;&#x6d;&#x40;&#110;&#101;&#x74;&#46;&#104;&#114;</a></td>
<td>72%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png" alt=""></td>
<td>Deutsch</td>
<td>de_DE</td>
<td>Tobias Strobel <a href="&#109;&#97;&#105;&#108;&#x74;&#111;&#58;&#x67;&#x69;&#x74;&#64;&#x73;&#x74;&#x72;&#x6f;&#x62;&#x65;&#108;&#x74;&#111;&#98;&#x69;&#97;&#x73;&#x2e;&#x64;&#x65;">&#x67;&#x69;&#x74;&#64;&#x73;&#x74;&#x72;&#x6f;&#x62;&#x65;&#108;&#x74;&#111;&#98;&#x69;&#97;&#x73;&#x2e;&#x64;&#x65;</a></td>
<td>92%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/gb.png" alt=""></td>
<td>English</td>
<td>en_GB</td>
<td></td>
<td>100%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png" alt=""></td>
<td>Español</td>
<td>es_ES</td>
<td>Lucas Vieites</td>
<td>80%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cr.png" alt=""></td>
<td>Español (Costa Rica)</td>
<td>es_CR</td>
<td></td>
<td>68%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/fr.png" alt=""></td>
<td>Français</td>
<td>fr_FR</td>
<td>Laurent Cozic</td>
<td>100%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/it.png" alt=""></td>
<td>Italiano</td>
<td>it_IT</td>
<td></td>
<td>76%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/be.png" alt=""></td>
<td>Nederlands</td>
<td>nl_BE</td>
<td></td>
<td>90%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png" alt=""></td>
<td>Português (Brasil)</td>
<td>pt_BR</td>
<td></td>
<td>74%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png" alt=""></td>
<td>Русский</td>
<td>ru_RU</td>
<td>Artyom Karlov <a href="&#x6d;&#97;&#105;&#x6c;&#116;&#x6f;&#58;&#x61;&#114;&#116;&#x79;&#111;&#109;&#46;&#107;&#x61;&#114;&#x6c;&#x6f;&#118;&#64;&#x67;&#109;&#x61;&#105;&#x6c;&#x2e;&#x63;&#x6f;&#x6d;">&#x61;&#114;&#116;&#x79;&#111;&#109;&#46;&#107;&#x61;&#114;&#x6c;&#x6f;&#118;&#64;&#x67;&#109;&#x61;&#105;&#x6c;&#x2e;&#x63;&#x6f;&#x6d;</a></td>
<td>96%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png" alt=""></td>
<td>中文 (简体)</td>
<td>zh_CN</td>
<td>RCJacH <a href="&#x6d;&#97;&#x69;&#108;&#116;&#111;&#58;&#x52;&#67;&#x4a;&#97;&#x63;&#72;&#x40;&#x6f;&#x75;&#116;&#108;&#111;&#x6f;&#107;&#x2e;&#x63;&#111;&#109;">&#x52;&#67;&#x4a;&#97;&#x63;&#72;&#x40;&#x6f;&#x75;&#116;&#108;&#111;&#x6f;&#107;&#x2e;&#x63;&#111;&#109;</a></td>
<td>76%</td>
</tr>
<tr>
<td><img src="https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/jp.png" alt=""></td>
<td>日本語</td>
<td>ja_JP</td>
<td></td>
<td>74%</td>
</tr>
</tbody>
</table>
<!-- LOCALE-TABLE-AUTO-GENERATED -->
<h1 id="coming-features">Coming features</h1>
<ul>
<li>Mobile: manage tags</li>

View File

@@ -305,6 +305,8 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/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 - 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>
<p>Select the &quot;WebDAV&quot; synchronisation target and follow the same instructions as for Nextcloud above.</p>
<h2 id="onedrive-synchronisation">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>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). 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>