You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-05 20:56:22 +02:00
Compare commits
29 Commits
ios-v10.0.
...
android-v1
Author | SHA1 | Date | |
---|---|---|---|
|
6e143aef5c | ||
|
bf16aa6192 | ||
|
d96c58d192 | ||
|
e7e0264411 | ||
|
430a11282b | ||
|
9957b2798c | ||
|
2c5b0010bf | ||
|
1e3c6ed98c | ||
|
484f290eb0 | ||
|
06ad539941 | ||
|
5b84e80ac4 | ||
|
ca0f349348 | ||
|
d79089aea3 | ||
|
03611ad5ca | ||
|
c78c1cd3cf | ||
|
55afa7b5b7 | ||
|
a6c407b62b | ||
|
21897a3cd4 | ||
|
5796dd2098 | ||
|
d050071437 | ||
|
eaf8510f49 | ||
|
6ee2595dce | ||
|
0ecf2d6d9a | ||
|
50fd075168 | ||
|
6fa76bb83a | ||
|
b175c1fc94 | ||
|
b461625518 | ||
|
3819897ba1 | ||
|
6a031857ba |
@@ -16,10 +16,12 @@ File bugs in the [Github Issue Tracker](https://github.com/laurent22/joplin/issu
|
||||
|
||||
Please check that your request has not already been posted in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). If it has, **up-voting the issue** increases the chances it'll be noticed and implemented in the future. "+1" comments are not tracked.
|
||||
|
||||
As a general rule, suggestions to _improve Joplin_ should be posted first in the [Joplin Forum](https://discourse.joplinapp.org/) for discussion.
|
||||
As a general rule, suggestions to *improve Joplin* should be posted first in the [Joplin Forum](https://discourse.joplinapp.org/) for discussion.
|
||||
|
||||
Avoid listing multiple requests in one report in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). One issue per request makes it easier to track and discuss it.
|
||||
|
||||
Finally, when submitting a pull request, don't forget to [test your code](#unit-tests).
|
||||
|
||||
# Contribute to the project
|
||||
|
||||
## Contributing to Joplin's translation
|
||||
@@ -42,3 +44,20 @@ There are only two rules, but not following them means the pull request will not
|
||||
|
||||
- **Please use tabs, NOT spaces.**
|
||||
- **Please do not add or remove optional characters, such as spaces or colons.** Please setup your editor so that it only changes what you are working on and is not making automated changes elsewhere. The reason for this is that small white space changes make diff hard to read and can cause needless conflicts.
|
||||
|
||||
## Unit tests
|
||||
|
||||
When submitting a pull request for a new feature or bug fix, please add unit tests for your code. Unit testing GUI changes is not always possible so it is not required, but any change in a file under /lib for example should be unit tested.
|
||||
|
||||
The tests are under CliClient/tests. To get them running, you first need to build the CLI app:
|
||||
|
||||
cd CliClient
|
||||
npm i
|
||||
|
||||
Then to run all the test units:
|
||||
|
||||
./run_test.sh
|
||||
|
||||
To run just one particular file:
|
||||
|
||||
./run_test.sh markdownUtils # Don't add the .js extension
|
@@ -1439,9 +1439,9 @@ msgid ""
|
||||
"the attachments are downloaded whether you open the note or not."
|
||||
msgstr ""
|
||||
"En mode \"manuel\", les ressources sont téléchargées uniquement lorsque vous "
|
||||
"cliquez dessus. En mode \"auto\", elle sont téléchargée lorsque vous ouvrez "
|
||||
"la note. En mode \"toujours\", toutes les ressources sont téléchargées, que "
|
||||
"vous ayez ouvert la note ou pas."
|
||||
"cliquez dessus. En mode \"auto\", elles sont téléchargées lorsque vous "
|
||||
"ouvrez la note. En mode \"toujours\", toutes les ressources sont "
|
||||
"téléchargées, que vous ayez ouvert la note ou pas."
|
||||
|
||||
msgid "Always"
|
||||
msgstr "Toujours"
|
||||
@@ -1963,7 +1963,7 @@ msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Links with protocol \"%s\" are not supported"
|
||||
msgstr ""
|
||||
msgstr "Le schéma d'URI \"%s\" n'est pas supporté"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported image type: %s"
|
||||
|
@@ -735,7 +735,7 @@ msgid "Make a donation"
|
||||
msgstr "Fai una donazione"
|
||||
|
||||
msgid "Toggle development tools"
|
||||
msgstr ""
|
||||
msgstr "Attiva / disattiva strumenti di sviluppo"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Open %s"
|
||||
@@ -755,7 +755,7 @@ msgstr "La versione attuale è aggiornata."
|
||||
|
||||
#, javascript-format
|
||||
msgid "%s (pre-release)"
|
||||
msgstr ""
|
||||
msgstr "%s (pre-rilascio)"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr "È disponibile un aggiornamento, vuoi scaricarlo ora?"
|
||||
@@ -991,9 +991,8 @@ msgstr "Alcuni elementi non possono essere sincronizzati."
|
||||
msgid "View them now"
|
||||
msgstr "Mostrali ora"
|
||||
|
||||
#, fuzzy
|
||||
msgid "One or more master keys need a password."
|
||||
msgstr "Inserisci password principale:"
|
||||
msgstr "Una o più chiavi master necessitano di una password."
|
||||
|
||||
msgid "Set the password"
|
||||
msgstr "Imposta la password"
|
||||
@@ -1185,18 +1184,19 @@ msgstr "Taccuini"
|
||||
msgid "Decrypting items: %d/%d"
|
||||
msgstr "Decrittografia Elementi: %d/%d"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Fetching resources: %d/%d"
|
||||
msgstr "Risorse: %d/%d"
|
||||
msgstr "Recupero risorse: %d/%d"
|
||||
|
||||
msgid "Please select where the sync status should be exported to"
|
||||
msgstr ""
|
||||
"Prego selezionare dove lo stato della sincronizzazione deve essere esportato"
|
||||
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
msgstr "Riprova"
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Aggiungi o rimuovi etichetta"
|
||||
msgstr "Aggiungi o rimuovi etichette"
|
||||
|
||||
msgid "Duplicate"
|
||||
msgstr "Duplicare"
|
||||
@@ -1229,9 +1229,11 @@ msgid ""
|
||||
"Type a note title to jump to it. Or type # followed by a tag name, or @ "
|
||||
"followed by a notebook name."
|
||||
msgstr ""
|
||||
"Scrivi il titolo di una nota per saltare ad essa. Oppure digita # seguito "
|
||||
"dal nome di una etichetta, oppure @ seguito dal nome di un taccuino."
|
||||
|
||||
msgid "Goto Anything..."
|
||||
msgstr ""
|
||||
msgstr "Vai a..."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Usage: %s"
|
||||
@@ -1348,6 +1350,8 @@ msgstr "La sincronizzazione è già in corso. Stato: %s"
|
||||
msgid ""
|
||||
"Unknown item type downloaded - please upgrade Joplin to the latest version"
|
||||
msgstr ""
|
||||
"Tipo elemento scaricato sconosciuto - prego aggiornare Joplin all’ultima "
|
||||
"versione"
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr "Crittografato"
|
||||
@@ -1391,6 +1395,9 @@ msgid ""
|
||||
"to it before syncing, otherwise all files will be removed! See the FAQ for "
|
||||
"more details: %s"
|
||||
msgstr ""
|
||||
"Attenzione: se si cambia questa posizione, accertarsi di copiare tutto il "
|
||||
"contenuto prima di sincronizzare, altrimenti tutti i file saranno rimossi! "
|
||||
"Vedi le FAQ per maggiori dettagli: %s"
|
||||
|
||||
msgid "Synchronisation target"
|
||||
msgstr "Destinazione di sincronizzazione"
|
||||
@@ -1424,13 +1431,16 @@ msgid "WebDAV password"
|
||||
msgstr "Password WebDAV"
|
||||
|
||||
msgid "Attachment download behaviour"
|
||||
msgstr ""
|
||||
msgstr "Comportamento scaricamento allegati"
|
||||
|
||||
msgid ""
|
||||
"In \"Manual\" mode, attachments are downloaded only when you click on them. "
|
||||
"In \"Auto\", they are downloaded when you open the note. In \"Always\", all "
|
||||
"the attachments are downloaded whether you open the note or not."
|
||||
msgstr ""
|
||||
"In modalità \"Manuale\", gli allegati sono scaricati solo quando si clicca su "
|
||||
"di essi. In \"Auto\" sono scaricati quando si apre la nota. In \"Sempre\" tutti "
|
||||
"gli allegati sono scaricati sia che si apra o no la nota."
|
||||
|
||||
msgid "Always"
|
||||
msgstr "Sempre"
|
||||
@@ -1442,7 +1452,7 @@ msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
msgid "Max concurrent connections"
|
||||
msgstr ""
|
||||
msgstr "Massimo numero di connessioni concorrenti"
|
||||
|
||||
msgid "Language"
|
||||
msgstr "Linguaggio"
|
||||
@@ -1499,7 +1509,7 @@ msgid "Enable math expressions"
|
||||
msgstr "Attiva espressioni matematiche"
|
||||
|
||||
msgid "Enable ==mark== syntax"
|
||||
msgstr ""
|
||||
msgstr "Attiva sintassi ==mark=="
|
||||
|
||||
msgid "Enable footnotes"
|
||||
msgstr "Attiva note a piè pagina"
|
||||
@@ -1508,10 +1518,10 @@ msgid "Enable table of contents extension"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable ~sub~ syntax"
|
||||
msgstr ""
|
||||
msgstr "Attiva sintassi ~sub~"
|
||||
|
||||
msgid "Enable ^sup^ syntax"
|
||||
msgstr ""
|
||||
msgstr "Attiva sintassi ^sup^"
|
||||
|
||||
msgid "Enable deflist syntax"
|
||||
msgstr ""
|
||||
@@ -1523,7 +1533,7 @@ msgid "Enable markdown emoji"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable ++insert++ syntax"
|
||||
msgstr ""
|
||||
msgstr "Attiva sintassi ++insert++"
|
||||
|
||||
msgid "Enable multimarkdown table extension"
|
||||
msgstr ""
|
||||
@@ -1544,7 +1554,7 @@ msgstr ""
|
||||
"costantemente le tue note e quindi ridurre il numero di conflitti."
|
||||
|
||||
msgid "Start application minimised in the tray icon"
|
||||
msgstr ""
|
||||
msgstr "Avvia applicazione minimizzata nell’icona del vassoio"
|
||||
|
||||
msgid "Global zoom percentage"
|
||||
msgstr "Percentuale di zoom globale"
|
||||
@@ -1567,11 +1577,11 @@ msgid "Automatically update the application"
|
||||
msgstr "Aggiorna automaticamente l'applicazione"
|
||||
|
||||
msgid "Get pre-releases when checking for updates"
|
||||
msgstr ""
|
||||
msgstr "Ottieni pre-rilasci durante controllo aggiornamenti"
|
||||
|
||||
#, javascript-format
|
||||
msgid "See the pre-release page for more details: %s"
|
||||
msgstr ""
|
||||
msgstr "Vedi la pagina di pre-rilascio per maggiori dettagli: %s"
|
||||
|
||||
msgid "Synchronisation interval"
|
||||
msgstr "Intervallo di sincronizzazione"
|
||||
@@ -1631,7 +1641,7 @@ msgid "%d days"
|
||||
msgstr "%d giorni"
|
||||
|
||||
msgid "Keep note history for"
|
||||
msgstr ""
|
||||
msgstr "Mantieni cronologia nota per"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
@@ -1650,7 +1660,7 @@ msgid "Note"
|
||||
msgstr "Nota"
|
||||
|
||||
msgid "Plugins"
|
||||
msgstr ""
|
||||
msgstr "Plugins"
|
||||
|
||||
msgid "Application"
|
||||
msgstr "Applicazione"
|
||||
@@ -1725,15 +1735,17 @@ msgstr "%s (%s) non può essere caricato: %s"
|
||||
msgid "Item \"%s\" could not be downloaded: %s"
|
||||
msgstr "Elemento \"%s\" non può essere scaricato: %s"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Items that cannot be decrypted"
|
||||
msgstr "Elementi che non possono essere sincronizzati"
|
||||
msgstr "Elementi che non possono essere decriptati"
|
||||
|
||||
msgid ""
|
||||
"Joplin failed to decrypt these items multiple times, possibly because they "
|
||||
"are corrupted or too large. These items will remain on the device but Joplin "
|
||||
"will no longer attempt to decrypt them."
|
||||
msgstr ""
|
||||
"Joplin ha fallito la decriptazione di questi elementi più volte, forse "
|
||||
"perché sono corrotti o troppo grandi. Questi elementi rimarranno sul "
|
||||
"dispositivo, ma Joplin non proverà più a decriptarli."
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr "Stato di sincronizzazione (Elementi sincronizzati / Elementi totali)"
|
||||
@@ -1828,6 +1840,9 @@ msgid ""
|
||||
"Error. Please check that URL, username, password, etc. are correct and that "
|
||||
"the sync target is accessible. The reported error was:"
|
||||
msgstr ""
|
||||
"Errore. Prego controllare che URL, nome utente, password, etc. siano corretti "
|
||||
"e che la destinazione di sincronizzazione sia accessibile. L’errore "
|
||||
"riportato era:"
|
||||
|
||||
msgid "The application has been authorised!"
|
||||
msgstr "L'applicazione è stata autorizzata con successo!"
|
||||
@@ -1840,6 +1855,11 @@ msgid ""
|
||||
"\n"
|
||||
"Please try again."
|
||||
msgstr ""
|
||||
"Non è stato possibile autorizzare l’applicazione:\n"
|
||||
"\n"
|
||||
"%s\n"
|
||||
"\n"
|
||||
"Riprovare prego."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Decrypted items: %s / %s"
|
||||
@@ -1851,9 +1871,8 @@ msgstr "Nuovi tag:"
|
||||
msgid "Type new tags or select from list"
|
||||
msgstr "Digita nuovi tag o seleziona dalla lista"
|
||||
|
||||
#, fuzzy
|
||||
msgid "More information"
|
||||
msgstr "Configurazione"
|
||||
msgstr "Maggiori informazioni"
|
||||
|
||||
msgid ""
|
||||
"To work correctly, the app needs the following permissions. Please enable "
|
||||
@@ -1946,7 +1965,7 @@ msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Links with protocol \"%s\" are not supported"
|
||||
msgstr ""
|
||||
msgstr "Collegamenti con protocollo \"%s\" non sono supportati"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported image type: %s"
|
||||
|
@@ -14,6 +14,8 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.2.1\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
msgstr "移除相关笔记的标签后才可删除此标签。"
|
||||
@@ -22,7 +24,7 @@ msgid "Please select the note or notebook to be deleted first."
|
||||
msgstr "请先选择需要删除的笔记或笔记本。"
|
||||
|
||||
msgid "Press Ctrl+D or type \"exit\" to exit the application"
|
||||
msgstr "按 Ctrl+D 或输入 \"exit\" 退出程序"
|
||||
msgstr "按 Ctrl+D 或输入“exit”退出程序"
|
||||
|
||||
#, javascript-format
|
||||
msgid "More than one item match \"%s\". Please narrow down your query."
|
||||
@@ -55,7 +57,7 @@ msgstr "不存在该命令:%s"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr "命令 \"%s\" 仅在GUI模式下可用"
|
||||
msgstr "命令“%s”仅在 GUI 模式下可用"
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr "无法更改加密项目"
|
||||
@@ -80,7 +82,7 @@ msgstr "将选定文件添加到笔记中。"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Cannot find \"%s\"."
|
||||
msgstr "无法找到 \"%s\"。"
|
||||
msgstr "无法找到“%s”。"
|
||||
|
||||
msgid "Displays the given note."
|
||||
msgstr "显示选定笔记。"
|
||||
@@ -119,7 +121,7 @@ msgstr "标记待办事项为完成。"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr "笔记非待办事项:\"%s\""
|
||||
msgstr "笔记非待办事项:“%s”"
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
@@ -237,7 +239,7 @@ msgid ""
|
||||
msgstr "通过方向键与 page up/down 键来滚动列表与文本区域(包含此控制台)。"
|
||||
|
||||
msgid "To maximise/minimise the console, press \"tc\"."
|
||||
msgstr "按 \"TC\" 最大化/最小化控制台。"
|
||||
msgstr "按“TC”最大化/最小化控制台。"
|
||||
|
||||
msgid "To enter command line mode, press \":\""
|
||||
msgstr "按“:”键进入命令行模式"
|
||||
@@ -314,7 +316,7 @@ msgstr ""
|
||||
"待办事项。"
|
||||
|
||||
msgid "Either \"text\" or \"json\""
|
||||
msgstr "\"text\" 或 \"json\""
|
||||
msgstr "“text”或“json”"
|
||||
|
||||
msgid ""
|
||||
"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, "
|
||||
@@ -418,8 +420,8 @@ msgid ""
|
||||
"taking place, you may delete the lock file at \"%s\" and resume the "
|
||||
"operation."
|
||||
msgstr ""
|
||||
"锁定文件已被保存。如果您确认当前未在进行任何同步,可删除锁定文件 \"%s\" 后继"
|
||||
"续上一部操作。"
|
||||
"锁定文件已被保存。如果您确认当前未在进行任何同步,可删除锁定文件“%s”后继续上"
|
||||
"一部操作。"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Synchronisation target: %s (%s)"
|
||||
@@ -442,13 +444,13 @@ msgid ""
|
||||
"[tag] from [note], or to list the notes associated with [tag]. The command "
|
||||
"`tag list` can be used to list all the tags (use -l for long option)."
|
||||
msgstr ""
|
||||
"<tag-command> 可以是 \"add\"、\"remove\" 或者 \"list\", 用于从 [note] 中赋值"
|
||||
"或删除 [tag],或者列出与 [tag] 相关的笔记。`tag list` 命令可以用于列出所有的"
|
||||
"标签(对于过长选项请使用 -l 参数)。"
|
||||
"<tag-command> 可以是“add”、“remove”或者“list”, 用于从 [note] 中赋值或删除 "
|
||||
"[tag],或者列出与 [tag] 相关的笔记。`tag list` 命令可以用于列出所有的标签(对"
|
||||
"于过长选项请使用 -l 参数)。"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid command: \"%s\""
|
||||
msgstr "无效命令:\"%s\""
|
||||
msgstr "无效命令:“%s”"
|
||||
|
||||
msgid ""
|
||||
"<todo-command> can either be \"toggle\" or \"clear\". Use \"toggle\" to "
|
||||
@@ -456,9 +458,9 @@ msgid ""
|
||||
"target is a regular note it will be converted to a to-do). Use \"clear\" to "
|
||||
"convert the to-do back to a regular note."
|
||||
msgstr ""
|
||||
"<todo-command> 可以是 \"toggle\" 或者 \"clear\"。使用 \"toggle\" 命令来切换待"
|
||||
"办事项的完成状态(若目标为普通笔记则将会转换成待办事项)。使用 \"clear\" 命令"
|
||||
"来把待办事项转换到普通笔记。"
|
||||
"<todo-command> 可以是“toggle”或者“clear”。使用“toggle”命令来切换待办事项的完"
|
||||
"成状态(若目标为普通笔记则将会转换成待办事项)。使用“clear”命令来把待办事项转"
|
||||
"换到普通笔记。"
|
||||
|
||||
msgid "Marks a to-do as non-completed."
|
||||
msgstr "标记待办事项为未完成。"
|
||||
@@ -513,9 +515,9 @@ msgid ""
|
||||
"any files outside this directory nor to any other personal data. No data "
|
||||
"will be shared with any third party."
|
||||
msgstr ""
|
||||
"请在浏览器中打开以下链接激活该应用程序。该应用会建立 \"Apps/Joplin\" 文件目"
|
||||
"录,并只会读写该目录中的文件。它没有任何权限访问此目录以外的任何文件或个人信"
|
||||
"息。也不会与第三方分享任何数据。"
|
||||
"请在浏览器中打开以下链接激活该应用程序。该应用会建立“Apps/Joplin”文件目录,并"
|
||||
"只会读写该目录中的文件。它没有任何权限访问此目录以外的任何文件或个人信息。也"
|
||||
"不会与第三方分享任何数据。"
|
||||
|
||||
msgid "Search:"
|
||||
msgstr "搜索:"
|
||||
@@ -545,7 +547,7 @@ msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
|
||||
msgstr "从 \"%s\" 导出,导出格式为 \"%s\"。请稍等…"
|
||||
msgstr "从“%s”导出,导出格式为“%s”。请稍等……"
|
||||
|
||||
msgid "Sidebar"
|
||||
msgstr "边栏"
|
||||
@@ -561,7 +563,7 @@ msgstr "笔记正文"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
|
||||
msgstr "从 \"%s\" 导入,导入格式为 \"%s\" 。请稍等…"
|
||||
msgstr "从“%s”导入,导入格式为“%s”。请稍等……"
|
||||
|
||||
msgid "PDF File"
|
||||
msgstr "PDF 文件"
|
||||
@@ -692,7 +694,7 @@ msgid "Make a donation"
|
||||
msgstr "捐赠"
|
||||
|
||||
msgid "Toggle development tools"
|
||||
msgstr ""
|
||||
msgstr "切换开发者工具"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Open %s"
|
||||
@@ -899,7 +901,7 @@ msgstr "返回"
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
msgstr "将新建的笔记本“%s”,并将文件 \"%s\" 导入其中"
|
||||
msgstr "将新建的笔记本“%s”,并将文件“%s”导入其中"
|
||||
|
||||
msgid "Please create a notebook first."
|
||||
msgstr "请先创建笔记本。"
|
||||
@@ -934,19 +936,18 @@ msgstr "一些项目无法被同步。"
|
||||
msgid "View them now"
|
||||
msgstr "立刻查看"
|
||||
|
||||
#, fuzzy
|
||||
msgid "One or more master keys need a password."
|
||||
msgstr "输入主密码:"
|
||||
msgstr "一个或多个主密钥需要密码。"
|
||||
|
||||
msgid "Set the password"
|
||||
msgstr "设置密码"
|
||||
|
||||
msgid "No notes in here. Create one by clicking on \"New note\"."
|
||||
msgstr "此处没有任何笔记。点击\"新建笔记\"创建。"
|
||||
msgstr "此处没有任何笔记。点击“新建笔记”创建。"
|
||||
|
||||
msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "此处没有任何笔记本。点击\"新建笔记本\"创建。"
|
||||
msgstr "此处没有任何笔记本。点击“新建笔记本”创建。"
|
||||
|
||||
msgid "Location"
|
||||
msgstr "位置"
|
||||
@@ -965,7 +966,7 @@ msgstr "笔记属性"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
|
||||
msgstr "笔记\"%s\"已成功恢复到笔记本\"%s\"中。"
|
||||
msgstr "笔记“%s”已成功恢复到笔记本“%s”中。"
|
||||
|
||||
msgid "This note has no history"
|
||||
msgstr "此笔记没有历史记录"
|
||||
@@ -978,8 +979,8 @@ msgid ""
|
||||
"Click \"%s\" to restore the note. It will be copied in the notebook named "
|
||||
"\"%s\". The current version of the note will not be replaced or modified."
|
||||
msgstr ""
|
||||
"单击 \"%s\" 以恢复笔记。它将会被复制到名为 \"%s\" 的笔记本中。笔记的当前版本"
|
||||
"不会被替换或修改。"
|
||||
"单击“%s”以恢复笔记。它将会被复制到名为“%s”的笔记本中。笔记的当前版本不会被替"
|
||||
"换或修改。"
|
||||
|
||||
msgid "Open..."
|
||||
msgstr "打开…"
|
||||
@@ -1131,7 +1132,7 @@ msgid "Please select where the sync status should be exported to"
|
||||
msgstr "请选择同步状态的导出位置"
|
||||
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
msgstr "重试"
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "添加或删除标签"
|
||||
@@ -1306,7 +1307,7 @@ msgstr "无法移动笔记本到该位置"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notebooks cannot be named \"%s\", which is a reserved title."
|
||||
msgstr "笔记本无法被命名为 \"%s\",这个标题被留作他用。"
|
||||
msgstr "笔记本无法被命名为“%s”,这个标题被留作他用。"
|
||||
|
||||
msgid "created date"
|
||||
msgstr "创建日期"
|
||||
@@ -1362,25 +1363,27 @@ msgid "WebDAV password"
|
||||
msgstr "WebDAV 密码"
|
||||
|
||||
msgid "Attachment download behaviour"
|
||||
msgstr ""
|
||||
msgstr "附件下载行为"
|
||||
|
||||
msgid ""
|
||||
"In \"Manual\" mode, attachments are downloaded only when you click on them. "
|
||||
"In \"Auto\", they are downloaded when you open the note. In \"Always\", all "
|
||||
"the attachments are downloaded whether you open the note or not."
|
||||
msgstr ""
|
||||
"在“手动”模式下,只有单击附件时才会下载它们。在“自动”中,当你打开笔记时,它们"
|
||||
"就会被下载下来。在“总是”中,无论你是否打开笔记,所有的附件都会被下载。"
|
||||
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
msgstr "总是"
|
||||
|
||||
msgid "Manual"
|
||||
msgstr ""
|
||||
msgstr "手动"
|
||||
|
||||
msgid "Auto"
|
||||
msgstr ""
|
||||
msgstr "自动"
|
||||
|
||||
msgid "Max concurrent connections"
|
||||
msgstr ""
|
||||
msgstr "最大并发连接数"
|
||||
|
||||
msgid "Language"
|
||||
msgstr "语言"
|
||||
@@ -1564,11 +1567,11 @@ msgid "%d days"
|
||||
msgstr ""
|
||||
|
||||
msgid "Keep note history for"
|
||||
msgstr ""
|
||||
msgstr "保留笔记历史记录"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "无效的选项值:\"%s\"。可用值有:%s。"
|
||||
msgstr "无效的选项值:“%s”。可用值有:%s。"
|
||||
|
||||
msgid "General"
|
||||
msgstr "通用选项"
|
||||
@@ -1590,7 +1593,7 @@ msgstr "应用程序"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The tag \"%s\" already exists. Please choose a different name."
|
||||
msgstr "标签 \"%s\" 已存在。请选择一个不同的名称。"
|
||||
msgstr "标签“%s”已存在。请选择一个不同的名称。"
|
||||
|
||||
msgid "Joplin Export File"
|
||||
msgstr "Joplin 导出文件"
|
||||
@@ -1615,7 +1618,7 @@ msgstr "文件目录"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Cannot load \"%s\" module for format \"%s\""
|
||||
msgstr "无法加载 \"%s\" 模块用于读取 \"%s\" 格式"
|
||||
msgstr "无法加载“%s”模块用于读取“%s”格式"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Please specify import format for %s"
|
||||
@@ -1625,7 +1628,7 @@ msgstr "请指定 %s 的导入格式"
|
||||
msgid ""
|
||||
"This item is currently encrypted: %s \"%s\". Please wait for all items to be "
|
||||
"decrypted and try again."
|
||||
msgstr "该项目当前已加密:%s \"%s\"。请等待所有项目解密后再重试。"
|
||||
msgstr "该项目当前已加密:%s“%s”。请等待所有项目解密后再重试。"
|
||||
|
||||
msgid "There is no data to export."
|
||||
msgstr "没有可导出的数据。"
|
||||
@@ -1653,17 +1656,18 @@ msgstr "%s (%s) 无法上传到:%s"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Item \"%s\" could not be downloaded: %s"
|
||||
msgstr "项目 \"%s\" 无法从 %s 中下载"
|
||||
msgstr "项目“%s”无法从 %s 中下载"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Items that cannot be decrypted"
|
||||
msgstr "无法同步项目"
|
||||
msgstr "无法解密的项目"
|
||||
|
||||
msgid ""
|
||||
"Joplin failed to decrypt these items multiple times, possibly because they "
|
||||
"are corrupted or too large. These items will remain on the device but Joplin "
|
||||
"will no longer attempt to decrypt them."
|
||||
msgstr ""
|
||||
"Joplin 多次解密这些项目均已失败,可能是它们太大或已经损坏导致的。这些项目会保"
|
||||
"留再设备上,但 Joplin 不会再尝试对它们进行解密。"
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr "同步状态(已同步项目/项目总数)"
|
||||
@@ -1787,9 +1791,8 @@ msgstr "新建标签:"
|
||||
msgid "Type new tags or select from list"
|
||||
msgstr "输入新的标签或从列表中选择"
|
||||
|
||||
#, fuzzy
|
||||
msgid "More information"
|
||||
msgstr "配置"
|
||||
msgstr "更多信息"
|
||||
|
||||
msgid ""
|
||||
"To work correctly, the app needs the following permissions. Please enable "
|
||||
@@ -1875,7 +1878,7 @@ msgstr "Joplin 手机应用目前不支持这种类型的链接:%s"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Links with protocol \"%s\" are not supported"
|
||||
msgstr ""
|
||||
msgstr "不支持“%s”协议链接"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported image type: %s"
|
||||
|
62
CliClient/package-lock.json
generated
62
CliClient/package-lock.json
generated
@@ -165,6 +165,11 @@
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
@@ -453,6 +458,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
|
||||
"integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"source-map": "^0.6.1",
|
||||
"source-map-resolve": "^0.5.2",
|
||||
"urix": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"cssom": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz",
|
||||
@@ -517,6 +540,11 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"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",
|
||||
@@ -1437,10 +1465,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"joplin-turndown": {
|
||||
"version": "4.0.12",
|
||||
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.12.tgz",
|
||||
"integrity": "sha512-HlxkcIiNFSMLBvYktoXqLLHFGuwQYlcPclo0Peeatw3cPe6iFqSsEgEGY/0bYM/fubA/zpPULrJcjST99BO9wQ==",
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.15.tgz",
|
||||
"integrity": "sha512-68ukx19XFbKtJ5hfPfPX6IDLFZ1+NI+CpxJZyDEXAN5rPkyGXDw9xnEfo1IYRd+fq56upjo5Fn7J1hTCQTVTIA==",
|
||||
"requires": {
|
||||
"css": "^2.2.4",
|
||||
"html-entities": "^1.2.1",
|
||||
"jsdom": "^11.9.0"
|
||||
}
|
||||
@@ -2441,6 +2470,11 @@
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||
},
|
||||
"resolve-url": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
|
||||
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo="
|
||||
},
|
||||
"retry": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
|
||||
@@ -2598,6 +2632,23 @@
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
},
|
||||
"source-map-resolve": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
|
||||
"integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
|
||||
"requires": {
|
||||
"atob": "^2.1.1",
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"resolve-url": "^0.2.1",
|
||||
"source-map-url": "^0.4.0",
|
||||
"urix": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"source-map-url": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
|
||||
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM="
|
||||
},
|
||||
"split-skip": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/split-skip/-/split-skip-0.0.2.tgz",
|
||||
@@ -3114,6 +3165,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"urix": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
|
||||
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz",
|
||||
|
@@ -43,7 +43,7 @@
|
||||
"html-minifier": "^3.5.15",
|
||||
"image-data-uri": "^2.0.0",
|
||||
"image-type": "^3.0.0",
|
||||
"joplin-turndown": "^4.0.12",
|
||||
"joplin-turndown": "^4.0.15",
|
||||
"joplin-turndown-plugin-gfm": "^1.0.8",
|
||||
"jssha": "^2.3.0",
|
||||
"levenshtein": "^1.0.5",
|
||||
|
@@ -35,7 +35,7 @@ describe('EnexToMd', function() {
|
||||
const htmlPath = basePath + '/' + htmlFilename;
|
||||
const mdPath = basePath + '/' + filename(htmlFilename) + '.md';
|
||||
|
||||
// if (htmlFilename !== 'list5.html') continue;
|
||||
// if (htmlFilename !== 'multiline_inner_text.html') continue;
|
||||
|
||||
const html = await shim.fsDriver().readFile(htmlPath);
|
||||
let expectedMd = await shim.fsDriver().readFile(mdPath);
|
||||
|
7
CliClient/tests/enex_to_md/multiline_inner_text.html
Normal file
7
CliClient/tests/enex_to_md/multiline_inner_text.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div>Sometimes Evernote
|
||||
wraps lines inside blocks</div>
|
||||
<div>Sometimes it doesn't wrap them</div>
|
||||
<pre>But
|
||||
careful
|
||||
with
|
||||
pre tags</pre>
|
6
CliClient/tests/enex_to_md/multiline_inner_text.md
Normal file
6
CliClient/tests/enex_to_md/multiline_inner_text.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Sometimes Evernote wraps lines inside blocks
|
||||
Sometimes it doesn't wrap them
|
||||
But
|
||||
careful
|
||||
with
|
||||
pre tags
|
@@ -1,5 +1,7 @@
|
||||
def ma_fonction():
|
||||
"""
|
||||
C'est une super fonction
|
||||
"""
|
||||
pass
|
||||
```
|
||||
def ma_fonction():
|
||||
"""
|
||||
C'est une super fonction
|
||||
"""
|
||||
pass
|
||||
```
|
2
CliClient/tests/html_to_md/code_2.html
Normal file
2
CliClient/tests/html_to_md/code_2.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<pre style="font-family: Menlo, Monaco, Consolas, "Courier New", monospace;"><strong><font color="#008080">thatsCode();</font></strong></pre>
|
||||
<pre>thatsJustPre(); // In that case we do not have enough info to know if it is a codeblock or not, so we leave it as plain text</pre>
|
5
CliClient/tests/html_to_md/code_2.md
Normal file
5
CliClient/tests/html_to_md/code_2.md
Normal file
@@ -0,0 +1,5 @@
|
||||
```
|
||||
thatsCode();
|
||||
```
|
||||
|
||||
thatsJustPre(); // In that case we do not have enough info to know if it is a codeblock or not, so we leave it as plain text
|
@@ -6,4 +6,6 @@ Some text, not an image, so it should remain escaped:
|
||||
|
||||
But this is code so it can be unescaped:
|
||||
|
||||
<img src="http://test.com/image.png" />
|
||||
```
|
||||
<img src="http://test.com/image.png" />
|
||||
```
|
@@ -156,6 +156,25 @@ describe('services_rest_Api', function() {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should preserve user timestamps when creating notes', async (done) => {
|
||||
let response = null;
|
||||
const f = await Folder.save({ title: "mon carnet" });
|
||||
|
||||
const updatedTime = Date.now() - 1000;
|
||||
const createdTime = Date.now() - 10000;
|
||||
|
||||
response = await api.route('POST', 'notes', null, JSON.stringify({
|
||||
parent_id: f.id,
|
||||
user_updated_time: updatedTime,
|
||||
user_created_time: createdTime,
|
||||
}));
|
||||
|
||||
expect(response.user_updated_time).toBe(updatedTime);
|
||||
expect(response.user_created_time).toBe(createdTime);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create notes with supplied ID', async (done) => {
|
||||
let response = null;
|
||||
const f = await Folder.save({ title: "mon carnet" });
|
||||
|
@@ -87,20 +87,31 @@
|
||||
}
|
||||
|
||||
// Cleans up element by removing all its invisible children (which we don't want to render as Markdown)
|
||||
// And hard-code the image dimensions so that the information can be used by the clipper server to
|
||||
// display them at the right sizes in the notes.
|
||||
function cleanUpElement(element, imageSizes) {
|
||||
const childNodes = element.childNodes;
|
||||
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
for (let i = childNodes.length - 1; i >= 0; i--) {
|
||||
const node = childNodes[i];
|
||||
const nodeName = node.nodeName.toLowerCase();
|
||||
|
||||
let isVisible = node.nodeType === 1 ? window.getComputedStyle(node).display !== 'none' : true;
|
||||
if (isVisible && ['input', 'textarea', 'script', 'noscript', 'style', 'select', 'option', 'button'].indexOf(node.nodeName.toLowerCase()) >= 0) isVisible = false;
|
||||
const isHidden = node && node.classList && node.classList.contains('joplin-clipper-hidden');
|
||||
|
||||
if (!isVisible) {
|
||||
if (isHidden) {
|
||||
element.removeChild(node);
|
||||
} else {
|
||||
|
||||
if (node.nodeName.toLowerCase() === 'img') {
|
||||
// If the data-joplin-clipper-value has been set earlier, create a new DIV element
|
||||
// to replace the input or text area, so that it can be exported.
|
||||
if (node.getAttribute && node.getAttribute('data-joplin-clipper-value')) {
|
||||
const div = document.createElement('div');
|
||||
div.innerText = node.getAttribute('data-joplin-clipper-value');
|
||||
node.parentNode.insertBefore(div, node.nextSibling);
|
||||
element.removeChild(node);
|
||||
}
|
||||
|
||||
if (nodeName === 'img') {
|
||||
node.src = absoluteUrl(node.src);
|
||||
const imageSize = imageSizes[node.src];
|
||||
if (imageSize) {
|
||||
@@ -114,6 +125,70 @@
|
||||
}
|
||||
}
|
||||
|
||||
// When we clone the document before cleaning it, we lose some of the information that might have been set via CSS or
|
||||
// JavaScript, in particular whether an element was hidden or not. This function pre-process the document by
|
||||
// adding a "joplin-clipper-hidden" class to all currently hidden elements in the current document.
|
||||
// This class is then used in cleanUpElement() on the cloned document to find an element should be visible or not.
|
||||
function preProcessDocument(element) {
|
||||
const childNodes = element.childNodes;
|
||||
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const node = childNodes[i];
|
||||
const nodeName = node.nodeName.toLowerCase();
|
||||
|
||||
let isVisible = node.nodeType === 1 ? window.getComputedStyle(node).display !== 'none' : true;
|
||||
if (isVisible && ['script', 'noscript', 'style', 'select', 'option', 'button'].indexOf(nodeName) >= 0) isVisible = false;
|
||||
|
||||
// If it's a text input or a textarea and it has a value, save
|
||||
// that value to data-joplin-clipper-value. This is then used
|
||||
// when cleaning up the document to export the value.
|
||||
if (['input', 'textarea'].indexOf(nodeName) >= 0) {
|
||||
isVisible = !!node.value;
|
||||
if (nodeName === 'input' && node.getAttribute('type') !== 'text') isVisible = false;
|
||||
if (isVisible) node.setAttribute('data-joplin-clipper-value', node.value);
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
node.classList.add('joplin-clipper-hidden');
|
||||
} else {
|
||||
preProcessDocument(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This sets the PRE elements computed style to the style attribute, so that
|
||||
// the info can be exported and later processed by the htmlToMd converter
|
||||
// to detect code blocks.
|
||||
function hardcodePreStyles(doc) {
|
||||
const preElements = doc.getElementsByTagName('pre');
|
||||
|
||||
for (const preElement of preElements) {
|
||||
const fontFamily = getComputedStyle(preElement).getPropertyValue('font-family');
|
||||
const fontFamilyArray = fontFamily.split(',').map(f => f.toLowerCase().trim());
|
||||
if (fontFamilyArray.indexOf('monospace') >= 0) {
|
||||
preElement.style.fontFamily = fontFamily;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Given a document, return a <style> tag that contains all the styles
|
||||
// required to render the page. Not currently used but could be as an
|
||||
// option to clip pages as HTML.
|
||||
function getStyleTag(doc) {
|
||||
const styleText = [];
|
||||
for (var i=0; i<doc.styleSheets.length; i++) {
|
||||
try {
|
||||
var sheet = doc.styleSheets[i];
|
||||
for (const cssRule of sheet.cssRules) {
|
||||
styleText.push(cssRule.cssText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
return '<style>' + styleText.join('\n') + '</style>';
|
||||
}
|
||||
|
||||
function documentForReadability() {
|
||||
// Readability directly change the passed document so clone it so as
|
||||
// to preserve the original web page.
|
||||
@@ -180,6 +255,10 @@
|
||||
|
||||
} else if (command.name === "completePageHtml") {
|
||||
|
||||
hardcodePreStyles(document);
|
||||
preProcessDocument(document);
|
||||
// Because cleanUpElement is going to modify the DOM and remove elements we don't want to work
|
||||
// directly on the document, so we make a copy of it first.
|
||||
const cleanDocument = document.body.cloneNode(true);
|
||||
const imageSizes = getImageSizes(document, true);
|
||||
cleanUpElement(cleanDocument, imageSizes);
|
||||
@@ -187,10 +266,14 @@
|
||||
|
||||
} else if (command.name === "selectedHtml") {
|
||||
|
||||
const range = window.getSelection().getRangeAt(0);
|
||||
const container = document.createElement('div');
|
||||
container.appendChild(range.cloneContents());
|
||||
return clippedContentResponse(pageTitle(), container.innerHTML, getImageSizes(document), getAnchorNames(document));
|
||||
hardcodePreStyles(document);
|
||||
preProcessDocument(document);
|
||||
const range = window.getSelection().getRangeAt(0);
|
||||
const container = document.createElement('div');
|
||||
container.appendChild(range.cloneContents());
|
||||
const imageSizes = getImageSizes(document, true);
|
||||
cleanUpElement(container, imageSizes);
|
||||
return clippedContentResponse(pageTitle(), container.innerHTML, getImageSizes(document), getAnchorNames(document));
|
||||
|
||||
} else if (command.name === 'screenshot') {
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.17",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
|
@@ -239,6 +239,10 @@ class Application extends BaseApplication {
|
||||
Setting.setValue('sidebarVisibility', newState.sidebarVisibility);
|
||||
}
|
||||
|
||||
if (action.type.indexOf('NOTE_SELECT') === 0 || action.type.indexOf('FOLDER_SELECT') === 0) {
|
||||
this.updateMenuItemStates();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -618,20 +622,25 @@ class Application extends BaseApplication {
|
||||
|
||||
const rootMenus = {
|
||||
edit: {
|
||||
id: 'edit',
|
||||
label: _('&Edit'),
|
||||
submenu: [{
|
||||
id: 'edit:copy',
|
||||
label: _('Copy'),
|
||||
role: 'copy',
|
||||
accelerator: 'CommandOrControl+C',
|
||||
}, {
|
||||
id: 'edit:cut',
|
||||
label: _('Cut'),
|
||||
role: 'cut',
|
||||
accelerator: 'CommandOrControl+X',
|
||||
}, {
|
||||
id: 'edit:paste',
|
||||
label: _('Paste'),
|
||||
role: 'paste',
|
||||
accelerator: 'CommandOrControl+V',
|
||||
}, {
|
||||
id: 'edit:selectAll',
|
||||
label: _('Select all'),
|
||||
role: 'selectall',
|
||||
accelerator: 'CommandOrControl+A',
|
||||
@@ -639,6 +648,7 @@ class Application extends BaseApplication {
|
||||
type: 'separator',
|
||||
screens: ['Main'],
|
||||
}, {
|
||||
id: 'edit:bold',
|
||||
label: _('Bold'),
|
||||
screens: ['Main'],
|
||||
accelerator: 'CommandOrControl+B',
|
||||
@@ -649,6 +659,7 @@ class Application extends BaseApplication {
|
||||
});
|
||||
},
|
||||
}, {
|
||||
id: 'edit:italic',
|
||||
label: _('Italic'),
|
||||
screens: ['Main'],
|
||||
accelerator: 'CommandOrControl+I',
|
||||
@@ -659,6 +670,7 @@ class Application extends BaseApplication {
|
||||
});
|
||||
},
|
||||
}, {
|
||||
id: 'edit:link',
|
||||
label: _('Link'),
|
||||
screens: ['Main'],
|
||||
accelerator: 'CommandOrControl+K',
|
||||
@@ -669,6 +681,7 @@ class Application extends BaseApplication {
|
||||
});
|
||||
},
|
||||
}, {
|
||||
id: 'edit:code',
|
||||
label: _('Code'),
|
||||
screens: ['Main'],
|
||||
accelerator: 'CommandOrControl+`',
|
||||
@@ -682,6 +695,7 @@ class Application extends BaseApplication {
|
||||
type: 'separator',
|
||||
screens: ['Main'],
|
||||
}, {
|
||||
id: 'edit:insertDateTime',
|
||||
label: _('Insert Date Time'),
|
||||
screens: ['Main'],
|
||||
accelerator: 'CommandOrControl+Shift+T',
|
||||
@@ -695,6 +709,7 @@ class Application extends BaseApplication {
|
||||
type: 'separator',
|
||||
screens: ['Main'],
|
||||
}, {
|
||||
id: 'edit:commandStartExternalEditing',
|
||||
label: _('Edit in external editor'),
|
||||
screens: ['Main'],
|
||||
accelerator: 'CommandOrControl+E',
|
||||
@@ -705,29 +720,36 @@ class Application extends BaseApplication {
|
||||
});
|
||||
},
|
||||
}, {
|
||||
id: 'edit:setTags',
|
||||
label: _('Tags'),
|
||||
screens: ['Main'],
|
||||
accelerator: 'CommandOrControl+Alt+T',
|
||||
click: () => {
|
||||
const selectedNoteIds = this.store().getState().selectedNoteIds;
|
||||
if (selectedNoteIds.length !== 1) return;
|
||||
|
||||
this.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'setTags',
|
||||
noteId: selectedNoteIds[0],
|
||||
});
|
||||
},
|
||||
}, {
|
||||
type: 'separator',
|
||||
screens: ['Main'],
|
||||
}, {
|
||||
id: 'edit:focusSearch',
|
||||
label: _('Search in all the notes'),
|
||||
screens: ['Main'],
|
||||
accelerator: shim.isMac() ? 'Shift+Command+F' : 'F6',
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'focus_search',
|
||||
name: 'focusSearch',
|
||||
});
|
||||
},
|
||||
}, {
|
||||
id: 'edit:showLocalSearch',
|
||||
label: _('Search in current note'),
|
||||
screens: ['Main'],
|
||||
accelerator: 'CommandOrControl+F',
|
||||
@@ -924,6 +946,19 @@ class Application extends BaseApplication {
|
||||
this.lastMenuScreen_ = screen;
|
||||
}
|
||||
|
||||
updateMenuItemStates() {
|
||||
if (!this.lastMenuScreen_) return;
|
||||
if (!this.store()) return;
|
||||
|
||||
const selectedNoteIds = this.store().getState().selectedNoteIds;
|
||||
|
||||
for (const itemId of ['copy', 'paste', 'cut', 'selectAll', 'bold', 'italic', 'link', 'code', 'insertDateTime', 'commandStartExternalEditing', 'setTags', 'showLocalSearch']) {
|
||||
const menuItem = Menu.getApplicationMenu().getMenuItemById('edit:' + itemId);
|
||||
if (!menuItem) continue;
|
||||
menuItem.enabled = selectedNoteIds.length === 1;
|
||||
}
|
||||
}
|
||||
|
||||
updateTray() {
|
||||
const app = bridge().electronApp();
|
||||
|
||||
@@ -1092,6 +1127,8 @@ class Application extends BaseApplication {
|
||||
|
||||
RevisionService.instance().runInBackground();
|
||||
|
||||
this.updateMenuItemStates();
|
||||
|
||||
// Make it available to the console window - useful to call revisionService.collectRevisions()
|
||||
window.revisionService = RevisionService.instance();
|
||||
window.migrationService = MigrationService.instance();
|
||||
|
@@ -96,7 +96,7 @@ class HeaderComponent extends React.Component {
|
||||
|
||||
let commandProcessed = true;
|
||||
|
||||
if (command.name === 'focus_search' && this.searchElement_) {
|
||||
if (command.name === 'focusSearch' && this.searchElement_) {
|
||||
this.searchElement_.focus();
|
||||
} else {
|
||||
commandProcessed = false;
|
||||
|
@@ -954,6 +954,7 @@ class NoteTextComponent extends React.Component {
|
||||
postMessageSyntax: 'ipcProxySendToHost',
|
||||
userCss: options.useCustomCss ? this.props.customCss : '',
|
||||
resources: await shared.attachedResources(bodyToRender),
|
||||
codeHighlightCacheKey: this.state.note ? this.state.note.id : null,
|
||||
};
|
||||
|
||||
let bodyHtml = '';
|
||||
@@ -1794,6 +1795,7 @@ class NoteTextComponent extends React.Component {
|
||||
accuracy: 'partially',
|
||||
}]
|
||||
markerOptions.selectedIndex = this.state.localSearch.selectedIndex;
|
||||
markerOptions.separateWordSearch = false;
|
||||
} else {
|
||||
const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
|
||||
if (search) {
|
||||
@@ -1917,7 +1919,6 @@ const mapStateToProps = (state) => {
|
||||
itemType: state.selectedItemType,
|
||||
folders: state.folders,
|
||||
theme: state.settings.theme,
|
||||
showAdvancedOptions: state.settings.showAdvancedOptions,
|
||||
syncStarted: state.syncStarted,
|
||||
newNote: state.newNote,
|
||||
windowCommand: state.windowCommand,
|
||||
|
@@ -205,15 +205,19 @@
|
||||
elementIndex++;
|
||||
}
|
||||
|
||||
const markKeywordOptions = {
|
||||
each: onEachElement,
|
||||
};
|
||||
|
||||
if ('separateWordSearch' in options) markKeywordOptions.separateWordSearch = options.separateWordSearch;
|
||||
|
||||
for (let i = 0; i < keywords.length; i++) {
|
||||
let keyword = keywords[i];
|
||||
|
||||
markJsUtils.markKeyword(mark_, keyword, {
|
||||
pregQuote: pregQuote,
|
||||
replaceRegexDiacritics: replaceRegexDiacritics,
|
||||
}, {
|
||||
each: onEachElement,
|
||||
});
|
||||
}, markKeywordOptions);
|
||||
}
|
||||
|
||||
ipcProxySendToHost('setMarkerCount', elementIndex);
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -101,7 +101,7 @@
|
||||
"highlight.js": "^9.15.6",
|
||||
"html-entities": "^1.2.1",
|
||||
"image-type": "^3.0.0",
|
||||
"joplin-turndown": "^4.0.12",
|
||||
"joplin-turndown": "^4.0.15",
|
||||
"joplin-turndown-plugin-gfm": "^1.0.8",
|
||||
"jssha": "^2.3.1",
|
||||
"katex": "^0.10.0",
|
||||
|
@@ -71,7 +71,7 @@ if [[ ! -e ~/.joplin/VERSION ]] || [[ $(< ~/.joplin/VERSION) != "$version" ]]; t
|
||||
|
||||
# Create icon for Gnome
|
||||
echo 'Create Desktop icon.'
|
||||
if [[ $desktop =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.* ]]
|
||||
if [[ $desktop =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.*|.*unity.* ]]
|
||||
then
|
||||
: "${TMPDIR:=/tmp}"
|
||||
# This command extracts to squashfs-root by default and can't be changed...
|
||||
|
13
README.md
13
README.md
@@ -8,7 +8,7 @@ Notes exported from Evernote via .enex files [can be imported](#importing) into
|
||||
|
||||
The notes can be [synchronised](#synchronisation) with various cloud services including [Nextcloud](https://nextcloud.com/), Dropbox, OneDrive, WebDAV or the file system (for example with a network directory). When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
|
||||
|
||||
The application is available for Windows, Linux, macOS, Android and iOS (the terminal app also work on FreeBSD). A [Web Clipper](https://github.com/laurent22/joplin/blob/master/readme/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
|
||||
The application is available for Windows, Linux, macOS, Android and iOS (the terminal app also works on FreeBSD). A [Web Clipper](https://github.com/laurent22/joplin/blob/master/readme/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
|
||||
|
||||
<div class="top-screenshot"><img src="https://joplinapp.org/images/AllClients.jpg" style="max-width: 100%; max-height: 35em;"></div>
|
||||
|
||||
@@ -28,7 +28,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
-----------------|----------|----------------
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.276/joplin-v1.0.276.apk)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.277/joplin-v1.0.277.apk)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
@@ -84,6 +84,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
- Import Enex files (Evernote export format) and Markdown files.
|
||||
- Export JEX files (Joplin Export format) and raw files.
|
||||
- Support notes, to-dos, tags and notebooks.
|
||||
- Goto Anything feature.
|
||||
- Sort notes by multiple criteria - title, updated time, etc.
|
||||
- Support for alarms (notifications) in mobile and desktop applications.
|
||||
- Offline first, so the entire data is always available on the device even without an internet connection.
|
||||
@@ -329,6 +330,10 @@ Field restricted | Add either `title:` or `body:` before a note to restrict your
|
||||
|
||||
Notes are sorted by "relevance". Currently it means the notes that contain the requested terms the most times are on top. For queries with multiple terms, it also matter how close to each others are the terms. This is a bit experimental so if you notice a search query that returns unexpected results, please report it in the forum, providing as much details as possible to replicate the issue.
|
||||
|
||||
# Goto Anything
|
||||
|
||||
In the desktop application, press Ctrl+G or Cmd+G and type the title of a note to jump directly to it. You can also type `#` followed by a tag or `@` followed by a notebook title.
|
||||
|
||||
# Donations
|
||||
|
||||
Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.
|
||||
@@ -376,9 +381,9 @@ Current translations:
|
||||
 | English (UK) | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100%
|
||||
 | English (US) | [en_US](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_US.po) | | 100%
|
||||
 | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Andros Fenollosa (andros@fenollosa.email) | 97%
|
||||
 | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 99%
|
||||
 | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 100%
|
||||
 | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans (marcoslansgarza@gmail.com) | 65%
|
||||
 | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 92%
|
||||
 | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 98%
|
||||
 | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 51%
|
||||
 | Nederlands | [nl_NL](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_NL.po) | Heimen Stoffels (vistausss@outlook.com) | 79%
|
||||
 | Norwegian | [nb_NO](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nb_NO.po) | Mats Estensen (code@mxe.no) | 91%
|
||||
|
@@ -94,8 +94,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097512
|
||||
versionName "1.0.276"
|
||||
versionCode 2097513
|
||||
versionName "1.0.277"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ class HtmlToMd {
|
||||
const turndown = new TurndownService({
|
||||
headingStyle: 'atx',
|
||||
anchorNames: options.anchorNames ? options.anchorNames.map(n => n.trim().toLowerCase()) : [],
|
||||
codeBlockStyle: 'fenced',
|
||||
})
|
||||
turndown.use(turndownPluginGfm)
|
||||
turndown.remove('script');
|
||||
|
@@ -22,7 +22,7 @@ const hljs = require('highlight.js');
|
||||
const markdownItAnchor = require('markdown-it-anchor');
|
||||
// The keys must match the corresponding entry in Setting.js
|
||||
const plugins = {
|
||||
mark: {module: require('markdown-it-mark')},
|
||||
mark: {module: require('markdown-it-mark')},
|
||||
footnote: {module: require('markdown-it-footnote')},
|
||||
sub: {module: require('markdown-it-sub')},
|
||||
sup: {module: require('markdown-it-sup')},
|
||||
@@ -43,6 +43,9 @@ class MdToHtml {
|
||||
this.resourceBaseUrl_ = ('resourceBaseUrl' in options) ? options.resourceBaseUrl : null;
|
||||
|
||||
this.cachedOutputs_ = {};
|
||||
|
||||
this.lastCodeHighlightCacheKey_ = null;
|
||||
this.cachedHighlightedCode_ = {};
|
||||
}
|
||||
|
||||
render(body, style, options = null) {
|
||||
@@ -51,6 +54,15 @@ class MdToHtml {
|
||||
if (!options.paddingBottom) options.paddingBottom = '0';
|
||||
if (!options.highlightedKeywords) options.highlightedKeywords = [];
|
||||
|
||||
// The "codeHighlightCacheKey" option indicates what set of cached object should be
|
||||
// associated with this particular Markdown body. It is only used to allow us to
|
||||
// clear the cache whenever switching to a different note.
|
||||
// If "codeHighlightCacheKey" is not specified, code highlighting won't be cached.
|
||||
if (options.codeHighlightCacheKey !== this.lastCodeHighlightCacheKey_ || !options.codeHighlightCacheKey) {
|
||||
this.cachedHighlightedCode_ = {};
|
||||
this.lastCodeHighlightCacheKey_ = options.codeHighlightCacheKey;
|
||||
}
|
||||
|
||||
const breaks_ = Setting.value('markdown.softbreaks') ? false : true;
|
||||
|
||||
const cacheKey = md5(escape(body + JSON.stringify(options) + JSON.stringify(style)));
|
||||
@@ -67,14 +79,21 @@ class MdToHtml {
|
||||
breaks: breaks_,
|
||||
linkify: true,
|
||||
html: true,
|
||||
highlight: function(str, lang) {
|
||||
highlight: (str, lang) => {
|
||||
try {
|
||||
let hlCode = '';
|
||||
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
hlCode = hljs.highlight(lang, str, true).value;
|
||||
|
||||
const cacheKey = md5(str + '_' + lang);
|
||||
|
||||
if (options.codeHighlightCacheKey && this.cachedHighlightedCode_[cacheKey]) {
|
||||
hlCode = this.cachedHighlightedCode_[cacheKey];
|
||||
} else {
|
||||
hlCode = hljs.highlightAuto(str).value;
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
hlCode = hljs.highlight(lang, str, true).value;
|
||||
} else {
|
||||
hlCode = hljs.highlightAuto(str).value;
|
||||
}
|
||||
this.cachedHighlightedCode_[cacheKey] = hlCode;
|
||||
}
|
||||
|
||||
if (shim.isReactNative()) {
|
||||
@@ -124,15 +143,13 @@ class MdToHtml {
|
||||
markdownIt.use(rules.checkbox(context, ruleOptions));
|
||||
markdownIt.use(rules.link_open(context, ruleOptions));
|
||||
markdownIt.use(rules.html_image(context, ruleOptions));
|
||||
if (Setting.value('markdown.plugin.katex'))
|
||||
markdownIt.use(rules.katex(context, ruleOptions));
|
||||
if (Setting.value('markdown.plugin.katex')) markdownIt.use(rules.katex(context, ruleOptions));
|
||||
markdownIt.use(rules.highlight_keywords(context, ruleOptions));
|
||||
markdownIt.use(rules.code_inline(context, ruleOptions));
|
||||
markdownIt.use(markdownItAnchor)
|
||||
|
||||
for (let key in plugins) {
|
||||
if (Setting.value('markdown.plugin.' + key))
|
||||
markdownIt.use(plugins[key].module, plugins[key].options);
|
||||
if (Setting.value('markdown.plugin.' + key)) markdownIt.use(plugins[key].module, plugins[key].options);
|
||||
}
|
||||
|
||||
setupLinkify(markdownIt);
|
||||
|
@@ -52,14 +52,6 @@ class ActionButtonComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
newFolder_press() {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Folder',
|
||||
folderId: null,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let buttons = this.props.buttons ? this.props.buttons : [];
|
||||
|
||||
@@ -79,13 +71,6 @@ class ActionButtonComponent extends React.Component {
|
||||
icon: 'md-document',
|
||||
});
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
title: _('New notebook'),
|
||||
onPress: () => { this.newFolder_press() },
|
||||
color: '#3498db',
|
||||
icon: 'md-folder',
|
||||
});
|
||||
}
|
||||
|
||||
let buttonComps = [];
|
||||
|
@@ -4,7 +4,6 @@ const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image, Scrol
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const { BackButtonService } = require('lib/services/back-button.js');
|
||||
const NavService = require('lib/services/NavService.js');
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
const { Menu, MenuOptions, MenuOption, MenuTrigger } = require('react-native-popup-menu');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
@@ -191,44 +190,10 @@ class ScreenHeaderComponent extends Component {
|
||||
NavService.go('Status');
|
||||
}
|
||||
|
||||
config_press() {
|
||||
NavService.go('Config');
|
||||
}
|
||||
|
||||
encryptionConfig_press() {
|
||||
NavService.go('EncryptionConfig');
|
||||
}
|
||||
|
||||
warningBox_press() {
|
||||
NavService.go('EncryptionConfig');
|
||||
}
|
||||
|
||||
async debugReport_press() {
|
||||
const service = new ReportService();
|
||||
|
||||
const logItems = await reg.logger().lastEntries(null);
|
||||
const logItemRows = [
|
||||
['Date','Level','Message']
|
||||
];
|
||||
for (let i = 0; i < logItems.length; i++) {
|
||||
const item = logItems[i];
|
||||
logItemRows.push([
|
||||
time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss'),
|
||||
item.level,
|
||||
item.message
|
||||
]);
|
||||
}
|
||||
const logItemCsv = service.csvCreate(logItemRows);
|
||||
|
||||
const itemListCsv = await service.basicItemList({ format: 'csv' });
|
||||
const filePath = RNFS.ExternalDirectoryPath + '/syncReport-' + (new Date()).getTime() + '.txt';
|
||||
|
||||
const finalText = [logItemCsv, itemListCsv].join("\n================================================================================\n");
|
||||
|
||||
await RNFS.writeFile(filePath, finalText);
|
||||
alert('Debug report exported to ' + filePath);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
function sideMenuButton(styles, onPress) {
|
||||
@@ -312,42 +277,9 @@ class ScreenHeaderComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.showAdvancedOptions) {
|
||||
if (menuOptionComponents.length) {
|
||||
menuOptionComponents.push(<View key={'menuOption_showAdvancedOptions'} style={this.styles().divider}/>);
|
||||
}
|
||||
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.log_press()} key={'menuOption_log'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Log')}</Text>
|
||||
</MenuOption>);
|
||||
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.status_press()} key={'menuOption_status'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Status')}</Text>
|
||||
</MenuOption>);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.debugReport_press()} key={'menuOption_debugReport'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Export Debug Report')}</Text>
|
||||
</MenuOption>);
|
||||
}
|
||||
}
|
||||
|
||||
if (menuOptionComponents.length) {
|
||||
menuOptionComponents.push(<View key={'menuOption_' + key++} style={this.styles().divider}/>);
|
||||
}
|
||||
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.encryptionConfig_press()} key={'menuOption_encryptionConfig'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Encryption Config')}</Text>
|
||||
</MenuOption>);
|
||||
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.config_press()} key={'menuOption_config'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Configuration')}</Text>
|
||||
</MenuOption>);
|
||||
} else {
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.deleteButton_press()} key={'menuOption_delete'} style={this.styles().contextMenuItem}>
|
||||
@@ -453,7 +385,7 @@ class ScreenHeaderComponent extends Component {
|
||||
const sortButtonComp = this.props.sortButton_press ? sortButton(this.styles(), () => this.props.sortButton_press()) : null;
|
||||
const windowHeight = Dimensions.get('window').height - 50;
|
||||
|
||||
const menuComp = !showContextMenuButton ? null : (
|
||||
const menuComp = !menuOptionComponents.length || !showContextMenuButton ? null : (
|
||||
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
<MenuTrigger style={{ paddingTop: PADDING_V, paddingBottom: PADDING_V }}>
|
||||
<Icon name='md-more' style={this.styles().contextMenuTrigger} />
|
||||
@@ -497,7 +429,6 @@ const ScreenHeader = connect(
|
||||
locale: state.settings.locale,
|
||||
folders: state.folders,
|
||||
theme: state.settings.theme,
|
||||
showAdvancedOptions: state.settings.showAdvancedOptions,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
|
||||
|
@@ -10,7 +10,11 @@ const Setting = require('lib/models/Setting.js');
|
||||
const shared = require('lib/components/shared/config-shared.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const NavService = require('lib/services/NavService.js');
|
||||
const VersionInfo = require('react-native-version-info').default;
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
const { time } = require('lib/time-utils');
|
||||
const RNFS = require('react-native-fs');
|
||||
|
||||
import Slider from '@react-native-community/slider';
|
||||
|
||||
@@ -24,15 +28,59 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
super();
|
||||
this.styles_ = {};
|
||||
|
||||
this.state = {
|
||||
creatingReport: false,
|
||||
};
|
||||
|
||||
shared.init(this);
|
||||
|
||||
this.checkSyncConfig_ = async () => {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
}
|
||||
|
||||
this.e2eeConfig_ = () => {
|
||||
NavService.go('EncryptionConfig');
|
||||
}
|
||||
|
||||
this.saveButton_press = () => {
|
||||
return shared.saveSettings(this);
|
||||
};
|
||||
|
||||
this.syncStatusButtonPress_ = () => {
|
||||
NavService.go('Status');
|
||||
}
|
||||
|
||||
this.exportDebugButtonPress_ = async () => {
|
||||
this.setState({ creatingReport: true });
|
||||
const service = new ReportService();
|
||||
|
||||
const logItems = await reg.logger().lastEntries(null);
|
||||
const logItemRows = [
|
||||
['Date','Level','Message']
|
||||
];
|
||||
for (let i = 0; i < logItems.length; i++) {
|
||||
const item = logItems[i];
|
||||
logItemRows.push([
|
||||
time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss'),
|
||||
item.level,
|
||||
item.message
|
||||
]);
|
||||
}
|
||||
const logItemCsv = service.csvCreate(logItemRows);
|
||||
|
||||
const itemListCsv = await service.basicItemList({ format: 'csv' });
|
||||
const filePath = RNFS.ExternalDirectoryPath + '/syncReport-' + (new Date()).getTime() + '.txt';
|
||||
|
||||
const finalText = [logItemCsv, itemListCsv].join("\n================================================================================\n");
|
||||
|
||||
await RNFS.writeFile(filePath, finalText);
|
||||
alert('Debug report exported to ' + filePath);
|
||||
this.setState({ creatingReport: false });
|
||||
}
|
||||
|
||||
this.logButtonPress_ = () => {
|
||||
NavService.go('Log');
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
@@ -139,6 +187,21 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderButton(key, title, clickHandler, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
return (
|
||||
<View key={key} style={this.styles().settingContainer}>
|
||||
<View style={{flex:1, flexDirection: 'column'}}>
|
||||
<View style={{flex:1}}>
|
||||
<Button title={title} onPress={clickHandler} disabled={!!options.disabled}/>
|
||||
</View>
|
||||
{ options.statusComp }
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
sectionToComponent(key, section, settings) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const settingComps = [];
|
||||
@@ -157,15 +220,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
{messages.length >= 1 ? (<View style={{marginTop:10}}><Text style={this.styles().descriptionText}>{messages[1]}</Text></View>) : null}
|
||||
</View>);
|
||||
|
||||
settingComps.push(
|
||||
<View key="check_sync_config_button" style={this.styles().settingContainer}>
|
||||
<View style={{flex:1, flexDirection: 'column'}}>
|
||||
<View style={{flex:1}}>
|
||||
<Button title={_('Check synchronisation configuration')} onPress={this.checkSyncConfig_}/>
|
||||
</View>
|
||||
{ statusComp }
|
||||
</View>
|
||||
</View>);
|
||||
settingComps.push(this.renderButton('check_sync_config_button', _('Check synchronisation configuration'), this.checkSyncConfig_, { statusComp: statusComp }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +228,10 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
settingComps.push(settingComp);
|
||||
}
|
||||
|
||||
if (section.name === 'sync') {
|
||||
settingComps.push(this.renderButton('e2ee_config_button', _('Encryption Config') + ' ▶', this.e2eeConfig_));
|
||||
}
|
||||
|
||||
const headerWrapperStyle = this.styles().headerWrapperStyle;
|
||||
|
||||
return (
|
||||
@@ -273,6 +332,12 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
|
||||
const settingComps = shared.settingsToComponents2(this, 'mobile', settings);
|
||||
|
||||
settingComps.push(this.renderHeader('moreInfo', _('Tools')));
|
||||
|
||||
settingComps.push(this.renderButton('status_button', _('Sync Status') + ' ▶', this.syncStatusButtonPress_));
|
||||
settingComps.push(this.renderButton('log_button', _('Log') + ' ▶', this.logButtonPress_));
|
||||
settingComps.push(this.renderButton('export_report_button', this.state.creatingReport ? _('Creating report...') : _('Export Debug Report'), this.exportDebugButtonPress_, { disabled: this.state.creatingReport }));
|
||||
|
||||
settingComps.push(this.renderHeader('moreInfo', _('More information')));
|
||||
|
||||
if (Platform.OS === 'android' && Platform.Version >= 23) {
|
||||
|
@@ -580,7 +580,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
output.push({ title: isTodo ? _('Convert to note') : _('Convert to todo'), onPress: () => { this.toggleIsTodo_onPress(); } });
|
||||
if (isSaved) output.push({ title: _('Copy Markdown link'), onPress: () => { this.copyMarkdownLink_onPress(); } });
|
||||
output.push({ isDivider: true });
|
||||
if (this.props.showAdvancedOptions) output.push({ title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } });
|
||||
output.push({ title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } });
|
||||
output.push({ title: _('View on map'), onPress: () => { this.showOnMap_onPress(); } });
|
||||
if (hasSource) output.push({ title: _('Go to source URL'), onPress: () => { this.showSource_onPress(); } });
|
||||
output.push({ isDivider: true });
|
||||
@@ -805,7 +805,6 @@ const NoteScreen = connect(
|
||||
theme: state.settings.theme,
|
||||
ftsEnabled: state.settings['db.ftsEnabled'],
|
||||
sharedData: state.sharedData,
|
||||
showAdvancedOptions: state.settings.showAdvancedOptions,
|
||||
};
|
||||
}
|
||||
)(NoteScreenComponent)
|
||||
|
@@ -155,8 +155,8 @@ class NotesScreenComponent extends BaseScreenComponent {
|
||||
if (!folder) return [];
|
||||
|
||||
let output = [];
|
||||
if (!folder.encryption_applied) output.push({ title: _('Edit notebook'), onPress: () => { this.editFolder_onPress(this.props.selectedFolderId); } });
|
||||
output.push({ title: _('Delete notebook'), onPress: () => { this.deleteFolder_onPress(this.props.selectedFolderId); } });
|
||||
// if (!folder.encryption_applied) output.push({ title: _('Edit notebook'), onPress: () => { this.editFolder_onPress(this.props.selectedFolderId); } });
|
||||
// output.push({ title: _('Delete notebook'), onPress: () => { this.deleteFolder_onPress(this.props.selectedFolderId); } });
|
||||
|
||||
return output;
|
||||
} else {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { TouchableOpacity , Button, Text, Image, StyleSheet, ScrollView, View } = require('react-native');
|
||||
const { TouchableOpacity , Button, Text, Image, StyleSheet, ScrollView, View, Alert } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
@@ -8,21 +8,27 @@ const Folder = require('lib/models/Folder.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const NavService = require('lib/services/NavService.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { globalStyle, themeStyle } = require('lib/components/global-style.js');
|
||||
const shared = require('lib/components/shared/side-menu-shared.js');
|
||||
const { ActionButton } = require('lib/components/action-button.js');
|
||||
|
||||
class SideMenuContentComponent extends Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { syncReportText: '',
|
||||
//width: 0,
|
||||
this.state = {
|
||||
syncReportText: '',
|
||||
};
|
||||
this.styles_ = {};
|
||||
|
||||
this.tagButton_press = this.tagButton_press.bind(this);
|
||||
this.newFolderButton_press = this.newFolderButton_press.bind(this);
|
||||
this.synchronize_press = this.synchronize_press.bind(this);
|
||||
this.configButton_press = this.configButton_press.bind(this);
|
||||
this.renderFolderItem = this.renderFolderItem.bind(this);
|
||||
}
|
||||
|
||||
styles() {
|
||||
@@ -55,6 +61,11 @@ class SideMenuContentComponent extends Component {
|
||||
paddingRight: theme.marginRight,
|
||||
color: theme.colorFaded,
|
||||
fontSize: theme.fontSizeSmaller,
|
||||
flex: 0,
|
||||
},
|
||||
sidebarIcon: {
|
||||
fontSize: 22,
|
||||
color: theme.color,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -67,7 +78,7 @@ class SideMenuContentComponent extends Component {
|
||||
styles.folderIcon.color = theme.colorFaded;//'#0072d5';
|
||||
styles.folderIcon.paddingTop = 3;
|
||||
|
||||
styles.sideButton = Object.assign({}, styles.button);
|
||||
styles.sideButton = Object.assign({}, styles.button, { flex: 0 });
|
||||
styles.sideButtonText = Object.assign({}, styles.buttonText);
|
||||
|
||||
this.styles_[this.props.theme] = StyleSheet.create(styles);
|
||||
@@ -84,6 +95,59 @@ class SideMenuContentComponent extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
async folder_longPress(folder) {
|
||||
const buttons = [];
|
||||
|
||||
Alert.alert(
|
||||
'',
|
||||
_('Notebook: %s', folder.title), [
|
||||
{
|
||||
text: _('Rename'),
|
||||
onPress: () => {
|
||||
if (folder.encryption_applied) {
|
||||
alert(_('Encrypted notebooks cannot be renamed'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Folder',
|
||||
folderId: folder.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
text: _('Delete'),
|
||||
onPress: () => {
|
||||
Alert.alert('', _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', folder.title), [
|
||||
{
|
||||
text: _('OK'),
|
||||
onPress: () => {
|
||||
Folder.delete(folder.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: _('Cancel'),
|
||||
onPress: () => {},
|
||||
style: 'cancel',
|
||||
},
|
||||
]);
|
||||
},
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: _('Cancel'),
|
||||
onPress: () => {},
|
||||
style: 'cancel',
|
||||
}
|
||||
], {
|
||||
cancelable: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
folder_togglePress(folder) {
|
||||
this.props.dispatch({
|
||||
type: 'FOLDER_TOGGLE',
|
||||
@@ -100,12 +164,27 @@ class SideMenuContentComponent extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
configButton_press() {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
NavService.go('Config');
|
||||
}
|
||||
|
||||
newFolderButton_press() {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Folder',
|
||||
folderId: null,
|
||||
});
|
||||
}
|
||||
|
||||
async synchronize_press() {
|
||||
const actionDone = await shared.synchronize_press(this);
|
||||
if (actionDone === 'auth') this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
}
|
||||
|
||||
folderItem(folder, selected, hasChildren, depth) {
|
||||
renderFolderItem(folder, selected, hasChildren, depth) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const folderButtonStyle = {
|
||||
@@ -133,7 +212,7 @@ class SideMenuContentComponent extends Component {
|
||||
|
||||
return (
|
||||
<View key={folder.id} style={{ flex: 1, flexDirection: 'row' }}>
|
||||
<TouchableOpacity style={{ flex: 1 }} onPress={() => { this.folder_press(folder) }}>
|
||||
<TouchableOpacity style={{ flex: 1 }} onPress={() => { this.folder_press(folder) }} onLongPress={() => { this.folder_longPress(folder) }}>
|
||||
<View style={folderButtonStyle}>
|
||||
<Text numberOfLines={1} style={this.styles().folderButtonText}>{Folder.displayTitle(folder)}</Text>
|
||||
</View>
|
||||
@@ -143,59 +222,37 @@ class SideMenuContentComponent extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
synchronizeButton(state) {
|
||||
renderSideBarButton(key, title, iconName, onPressHandler) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const title = state == 'sync' ? _('Synchronise') : _('Cancel synchronisation');
|
||||
const iconComp = state == 'sync' ? <Icon name='md-sync' style={theme.icon} /> : <Icon name='md-close' style={theme.icon} />;
|
||||
|
||||
return (
|
||||
<TouchableOpacity key={'synchronize_button'} onPress={() => { this.synchronize_press() }}>
|
||||
<TouchableOpacity key={key} onPress={onPressHandler}>
|
||||
<View style={this.styles().sideButton}>
|
||||
{ iconComp }
|
||||
<Icon name={iconName} style={this.styles().sidebarIcon} />
|
||||
<Text style={this.styles().sideButtonText}>{title}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
tagButton() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
return (
|
||||
<TouchableOpacity key={'tag_button'} onPress={this.tagButton_press}>
|
||||
<View style={this.styles().sideButton}>
|
||||
<Icon name='md-pricetag' style={theme.icon} />
|
||||
<Text style={this.styles().sideButtonText}>Tags</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
makeDivider(key) {
|
||||
return <View style={{ marginTop: 15, marginBottom: 15, flex: -1, borderBottomWidth: 1, borderBottomColor: globalStyle.dividerColor }} key={key}></View>
|
||||
}
|
||||
|
||||
render() {
|
||||
let items = [];
|
||||
|
||||
renderBottomPanel() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
// HACK: inner height of ScrollView doesn't appear to be calculated correctly when
|
||||
// using padding. So instead creating blank elements for padding bottom and top.
|
||||
items.push(<View style={{ height: globalStyle.marginTop }} key='bottom_top_hack'/>);
|
||||
|
||||
if (this.props.folders.length) {
|
||||
const result = shared.renderFolders(this.props, this.folderItem.bind(this));
|
||||
const folderItems = result.items;
|
||||
items = items.concat(folderItems);
|
||||
}
|
||||
const items = [];
|
||||
|
||||
items.push(this.makeDivider('divider_1'));
|
||||
|
||||
items.push(this.tagButton());
|
||||
|
||||
items.push(this.makeDivider('divider_tag_bottom'));
|
||||
items.push(this.renderSideBarButton('newFolder_button', _('New Notebook'), 'md-folder-open', this.newFolderButton_press));
|
||||
|
||||
items.push(this.renderSideBarButton('tag_button', _('Tags'), 'md-pricetag', this.tagButton_press));
|
||||
|
||||
items.push(this.renderSideBarButton('config_button', _('Configuration'), 'md-settings', this.configButton_press));
|
||||
|
||||
items.push(this.makeDivider('divider_2'));
|
||||
|
||||
let lines = Synchronizer.reportToLines(this.props.syncReport);
|
||||
const syncReportText = lines.join("\n");
|
||||
@@ -212,17 +269,39 @@ class SideMenuContentComponent extends Component {
|
||||
|
||||
let fullReport = [];
|
||||
if (syncReportText) fullReport.push(syncReportText);
|
||||
// if (fullReport.length) fullReport.push('');
|
||||
if (resourceFetcherText) fullReport.push(resourceFetcherText);
|
||||
if (decryptionReportText) fullReport.push(decryptionReportText);
|
||||
|
||||
while (fullReport.length < 12) fullReport.push(''); // Add blank lines so that height of report text is fixed and doesn't affect scrolling
|
||||
items.push(this.renderSideBarButton(
|
||||
'synchronize_button',
|
||||
!this.props.syncStarted ? _('Synchronise') : _('Cancel synchronisation'),
|
||||
!this.props.syncStarted ? 'md-sync' : 'md-close',
|
||||
this.synchronize_press
|
||||
));
|
||||
|
||||
items.push(this.synchronizeButton(this.props.syncStarted ? 'cancel' : 'sync'));
|
||||
if (fullReport.length) items.push(<Text key='sync_report' style={this.styles().syncStatus}>{fullReport.join('\n')}</Text>);
|
||||
|
||||
items.push(<Text key='sync_report' style={this.styles().syncStatus}>{fullReport.join('\n')}</Text>);
|
||||
return (
|
||||
<View style={{ flex: 0, flexDirection: 'column', paddingBottom: theme.marginBottom }}>
|
||||
{ items }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
items.push(<View style={{ height: globalStyle.marginBottom }} key='bottom_padding_hack'/>);
|
||||
render() {
|
||||
let items = [];
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
// HACK: inner height of ScrollView doesn't appear to be calculated correctly when
|
||||
// using padding. So instead creating blank elements for padding bottom and top.
|
||||
items.push(<View style={{ height: globalStyle.marginTop }} key='bottom_top_hack'/>);
|
||||
|
||||
if (this.props.folders.length) {
|
||||
const result = shared.renderFolders(this.props, this.renderFolderItem);
|
||||
const folderItems = result.items;
|
||||
items = items.concat(folderItems);
|
||||
}
|
||||
|
||||
let style = {
|
||||
flex:1,
|
||||
@@ -237,6 +316,7 @@ class SideMenuContentComponent extends Component {
|
||||
<ScrollView scrollsToTop={false} style={this.styles().menu}>
|
||||
{ items }
|
||||
</ScrollView>
|
||||
{ this.renderBottomPanel() }
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
@@ -430,8 +430,34 @@ function enexXmlToMdArray(stream, resources) {
|
||||
//reject(e);
|
||||
})
|
||||
|
||||
const unwrapInnerText = text => {
|
||||
const lines = text.split('\n');
|
||||
|
||||
let output = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const nextLine = i < lines.length - 1 ? lines[i+1] : '';
|
||||
|
||||
if (!line) {
|
||||
output += '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nextLine) {
|
||||
output += line + ' ';
|
||||
} else {
|
||||
output += line;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
saxStream.on('text', function(text) {
|
||||
if (['table', 'tr', 'tbody'].indexOf(section.type) >= 0) return;
|
||||
|
||||
text = !state.inPre ? unwrapInnerText(text) : text;
|
||||
section.lines = collapseWhiteSpaceAndAppend(section.lines, state, text);
|
||||
})
|
||||
|
||||
|
@@ -194,7 +194,6 @@ class Setting extends BaseModel {
|
||||
'tagHeaderIsExpanded': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
|
||||
'folderHeaderIsExpanded': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] },
|
||||
'editor': { value: '', type: Setting.TYPE_STRING, subType: 'file_path_and_args', public: true, appTypes: ['cli', 'desktop'], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
|
||||
'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') },
|
||||
|
||||
'net.customCertificates': { value: '', type: Setting.TYPE_STRING, section:'sync', show: (settings) => { return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0 }, public: true, appTypes: ['desktop', 'cli'], label: () => _('Custom TLS certificates'), description: () => _('Comma-separated list of paths to directories to load the certificates from, or path to individual cert files. For example: /my/cert_dir, /other/custom.pem. Note that if you make changes to the TLS settings, you must save your changes before clicking on "Check synchronisation configuration".') },
|
||||
'net.ignoreTlsErrors': { value: false, type: Setting.TYPE_BOOL, section:'sync', show: (settings) => { return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0 }, public: true, appTypes: ['desktop', 'cli'], label: () => _('Ignore TLS certificate errors') },
|
||||
|
@@ -102,7 +102,7 @@ class DecryptionWorker {
|
||||
if (!('errorHandler' in options)) options.errorHandler = 'log';
|
||||
|
||||
if (this.state_ !== 'idle') {
|
||||
this.logger().info('DecryptionWorker: cannot start because state is "' + this.state_ + '"');
|
||||
this.logger().debug('DecryptionWorker: cannot start because state is "' + this.state_ + '"');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -87,6 +87,14 @@ class ExternalEditWatcher {
|
||||
this.logger().error(error)
|
||||
}
|
||||
});
|
||||
// Hack to support external watcher on some linux applications (gedit, gvim, etc)
|
||||
// taken from https://github.com/paulmillr/chokidar/issues/591
|
||||
this.watcher_.on('raw', async (event, path, {watchedPath}) => {
|
||||
if (event === 'rename') {
|
||||
this.watcher_.unwatch(watchedPath);
|
||||
this.watcher_.add(watchedPath);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.watcher_.add(fileToWatch);
|
||||
}
|
||||
@@ -297,4 +305,4 @@ class ExternalEditWatcher {
|
||||
|
||||
}
|
||||
|
||||
module.exports = ExternalEditWatcher;
|
||||
module.exports = ExternalEditWatcher;
|
||||
|
@@ -82,7 +82,11 @@ class SearchEngine {
|
||||
if (this.scheduleSyncTablesIID_) return;
|
||||
|
||||
this.scheduleSyncTablesIID_ = setTimeout(async () => {
|
||||
await this.syncTables();
|
||||
try {
|
||||
await this.syncTables();
|
||||
} catch (error) {
|
||||
this.logger().error('SearchEngine::scheduleSyncTables: Error while syncing tables:', error);
|
||||
}
|
||||
this.scheduleSyncTablesIID_ = null;
|
||||
}, 10000);
|
||||
}
|
||||
@@ -107,44 +111,48 @@ class SearchEngine {
|
||||
|
||||
let lastChangeId = Setting.value('searchEngine.lastProcessedChangeId');
|
||||
|
||||
while (true) {
|
||||
const changes = await ItemChange.modelSelectAll(`
|
||||
SELECT id, item_id, type
|
||||
FROM item_changes
|
||||
WHERE item_type = ?
|
||||
AND id > ?
|
||||
ORDER BY id ASC
|
||||
LIMIT 100
|
||||
`, [BaseModel.TYPE_NOTE, lastChangeId]);
|
||||
try {
|
||||
while (true) {
|
||||
const changes = await ItemChange.modelSelectAll(`
|
||||
SELECT id, item_id, type
|
||||
FROM item_changes
|
||||
WHERE item_type = ?
|
||||
AND id > ?
|
||||
ORDER BY id ASC
|
||||
LIMIT 100
|
||||
`, [BaseModel.TYPE_NOTE, lastChangeId]);
|
||||
|
||||
if (!changes.length) break;
|
||||
if (!changes.length) break;
|
||||
|
||||
const noteIds = changes.map(a => a.item_id);
|
||||
const notes = await Note.modelSelectAll('SELECT id, title, body FROM notes WHERE id IN ("' + noteIds.join('","') + '") AND is_conflict = 0 AND encryption_applied = 0');
|
||||
const queries = [];
|
||||
const noteIds = changes.map(a => a.item_id);
|
||||
const notes = await Note.modelSelectAll('SELECT id, title, body FROM notes WHERE id IN ("' + noteIds.join('","') + '") AND is_conflict = 0 AND encryption_applied = 0');
|
||||
const queries = [];
|
||||
|
||||
for (let i = 0; i < changes.length; i++) {
|
||||
const change = changes[i];
|
||||
for (let i = 0; i < changes.length; i++) {
|
||||
const change = changes[i];
|
||||
|
||||
if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) {
|
||||
queries.push({ sql: 'DELETE FROM notes_normalized WHERE id = ?', params: [change.item_id] });
|
||||
const note = this.noteById_(notes, change.item_id);
|
||||
if (note) {
|
||||
const n = this.normalizeNote_(note);
|
||||
queries.push({ sql: 'INSERT INTO notes_normalized(id, title, body) VALUES (?, ?, ?)', params: [change.item_id, n.title, n.body] });
|
||||
if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) {
|
||||
queries.push({ sql: 'DELETE FROM notes_normalized WHERE id = ?', params: [change.item_id] });
|
||||
const note = this.noteById_(notes, change.item_id);
|
||||
if (note) {
|
||||
const n = this.normalizeNote_(note);
|
||||
queries.push({ sql: 'INSERT INTO notes_normalized(id, title, body) VALUES (?, ?, ?)', params: [change.item_id, n.title, n.body] });
|
||||
}
|
||||
} else if (change.type === ItemChange.TYPE_DELETE) {
|
||||
queries.push({ sql: 'DELETE FROM notes_normalized WHERE id = ?', params: [change.item_id] });
|
||||
} else {
|
||||
throw new Error('Invalid change type: ' + change.type);
|
||||
}
|
||||
} else if (change.type === ItemChange.TYPE_DELETE) {
|
||||
queries.push({ sql: 'DELETE FROM notes_normalized WHERE id = ?', params: [change.item_id] });
|
||||
} else {
|
||||
throw new Error('Invalid change type: ' + change.type);
|
||||
|
||||
lastChangeId = change.id;
|
||||
}
|
||||
|
||||
lastChangeId = change.id;
|
||||
await this.db().transactionExecBatch(queries);
|
||||
Setting.setValue('searchEngine.lastProcessedChangeId', lastChangeId);
|
||||
await Setting.saveAll();
|
||||
}
|
||||
|
||||
await this.db().transactionExecBatch(queries);
|
||||
Setting.setValue('searchEngine.lastProcessedChangeId', lastChangeId);
|
||||
await Setting.saveAll();
|
||||
} catch (error) {
|
||||
this.logger().error('SearchEngine: Error while processing changes:', error);
|
||||
}
|
||||
|
||||
await ItemChangeUtils.deleteProcessedChanges();
|
||||
|
@@ -1,4 +1,5 @@
|
||||
const { ltrimSlashes } = require('lib/path-utils.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Note = require('lib/models/Note');
|
||||
const Tag = require('lib/models/Tag');
|
||||
@@ -383,6 +384,11 @@ class Api {
|
||||
this.logger().info('Request (' + requestId + '): Saving note...');
|
||||
|
||||
const saveOptions = this.defaultSaveOptions_(note, 'POST');
|
||||
saveOptions.autoTimestamp = false; // No auto-timestamp because user may have provided them
|
||||
const timestamp = Date.now();
|
||||
note.updated_time = timestamp;
|
||||
note.created_time = timestamp;
|
||||
|
||||
note = await Note.save(note, saveOptions);
|
||||
|
||||
if (requestNote.tags) {
|
||||
@@ -432,6 +438,10 @@ class Api {
|
||||
baseUrl: requestNote.base_url ? requestNote.base_url : '',
|
||||
anchorNames: requestNote.anchor_names ? requestNote.anchor_names : [],
|
||||
});
|
||||
|
||||
// Note: to save the note as HTML, use the code below:
|
||||
// const minify = require('html-minifier').minify;
|
||||
// output.body = minify(requestNote.body_html, { collapseWhitespace: true });
|
||||
}
|
||||
|
||||
if (requestNote.parent_id) {
|
||||
@@ -442,8 +452,10 @@ class Api {
|
||||
output.parent_id = folder.id;
|
||||
}
|
||||
|
||||
if (requestNote.source_url) output.source_url = requestNote.source_url;
|
||||
if (requestNote.author) output.author = requestNote.author;
|
||||
if ('source_url' in requestNote) output.source_url = requestNote.source_url;
|
||||
if ('author' in requestNote) output.author = requestNote.author;
|
||||
if ('user_updated_time' in requestNote) output.user_updated_time = Database.formatValue(Database.TYPE_INT, requestNote.user_updated_time);
|
||||
if ('user_created_time' in requestNote) output.user_created_time = Database.formatValue(Database.TYPE_INT, requestNote.user_created_time);
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -498,7 +510,7 @@ class Api {
|
||||
|
||||
// If we could not find the file extension from the URL, try to get it
|
||||
// now based on the Content-Type header.
|
||||
if (!fileExt) imagePath = this.tryToGuessImageExtFromMimeType_(response, imagePath);
|
||||
if (!fileExt) imagePath = await this.tryToGuessImageExtFromMimeType_(response, imagePath);
|
||||
}
|
||||
return imagePath;
|
||||
} catch (error) {
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { AppState, Keyboard, NativeModules, BackHandler, Platform } = require('react-native');
|
||||
const { AppState, Keyboard, NativeModules, BackHandler, Platform, View, Animated } = require('react-native');
|
||||
const { SafeAreaView } = require('react-navigation');
|
||||
const { connect, Provider } = require('react-redux');
|
||||
const { BackButtonService } = require('lib/services/back-button.js');
|
||||
@@ -552,6 +552,11 @@ class AppComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
sideMenuContentOpacity: new Animated.Value(0),
|
||||
};
|
||||
|
||||
this.lastSyncStarted_ = defaultState.syncStarted;
|
||||
|
||||
this.backButtonHandler_ = () => {
|
||||
@@ -630,6 +635,15 @@ class AppComponent extends React.Component {
|
||||
AppState.removeEventListener('change', this.onAppStateChange_);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.showSideMenu !== prevProps.showSideMenu) {
|
||||
Animated.timing(this.state.sideMenuContentOpacity, {
|
||||
toValue: this.props.showSideMenu ? 0.5 : 0,
|
||||
duration: 600,
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
async backButtonHandler() {
|
||||
if (this.props.noteSelectionEnabled) {
|
||||
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
|
||||
@@ -704,6 +718,7 @@ class AppComponent extends React.Component {
|
||||
<AppNav screens={appNavInit} />
|
||||
</SafeAreaView>
|
||||
<DropdownAlert ref={ref => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
|
||||
<Animated.View pointerEvents='none' style={{position:'absolute', backgroundColor:'black', opacity: this.state.sideMenuContentOpacity, width: '100%', height: '100%'}}/>
|
||||
</MenuContext>
|
||||
</SideMenu>
|
||||
);
|
||||
|
@@ -273,7 +273,7 @@
|
||||
<p>Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in <a href="#markdown">Markdown format</a>.</p>
|
||||
<p>Notes exported from Evernote via .enex files <a href="#importing">can be imported</a> into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.</p>
|
||||
<p>The notes can be <a href="#synchronisation">synchronised</a> with various cloud services including <a href="https://nextcloud.com/">Nextcloud</a>, Dropbox, OneDrive, WebDAV or the file system (for example with a network directory). When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.</p>
|
||||
<p>The application is available for Windows, Linux, macOS, Android and iOS (the terminal app also work on FreeBSD). A <a href="https://github.com/laurent22/joplin/blob/master/readme/clipper.md">Web Clipper</a>, to save web pages and screenshots from your browser, is also available for <a href="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/">Firefox</a> and <a href="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB">Chrome</a>.</p>
|
||||
<p>The application is available for Windows, Linux, macOS, Android and iOS (the terminal app also works on FreeBSD). A <a href="https://github.com/laurent22/joplin/blob/master/readme/clipper.md">Web Clipper</a>, to save web pages and screenshots from your browser, is also available for <a href="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/">Firefox</a> and <a href="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB">Chrome</a>.</p>
|
||||
<div class="top-screenshot"><img src="https://joplinapp.org/images/AllClients.jpg" style="max-width: 100%; max-height: 35em;"></div>
|
||||
<h1><a name="installation" href="#installation" class="heading-anchor">🔗</a>Installation</h1>
|
||||
<p>Three types of applications are available: for the <strong>desktop</strong> (Windows, macOS and Linux), for <strong>mobile</strong> (Android and iOS) and for <strong>terminal</strong> (Windows, macOS, Linux and FreeBSD). All applications have similar user interfaces and can synchronise with each other.</p>
|
||||
@@ -317,7 +317,7 @@
|
||||
<tr>
|
||||
<td>Android</td>
|
||||
<td><a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a></td>
|
||||
<td>or <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.0.276/joplin-v1.0.276.apk">Download APK File</a></td>
|
||||
<td>or <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.0.277/joplin-v1.0.277.apk">Download APK File</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>iOS</td>
|
||||
@@ -363,6 +363,7 @@
|
||||
<li>Import Enex files (Evernote export format) and Markdown files.</li>
|
||||
<li>Export JEX files (Joplin Export format) and raw files.</li>
|
||||
<li>Support notes, to-dos, tags and notebooks.</li>
|
||||
<li>Goto Anything feature.</li>
|
||||
<li>Sort notes by multiple criteria - title, updated time, etc.</li>
|
||||
<li>Support for alarms (notifications) in mobile and desktop applications.</li>
|
||||
<li>Offline first, so the entire data is always available on the device even without an internet connection.</li>
|
||||
@@ -629,6 +630,8 @@ $$
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Notes are sorted by "relevance". Currently it means the notes that contain the requested terms the most times are on top. For queries with multiple terms, it also matter how close to each others are the terms. This is a bit experimental so if you notice a search query that returns unexpected results, please report it in the forum, providing as much details as possible to replicate the issue.</p>
|
||||
<h1><a name="goto-anything" href="#goto-anything" class="heading-anchor">🔗</a>Goto Anything</h1>
|
||||
<p>In the desktop application, press Ctrl+G or Cmd+G and type the title of a note to jump directly to it. You can also type <code>#</code> followed by a tag or <code>@</code> followed by a notebook title.</p>
|
||||
<h1><a name="donations" href="#donations" class="heading-anchor">🔗</a>Donations</h1>
|
||||
<p>Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.</p>
|
||||
<p>Please see the <a href="https://joplinapp.org/donate/">donation page</a> for information on how to support the development of Joplin.</p>
|
||||
@@ -670,7 +673,7 @@ $$
|
||||
<td>Arabic</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ar.po">ar</a></td>
|
||||
<td>عبد الناصر سعيد (<a href="mailto:as@althobaity.com">as@althobaity.com</a>)</td>
|
||||
<td>91%</td>
|
||||
<td>90%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/es/basque_country.png" alt=""></td>
|
||||
@@ -680,6 +683,13 @@ $$
|
||||
<td>51%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/bg.png" alt=""></td>
|
||||
<td>Bulgarian</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/bg_BG.po">bg_BG</a></td>
|
||||
<td></td>
|
||||
<td>99%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/es/catalonia.png" alt=""></td>
|
||||
<td>Catalan</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ca.po">ca</a></td>
|
||||
@@ -712,7 +722,7 @@ $$
|
||||
<td>Deutsch</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po">de_DE</a></td>
|
||||
<td>Michael Sonntag (<a href="mailto:ms@editorei.de">ms@editorei.de</a>)</td>
|
||||
<td>98%</td>
|
||||
<td>99%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/gb.png" alt=""></td>
|
||||
@@ -733,7 +743,7 @@ $$
|
||||
<td>Español</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po">es_ES</a></td>
|
||||
<td>Andros Fenollosa (andros@fenollosa.email)</td>
|
||||
<td>98%</td>
|
||||
<td>97%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/fr.png" alt=""></td>
|
||||
@@ -754,14 +764,14 @@ $$
|
||||
<td>Italiano</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po">it_IT</a></td>
|
||||
<td></td>
|
||||
<td>92%</td>
|
||||
<td>98%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/be.png" alt=""></td>
|
||||
<td>Nederlands</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po">nl_BE</a></td>
|
||||
<td></td>
|
||||
<td>52%</td>
|
||||
<td>51%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplinapp.org/images/flags/country-4x3/nl.png" alt=""></td>
|
||||
|
Reference in New Issue
Block a user