You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-23 23:33:01 +02:00
Compare commits
57 Commits
stress_tes
...
server_sha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93fe919300 | ||
|
|
a756db2ef4 | ||
|
|
843b52000e | ||
|
|
e8e8ea3780 | ||
|
|
3c13c8d080 | ||
|
|
97bfd5ef04 | ||
|
|
e3fd34e5d6 | ||
|
|
f144daed96 | ||
|
|
add9d884e6 | ||
|
|
62f81b4315 | ||
|
|
f33088fbe0 | ||
|
|
31b6d06418 | ||
|
|
06cd5ffa2d | ||
|
|
f3d4d8eaed | ||
|
|
dc08e1ded5 | ||
|
|
31c3fec8d8 | ||
|
|
4487cb85fc | ||
|
|
56cac1f729 | ||
|
|
3ade7ed849 | ||
|
|
a7eea9fc21 | ||
|
|
7fac1941cd | ||
|
|
061761f224 | ||
|
|
63e88c05d9 | ||
|
|
a6b1cffd50 | ||
|
|
8cc720963a | ||
|
|
da884752a8 | ||
|
|
818c7d4640 | ||
|
|
4577c9c161 | ||
|
|
03b4b6eb2d | ||
|
|
4d38397cd5 | ||
|
|
37d446b970 | ||
|
|
c91d4bda3c | ||
|
|
3e537967ee | ||
|
|
0cbc261051 | ||
|
|
542fdb496a | ||
|
|
d850eedd78 | ||
|
|
9429b51694 | ||
|
|
72e58ee195 | ||
|
|
56be4d59f4 | ||
|
|
bb740c75ec | ||
|
|
4244f712e1 | ||
|
|
e447acc076 | ||
|
|
87f83236cf | ||
|
|
6d981864ef | ||
|
|
0d40026d8b | ||
|
|
7a9ec627ee | ||
|
|
2d72d1435e | ||
|
|
12ec7b0c1d | ||
|
|
afe1cf747d | ||
|
|
c99aba0dff | ||
|
|
43c594b6b2 | ||
|
|
024967ce60 | ||
|
|
cd877f64cd | ||
|
|
f91b4edb30 | ||
|
|
b56177a4e3 | ||
|
|
4e70ca6fd0 | ||
|
|
2e04656b54 |
@@ -1000,6 +1000,9 @@ packages/lib/import-enex-md-gen.js.map
|
||||
packages/lib/import-enex-md-gen.test.d.ts
|
||||
packages/lib/import-enex-md-gen.test.js
|
||||
packages/lib/import-enex-md-gen.test.js.map
|
||||
packages/lib/import-enex.d.ts
|
||||
packages/lib/import-enex.js
|
||||
packages/lib/import-enex.js.map
|
||||
packages/lib/locale.d.ts
|
||||
packages/lib/locale.js
|
||||
packages/lib/locale.js.map
|
||||
@@ -1813,6 +1816,9 @@ packages/renderer/utils.js.map
|
||||
packages/tools/buildServerDocker.d.ts
|
||||
packages/tools/buildServerDocker.js
|
||||
packages/tools/buildServerDocker.js.map
|
||||
packages/tools/buildServerDocker.test.d.ts
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.test.js.map
|
||||
packages/tools/convertThemesToCss.d.ts
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/convertThemesToCss.js.map
|
||||
@@ -1846,6 +1852,9 @@ packages/tools/release-server.js.map
|
||||
packages/tools/setupNewRelease.d.ts
|
||||
packages/tools/setupNewRelease.js
|
||||
packages/tools/setupNewRelease.js.map
|
||||
packages/tools/tagServerLatest.d.ts
|
||||
packages/tools/tagServerLatest.js
|
||||
packages/tools/tagServerLatest.js.map
|
||||
packages/tools/tool-utils.d.ts
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/tool-utils.js.map
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -985,6 +985,9 @@ packages/lib/import-enex-md-gen.js.map
|
||||
packages/lib/import-enex-md-gen.test.d.ts
|
||||
packages/lib/import-enex-md-gen.test.js
|
||||
packages/lib/import-enex-md-gen.test.js.map
|
||||
packages/lib/import-enex.d.ts
|
||||
packages/lib/import-enex.js
|
||||
packages/lib/import-enex.js.map
|
||||
packages/lib/locale.d.ts
|
||||
packages/lib/locale.js
|
||||
packages/lib/locale.js.map
|
||||
@@ -1798,6 +1801,9 @@ packages/renderer/utils.js.map
|
||||
packages/tools/buildServerDocker.d.ts
|
||||
packages/tools/buildServerDocker.js
|
||||
packages/tools/buildServerDocker.js.map
|
||||
packages/tools/buildServerDocker.test.d.ts
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.test.js.map
|
||||
packages/tools/convertThemesToCss.d.ts
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/convertThemesToCss.js.map
|
||||
@@ -1831,6 +1837,9 @@ packages/tools/release-server.js.map
|
||||
packages/tools/setupNewRelease.d.ts
|
||||
packages/tools/setupNewRelease.js
|
||||
packages/tools/setupNewRelease.js.map
|
||||
packages/tools/tagServerLatest.d.ts
|
||||
packages/tools/tagServerLatest.js
|
||||
packages/tools/tagServerLatest.js.map
|
||||
packages/tools/tool-utils.d.ts
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/tool-utils.js.map
|
||||
|
||||
@@ -137,8 +137,8 @@ fi
|
||||
#-----------------------------------------------------
|
||||
print 'Downloading Joplin...'
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
wget -O ${TEMP_DIR}/Joplin.AppImage https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage
|
||||
wget -O ${TEMP_DIR}/joplin.png https://joplinapp.org/images/Icon512.png
|
||||
wget -O "${TEMP_DIR}/Joplin.AppImage" "https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage"
|
||||
wget -O "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
|
||||
|
||||
#-----------------------------------------------------
|
||||
print 'Installing Joplin...'
|
||||
@@ -149,7 +149,7 @@ rm -f ~/.joplin/*.AppImage ~/.local/share/applications/joplin.desktop ~/.joplin/
|
||||
mkdir -p ~/.joplin/
|
||||
|
||||
# Download the latest version
|
||||
mv ${TEMP_DIR}/Joplin.AppImage ~/.joplin/Joplin.AppImage
|
||||
mv "${TEMP_DIR}/Joplin.AppImage" ~/.joplin/Joplin.AppImage
|
||||
|
||||
# Gives execution privileges
|
||||
chmod +x ~/.joplin/Joplin.AppImage
|
||||
@@ -159,7 +159,7 @@ print "${COLOR_GREEN}OK${COLOR_RESET}"
|
||||
#-----------------------------------------------------
|
||||
print 'Installing icon...'
|
||||
mkdir -p ~/.local/share/icons/hicolor/512x512/apps
|
||||
mv ${TEMP_DIR}/joplin.png ~/.local/share/icons/hicolor/512x512/apps/joplin.png
|
||||
mv "${TEMP_DIR}/joplin.png" ~/.local/share/icons/hicolor/512x512/apps/joplin.png
|
||||
print "${COLOR_GREEN}OK${COLOR_RESET}"
|
||||
|
||||
# Detect desktop environment
|
||||
@@ -222,7 +222,7 @@ fi
|
||||
print "${COLOR_GREEN}Joplin version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
|
||||
|
||||
# Record version
|
||||
echo $RELEASE_VERSION > ~/.joplin/VERSION
|
||||
echo "$RELEASE_VERSION" > ~/.joplin/VERSION
|
||||
|
||||
#-----------------------------------------------------
|
||||
if [[ "$SHOW_CHANGELOG" == true ]]; then
|
||||
@@ -232,5 +232,5 @@ fi
|
||||
|
||||
#-----------------------------------------------------
|
||||
print "Cleaning up..."
|
||||
rm -rf $TEMP_DIR
|
||||
rm -rf "$TEMP_DIR"
|
||||
print "${COLOR_GREEN}OK${COLOR_RESET}"
|
||||
|
||||
38
README.md
38
README.md
@@ -504,47 +504,47 @@ Current translations:
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Po File | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 91%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 28%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 71%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 56%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 55%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [marph91](mailto:martin.d@andix.de) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 54%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Mora](mailto:francisco.m.collao@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 31%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 96%
|
||||
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 36%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 87%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 83%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | Alexander Dawson | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 67%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Nicolas Suzuki](mailto:nicolas.suzuki@pm.me) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 63%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 91%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 62%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 42%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vi.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 92%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 81%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 80%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Po-Chiang Chao](mailto:BobChao%29%20%28bobchao@gmail.com) | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [SiderealArt](mailto:nelson22768384@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 94%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Contributors
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"releaseIOS": "node packages/tools/release-ios.js",
|
||||
"releasePluginGenerator": "node packages/tools/release-plugin-generator.js",
|
||||
"releaseServer": "node packages/tools/release-server.js",
|
||||
"tagServerLatest": "node packages/tools/tagServerLatest.js",
|
||||
"buildServerDocker": "node packages/tools/buildServerDocker.js",
|
||||
"setupNewRelease": "node ./packages/tools/setupNewRelease",
|
||||
"test-ci": "lerna run test-ci --stream",
|
||||
|
||||
@@ -84,4 +84,10 @@ describe('HtmlToMd', function() {
|
||||
}
|
||||
}));
|
||||
|
||||
it('should allow disabling escape', async () => {
|
||||
const htmlToMd = new HtmlToMd();
|
||||
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: true })).toBe('https://test.com/1_2_3.pdf');
|
||||
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: false })).toBe('https://test.com/1\\_2\\_3.pdf');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <s>Strike</s>
|
||||
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <span style="text-decoration: underline;">Insert alt</span> <s>Strike</s>
|
||||
@@ -1 +1 @@
|
||||
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> ~~Strike~~
|
||||
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <ins>Insert alt</ins> ~~Strike~~
|
||||
@@ -1,3 +1,20 @@
|
||||
// This script can be used to simulate a running production environment, by
|
||||
// having multiple users in parallel changing notes and synchronising.
|
||||
//
|
||||
// To get it working:
|
||||
//
|
||||
// - Run the Postgres database -- `sudo docker-compose --file docker-compose.db-dev.yml up`
|
||||
// - Update the DB parameters in ~/joplin-credentials/server.env to use the dev
|
||||
// database
|
||||
// - Run the server - `JOPLIN_IS_TESTING=1 npm run start-dev`
|
||||
// - Then run this script - `node populateDatabase.js`
|
||||
//
|
||||
// Currently it doesn't actually create the users, so that should be done using:
|
||||
//
|
||||
// curl --data '{"action": "createTestUsers", "count": 400, "fromNum": 1}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
//
|
||||
// That will create n users with email `user<n>@example.com`
|
||||
|
||||
import * as fs from 'fs-extra';
|
||||
import { homedir } from 'os';
|
||||
import { execCommand2 } from '@joplin/tools/tool-utils';
|
||||
@@ -73,8 +90,13 @@ const main = async () => {
|
||||
// run the scripts (faster)
|
||||
await execCommand2(['npm', 'run', 'build']);
|
||||
|
||||
const focusUserNum = 400;
|
||||
|
||||
while (true) {
|
||||
const userNum = randomInt(minUserNum, maxUserNum);
|
||||
let userNum = randomInt(minUserNum, maxUserNum);
|
||||
|
||||
if (Math.random() >= .7) userNum = focusUserNum;
|
||||
|
||||
void processUser(userNum);
|
||||
await waitForProcessing(10);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 441 B After Width: | Height: | Size: 441 B |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -42,9 +42,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
this.sidebar_selectionChange = this.sidebar_selectionChange.bind(this);
|
||||
this.checkSyncConfig_ = this.checkSyncConfig_.bind(this);
|
||||
// this.checkNextcloudAppButton_click = this.checkNextcloudAppButton_click.bind(this);
|
||||
this.showLogButton_click = this.showLogButton_click.bind(this);
|
||||
this.nextcloudAppHelpLink_click = this.nextcloudAppHelpLink_click.bind(this);
|
||||
this.onCancelClick = this.onCancelClick.bind(this);
|
||||
this.onSaveClick = this.onSaveClick.bind(this);
|
||||
this.onApplyClick = this.onApplyClick.bind(this);
|
||||
@@ -58,19 +55,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
}
|
||||
|
||||
// async checkNextcloudAppButton_click() {
|
||||
// this.setState({ showNextcloudAppLog: true });
|
||||
// await shared.checkNextcloudApp(this, this.state.settings);
|
||||
// }
|
||||
|
||||
showLogButton_click() {
|
||||
this.setState({ showNextcloudAppLog: true });
|
||||
}
|
||||
|
||||
nextcloudAppHelpLink_click() {
|
||||
bridge().openExternal('https://joplinapp.org/nextcloud_app');
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import PluginBox, { InstallState } from './PluginBox';
|
||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useOnInstallHandler from './useOnInstallHandler';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
const Root = styled.div`
|
||||
`;
|
||||
@@ -100,6 +101,13 @@ export default function(props: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
const renderContentSourceInfo = () => {
|
||||
if (props.repoApi().isUsingDefaultContentUrl) return null;
|
||||
const theme = themeStyle(props.themeId);
|
||||
const url = new URL(props.repoApi().contentBaseUrl);
|
||||
return <div style={{ ...theme.textStyleMinor, marginTop: 5, fontSize: theme.fontSize }}>{_('Content provided by %s', url.hostname)}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<div style={{ marginBottom: 10, width: props.maxWidth }}>
|
||||
@@ -112,6 +120,7 @@ export default function(props: Props) {
|
||||
placeholder={props.disabled ? _('Please wait...') : _('Search for plugins...')}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
{renderContentSourceInfo()}
|
||||
</div>
|
||||
|
||||
<ResultsRoot>
|
||||
|
||||
@@ -4,7 +4,7 @@ const Folder = require('@joplin/lib/models/Folder').default;
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { filename, basename } = require('@joplin/lib/path-utils');
|
||||
const { importEnex } = require('@joplin/lib/import-enex');
|
||||
const importEnex = require('@joplin/lib/import-enex').default;
|
||||
|
||||
class ImportScreenComponent extends React.Component {
|
||||
UNSAFE_componentWillMount() {
|
||||
|
||||
@@ -19,6 +19,11 @@ export const runtime = (): CommandRuntime => {
|
||||
visible: !layoutItemProp(layout, 'noteList', 'visible'),
|
||||
});
|
||||
|
||||
// Toggling the sidebar will affect the size of most other on-screen components.
|
||||
// Dispatching a window resize event is a bit of a hack, but it ensures that any
|
||||
// component that watches for resizes will be accurately notified
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
context.dispatch({
|
||||
type: 'MAIN_LAYOUT_SET',
|
||||
value: newLayout,
|
||||
|
||||
@@ -19,6 +19,11 @@ export const runtime = (): CommandRuntime => {
|
||||
visible: !layoutItemProp(layout, 'sideBar', 'visible'),
|
||||
});
|
||||
|
||||
// Toggling the sidebar will affect the size of most other on-screen components.
|
||||
// Dispatching a window resize event is a bit of a hack, but it ensures that any
|
||||
// component that watches for resizes will be accurately notified
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
context.dispatch({
|
||||
type: 'MAIN_LAYOUT_SET',
|
||||
value: newLayout,
|
||||
|
||||
@@ -6,7 +6,8 @@ import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
|
||||
import { CommandValue } from '../../utils/types';
|
||||
import { useScrollHandler, usePrevious, cursorPositionToTextOffset, useRootSize } from './utils';
|
||||
import { useScrollHandler, usePrevious, cursorPositionToTextOffset } from './utils';
|
||||
import useElementSize from '@joplin/lib/hooks/useElementSize';
|
||||
import Toolbar from './Toolbar';
|
||||
import styles_ from './styles';
|
||||
import { RenderedBody, defaultRenderedBody } from './utils/types';
|
||||
@@ -59,7 +60,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
const props_onChangeRef = useRef<Function>(null);
|
||||
props_onChangeRef.current = props.onChange;
|
||||
|
||||
const rootSize = useRootSize({ rootRef });
|
||||
const rootSize = useElementSize(rootRef);
|
||||
|
||||
usePluginServiceRegistration(ref);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useCallback, useRef, useState } from 'react';
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
export function cursorPositionToTextOffset(cursorPos: any, body: string) {
|
||||
@@ -89,21 +89,3 @@ export function useScrollHandler(editorRef: any, webviewRef: any, onScroll: Func
|
||||
return { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll };
|
||||
}
|
||||
|
||||
|
||||
export function useRootSize(dependencies: any) {
|
||||
const { rootRef } = dependencies;
|
||||
|
||||
const [rootSize, setRootSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootRef.current) return;
|
||||
|
||||
const { width, height } = rootRef.current.getBoundingClientRect();
|
||||
|
||||
if (rootSize.width !== width || rootSize.height !== height) {
|
||||
setRootSize({ width: width, height: height });
|
||||
}
|
||||
});
|
||||
|
||||
return rootSize;
|
||||
}
|
||||
|
||||
@@ -171,6 +171,7 @@ export default function useKeymap(CodeMirror: any) {
|
||||
'Cmd-Right': 'goLineRightSmart',
|
||||
'Alt-Backspace': 'delGroupBefore',
|
||||
'Alt-Delete': 'delGroupAfter',
|
||||
'Cmd-Backspace': 'delWrappedLineLeft',
|
||||
|
||||
'fallthrough': 'basic',
|
||||
};
|
||||
|
||||
@@ -793,7 +793,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
};
|
||||
}
|
||||
|
||||
await loadDocumentAssets(editor, await props.allAssets(props.contentMarkupLanguage));
|
||||
await loadDocumentAssets(editor, await props.allAssets(props.contentMarkupLanguage, { contentMaxWidthTarget: '.mce-content-body' }));
|
||||
|
||||
dispatchDidUpdate(editor);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import useMarkupToHtml from './utils/useMarkupToHtml';
|
||||
import useFormNote, { OnLoadEvent } from './utils/useFormNote';
|
||||
import useFolder from './utils/useFolder';
|
||||
import styles_ from './styles';
|
||||
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types';
|
||||
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions } from './utils/types';
|
||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import ToolbarButton from '../ToolbarButton/ToolbarButton';
|
||||
@@ -151,7 +151,12 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
plugins: props.plugins,
|
||||
});
|
||||
|
||||
const allAssets = useCallback(async (markupLanguage: number): Promise<any[]> => {
|
||||
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => {
|
||||
options = {
|
||||
contentMaxWidthTarget: '',
|
||||
...options,
|
||||
};
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
|
||||
@@ -159,7 +164,10 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
customCss: props.customCss,
|
||||
});
|
||||
|
||||
return markupToHtml.allAssets(markupLanguage, theme, { contentMaxWidth: props.contentMaxWidth });
|
||||
return markupToHtml.allAssets(markupLanguage, theme, {
|
||||
contentMaxWidth: props.contentMaxWidth,
|
||||
contentMaxWidthTarget: options.contentMaxWidthTarget,
|
||||
});
|
||||
}, [props.themeId, props.customCss, props.contentMaxWidth]);
|
||||
|
||||
const handleProvisionalFlag = useCallback(() => {
|
||||
|
||||
@@ -69,8 +69,17 @@ export function htmlToClipboardData(html: string): ClipboardData {
|
||||
// In that case we need to set both HTML and Text context, otherwise it
|
||||
// won't be possible to paste the text in, for example, a text editor.
|
||||
// https://github.com/laurent22/joplin/issues/4788
|
||||
//
|
||||
// Also we don't escape the content produced in HTML to MD conversion
|
||||
// because it's not what would be expected. For example, if the content is
|
||||
// `* something`, strictly speaking it would be correct to escape to `\*
|
||||
// something`, however this is not what the user would expect when copying
|
||||
// text. Likewise for URLs that contain "_". So the resulting Markdown might
|
||||
// not be perfectly valid but would be closer to what a user would expect.
|
||||
// If they want accurate MArkdown they can switch to the MD editor.
|
||||
// https://github.com/laurent22/joplin/issues/5440
|
||||
return {
|
||||
text: htmlToMd().parse(copyableContent),
|
||||
text: htmlToMd().parse(copyableContent, { disableEscapeContent: true }),
|
||||
html: cleanUpCodeBlocks(copyableContent),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml';
|
||||
import { MarkupToHtmlOptions } from './useMarkupToHtml';
|
||||
|
||||
export interface AllAssetsOptions {
|
||||
contentMaxWidthTarget?: string;
|
||||
}
|
||||
|
||||
export interface ToolbarButtonInfos {
|
||||
[key: string]: ToolbarButtonInfo;
|
||||
}
|
||||
@@ -55,7 +59,7 @@ export interface NoteBodyEditorProps {
|
||||
onScroll(event: any): void;
|
||||
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
|
||||
htmlToMarkdown: Function;
|
||||
allAssets: (markupLanguage: MarkupLanguage)=> Promise<RenderResultPluginAsset[]>;
|
||||
allAssets: (markupLanguage: MarkupLanguage, options: AllAssetsOptions)=> Promise<RenderResultPluginAsset[]>;
|
||||
disabled: boolean;
|
||||
dispatch: Function;
|
||||
noteToolbar: any;
|
||||
|
||||
@@ -139,9 +139,10 @@ class NoteSearchBarComponent extends React.Component {
|
||||
color: theme.colorFaded,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
});
|
||||
const matchesFoundString = (query.length > 0 && this.props.resultCount > 0) ? (
|
||||
|
||||
const matchesFoundString = (query.length > 0) ? (
|
||||
<div style={textStyle}>
|
||||
{`${this.props.selectedIndex + 1} / ${this.props.resultCount}`}
|
||||
{`${this.props.resultCount === 0 ? 0 : this.props.selectedIndex + 1} / ${this.props.resultCount}`}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
|
||||
@@ -138,9 +138,9 @@ const syncTargetNames: string[] = [
|
||||
|
||||
|
||||
const logosImageNames: Record<string, string> = {
|
||||
'dropbox': 'Dropbox.svg',
|
||||
'joplinCloud': 'JoplinCloud.svg',
|
||||
'onedrive': 'OneDrive.svg',
|
||||
'dropbox': 'SyncTarget_Dropbox.svg',
|
||||
'joplinCloud': 'SyncTarget_JoplinCloud.svg',
|
||||
'onedrive': 'SyncTarget_OneDrive.svg',
|
||||
};
|
||||
|
||||
export default function(props: Props) {
|
||||
@@ -274,7 +274,7 @@ export default function(props: Props) {
|
||||
const height = info.name !== 'joplinCloud' ? descriptionHeight : null;
|
||||
|
||||
const logoImageName = logosImageNames[info.name];
|
||||
const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/syncTargetLogos/${logoImageName}` : '';
|
||||
const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/${logoImageName}` : '';
|
||||
const logo = logoImageSrc ? <SyncTargetLogo src={logoImageSrc}/> : null;
|
||||
const descriptionComp = <SyncTargetDescription height={height} ref={info.name === 'joplinCloud' ? joplinCloudDescriptionRef : null}>{info.description}</SyncTargetDescription>;
|
||||
const featuresComp = showJoplinCloudForm && info.name === 'joplinCloud' ? null : renderFeatures(info.name);
|
||||
|
||||
4
packages/app-desktop/package-lock.json
generated
4
packages/app-desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.6",
|
||||
"version": "2.4.8",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.6",
|
||||
"version": "2.4.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.4.6",
|
||||
"version": "2.4.8",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
||||
@@ -141,8 +141,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097649
|
||||
versionName "2.4.1"
|
||||
versionCode 2097650
|
||||
versionName "2.4.2"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ dialogs.confirm = (parentComponent, message) => {
|
||||
if (!parentComponent) throw new Error('parentComponent is required');
|
||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||
|
||||
return dialogs.confirmRef(parentComponent.dialogBox, message);
|
||||
return dialogs.confirmRef(parentComponent.dialogbox, message);
|
||||
};
|
||||
|
||||
dialogs.pop = (parentComponent, message, buttons, options = null) => {
|
||||
|
||||
@@ -251,6 +251,12 @@ export default class BaseApplication {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.indexOf('--user-data-dir=') === 0) {
|
||||
// Electron-specific flag. Allows users to run the app with chromedriver.
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.length && arg[0] == '-') {
|
||||
throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ParseOptions {
|
||||
anchorNames?: string[];
|
||||
preserveImageTagsWithSize?: boolean;
|
||||
baseUrl?: string;
|
||||
disableEscapeContent?: boolean;
|
||||
}
|
||||
|
||||
export default class HtmlToMd {
|
||||
@@ -20,6 +21,7 @@ export default class HtmlToMd {
|
||||
emDelimiter: '*',
|
||||
strongDelimiter: '**',
|
||||
br: '',
|
||||
disableEscapeContent: 'disableEscapeContent' in options ? options.disableEscapeContent : false,
|
||||
});
|
||||
turndown.use(turndownPluginGfm);
|
||||
turndown.remove('script');
|
||||
|
||||
@@ -479,6 +479,31 @@ export default class Synchronizer {
|
||||
void this.cancel();
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// 2. DELETE_REMOTE
|
||||
// ------------------------------------------------------------------------
|
||||
// Delete the remote items that have been deleted locally.
|
||||
// ========================================================================
|
||||
|
||||
if (syncSteps.indexOf('delete_remote') >= 0) {
|
||||
const deletedItems = await BaseItem.deletedItems(syncTargetId);
|
||||
for (let i = 0; i < deletedItems.length; i++) {
|
||||
if (this.cancelling()) break;
|
||||
|
||||
const item = deletedItems[i];
|
||||
const path = BaseItem.systemPath(item.item_id);
|
||||
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
|
||||
await this.apiCall('delete', path);
|
||||
|
||||
if (item.item_type === BaseModel.TYPE_RESOURCE) {
|
||||
const remoteContentPath = resourceRemotePath(item.item_id);
|
||||
await this.apiCall('delete', remoteContentPath);
|
||||
}
|
||||
|
||||
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
|
||||
}
|
||||
} // DELETE_REMOTE STEP
|
||||
|
||||
// ========================================================================
|
||||
// 1. UPLOAD
|
||||
// ------------------------------------------------------------------------
|
||||
@@ -763,31 +788,6 @@ export default class Synchronizer {
|
||||
}
|
||||
} // UPLOAD STEP
|
||||
|
||||
// ========================================================================
|
||||
// 2. DELETE_REMOTE
|
||||
// ------------------------------------------------------------------------
|
||||
// Delete the remote items that have been deleted locally.
|
||||
// ========================================================================
|
||||
|
||||
if (syncSteps.indexOf('delete_remote') >= 0) {
|
||||
const deletedItems = await BaseItem.deletedItems(syncTargetId);
|
||||
for (let i = 0; i < deletedItems.length; i++) {
|
||||
if (this.cancelling()) break;
|
||||
|
||||
const item = deletedItems[i];
|
||||
const path = BaseItem.systemPath(item.item_id);
|
||||
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
|
||||
await this.apiCall('delete', path);
|
||||
|
||||
if (item.item_type === BaseModel.TYPE_RESOURCE) {
|
||||
const remoteContentPath = resourceRemotePath(item.item_id);
|
||||
await this.apiCall('delete', remoteContentPath);
|
||||
}
|
||||
|
||||
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
|
||||
}
|
||||
} // DELETE_REMOTE STEP
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 3. DELTA
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
@@ -9,10 +9,8 @@ const shared = {};
|
||||
shared.init = function(comp) {
|
||||
if (!comp.state) comp.state = {};
|
||||
comp.state.checkSyncConfigResult = null;
|
||||
comp.state.checkNextcloudAppResult = null;
|
||||
comp.state.settings = {};
|
||||
comp.state.changedSettingKeys = [];
|
||||
comp.state.showNextcloudAppLog = false;
|
||||
comp.state.showAdvancedSettings = false;
|
||||
};
|
||||
|
||||
@@ -35,7 +33,6 @@ shared.checkSyncConfig = async function(comp, settings) {
|
||||
comp.setState({ checkSyncConfigResult: result });
|
||||
|
||||
if (result.ok) {
|
||||
// await shared.checkNextcloudApp(comp, settings);
|
||||
// Users often expect config to be auto-saved at this point, if the config check was successful
|
||||
shared.saveSettings(comp);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const os = require('os');
|
||||
const { filename } = require('./path-utils');
|
||||
import { setupDatabaseAndSynchronizer, switchClient, expectNotThrow, supportDir } from './testing/test-utils';
|
||||
const { enexXmlToMd } = require('./import-enex-md-gen.js');
|
||||
const { importEnex } = require('./import-enex');
|
||||
import importEnex from './import-enex';
|
||||
import Note from './models/Note';
|
||||
import Tag from './models/Tag';
|
||||
import Resource from './models/Resource';
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
const uuid = require('./uuid').default;
|
||||
import uuid from './uuid';
|
||||
import BaseModel from './BaseModel';
|
||||
import Note from './models/Note';
|
||||
import Tag from './models/Tag';
|
||||
import Resource from './models/Resource';
|
||||
import Setting from './models/Setting';
|
||||
import time from './time';
|
||||
import shim from './shim';
|
||||
import { NoteEntity } from './services/database/types';
|
||||
import { enexXmlToMd } from './import-enex-md-gen';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
const moment = require('moment');
|
||||
const BaseModel = require('./BaseModel').default;
|
||||
const Note = require('./models/Note').default;
|
||||
const Tag = require('./models/Tag').default;
|
||||
const Resource = require('./models/Resource').default;
|
||||
const Setting = require('./models/Setting').default;
|
||||
const { MarkupToHtml } = require('@joplin/renderer');
|
||||
const { wrapError } = require('./errorUtils');
|
||||
const { enexXmlToMd } = require('./import-enex-md-gen.js');
|
||||
const { enexXmlToHtml } = require('./import-enex-html-gen.js');
|
||||
const time = require('./time').default;
|
||||
const Levenshtein = require('levenshtein');
|
||||
const md5 = require('md5');
|
||||
const { Base64Decode } = require('base64-stream');
|
||||
const md5File = require('md5-file');
|
||||
const shim = require('./shim').default;
|
||||
const { mime } = require('./mime-utils');
|
||||
|
||||
// const Promise = require('promise');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
function dateToTimestamp(s, defaultValue = null) {
|
||||
function dateToTimestamp(s: string, defaultValue: number = null): number {
|
||||
// Most dates seem to be in this format
|
||||
let m = moment(s, 'YYYYMMDDTHHmmssZ');
|
||||
|
||||
@@ -36,12 +37,12 @@ function dateToTimestamp(s, defaultValue = null) {
|
||||
return m.toDate().getTime();
|
||||
}
|
||||
|
||||
function extractRecognitionObjId(recognitionXml) {
|
||||
function extractRecognitionObjId(recognitionXml: string) {
|
||||
const r = recognitionXml.match(/objID="(.*?)"/);
|
||||
return r && r.length >= 2 ? r[1] : null;
|
||||
}
|
||||
|
||||
async function decodeBase64File(sourceFilePath, destFilePath) {
|
||||
async function decodeBase64File(sourceFilePath: string, destFilePath: string) {
|
||||
// When something goes wrong with streams you can get an error "EBADF, Bad file descriptor"
|
||||
// with no strack trace to tell where the error happened.
|
||||
|
||||
@@ -73,17 +74,17 @@ async function decodeBase64File(sourceFilePath, destFilePath) {
|
||||
destStream.on('finish', () => {
|
||||
fs.fdatasyncSync(destFile);
|
||||
fs.closeSync(destFile);
|
||||
resolve();
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
sourceStream.on('error', (error) => reject(error));
|
||||
destStream.on('error', (error) => reject(error));
|
||||
sourceStream.on('error', (error: any) => reject(error));
|
||||
destStream.on('error', (error: any) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
async function md5FileAsync(filePath) {
|
||||
async function md5FileAsync(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
md5File(filePath, (error, hash) => {
|
||||
md5File(filePath, (error: any, hash: string) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
@@ -94,24 +95,24 @@ async function md5FileAsync(filePath) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeUndefinedProperties(note) {
|
||||
const output = {};
|
||||
function removeUndefinedProperties(note: NoteEntity) {
|
||||
const output: any = {};
|
||||
for (const n in note) {
|
||||
if (!note.hasOwnProperty(n)) continue;
|
||||
const v = note[n];
|
||||
const v = (note as any)[n];
|
||||
if (v === undefined || v === null) continue;
|
||||
output[n] = v;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function levenshteinPercent(s1, s2) {
|
||||
function levenshteinPercent(s1: string, s2: string) {
|
||||
const l = new Levenshtein(s1, s2);
|
||||
if (!s1.length || !s2.length) return 1;
|
||||
return Math.abs(l.distance / s1.length);
|
||||
}
|
||||
|
||||
async function fuzzyMatch(note) {
|
||||
async function fuzzyMatch(note: ExtractedNote) {
|
||||
if (note.created_time < time.unixMs() - 1000 * 60 * 60 * 24 * 360) {
|
||||
const notes = await Note.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0 AND created_time = ? AND title = ?', [note.created_time, note.title]);
|
||||
return notes.length !== 1 ? null : notes[0];
|
||||
@@ -137,9 +138,30 @@ async function fuzzyMatch(note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
interface ExtractedResource {
|
||||
hasData?: boolean;
|
||||
id?: string;
|
||||
size?: number;
|
||||
dataFilePath?: string;
|
||||
dataEncoding?: string;
|
||||
data?: string;
|
||||
filename?: string;
|
||||
sourceUrl?: string;
|
||||
mime?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface ExtractedNote extends NoteEntity {
|
||||
resources?: ExtractedResource[];
|
||||
tags?: string[];
|
||||
title?: string;
|
||||
bodyXml?: string;
|
||||
// is_todo?: boolean;
|
||||
}
|
||||
|
||||
// At this point we have the resource has it's been parsed from the XML, but additional
|
||||
// processing needs to be done to get the final resource file, its size, MD5, etc.
|
||||
async function processNoteResource(resource) {
|
||||
async function processNoteResource(resource: ExtractedResource) {
|
||||
if (!resource.hasData) {
|
||||
// Some resources have no data, go figure, so we need a special case for this.
|
||||
resource.id = md5(Date.now() + Math.random());
|
||||
@@ -175,7 +197,7 @@ async function processNoteResource(resource) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
async function saveNoteResources(note) {
|
||||
async function saveNoteResources(note: ExtractedNote) {
|
||||
let resourcesCreated = 0;
|
||||
for (let i = 0; i < note.resources.length; i++) {
|
||||
const resource = note.resources[i];
|
||||
@@ -198,7 +220,7 @@ async function saveNoteResources(note) {
|
||||
return resourcesCreated;
|
||||
}
|
||||
|
||||
async function saveNoteTags(note) {
|
||||
async function saveNoteTags(note: ExtractedNote) {
|
||||
let notesTagged = 0;
|
||||
for (let i = 0; i < note.tags.length; i++) {
|
||||
const tagTitle = note.tags[i];
|
||||
@@ -213,12 +235,19 @@ async function saveNoteTags(note) {
|
||||
return notesTagged;
|
||||
}
|
||||
|
||||
async function saveNoteToStorage(note, importOptions) {
|
||||
interface ImportOptions {
|
||||
fuzzyMatching?: boolean;
|
||||
onProgress?: Function;
|
||||
onError?: Function;
|
||||
outputFormat?: string;
|
||||
}
|
||||
|
||||
async function saveNoteToStorage(note: ExtractedNote, importOptions: ImportOptions) {
|
||||
importOptions = Object.assign({}, {
|
||||
fuzzyMatching: false,
|
||||
}, importOptions);
|
||||
|
||||
note = Note.filter(note);
|
||||
note = Note.filter(note as any);
|
||||
|
||||
const existingNote = importOptions.fuzzyMatching ? await fuzzyMatch(note) : null;
|
||||
|
||||
@@ -230,7 +259,7 @@ async function saveNoteToStorage(note, importOptions) {
|
||||
notesTagged: 0,
|
||||
};
|
||||
|
||||
const resourcesCreated = await saveNoteResources(note, importOptions);
|
||||
const resourcesCreated = await saveNoteResources(note);
|
||||
result.resourcesCreated += resourcesCreated;
|
||||
|
||||
const notesTagged = await saveNoteTags(note);
|
||||
@@ -262,16 +291,50 @@ async function saveNoteToStorage(note, importOptions) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
interface Node {
|
||||
name: string;
|
||||
attributes: Record<string, any>;
|
||||
}
|
||||
|
||||
interface NoteResourceRecognition {
|
||||
objID?: string;
|
||||
}
|
||||
|
||||
const preProcessFile = async (filePath: string): Promise<string> => {
|
||||
const content: string = await shim.fsDriver().readFile(filePath, 'utf8');
|
||||
|
||||
// The note content in an ENEX file is wrapped in a CDATA block so it means
|
||||
// that any "]]>" inside the note must be somehow escaped, or else the CDATA
|
||||
// block would be closed at the wrong point.
|
||||
//
|
||||
// The problem is that Evernote appears to encode "]]>" as "]]<![CDATA[>]]>"
|
||||
// instead of the more sensible "]]>", or perhaps they have nothing in
|
||||
// place to properly escape data imported from their web clipper. In any
|
||||
// case it results in invalid XML that Evernote cannot even import back.
|
||||
//
|
||||
// Handling that invalid XML with SAX would also be very tricky, so instead
|
||||
// we add a pre-processing step that converts this tags to just ">". It
|
||||
// should be safe to do so because such content can only be within the body
|
||||
// of a note - and ">" or ">" is equivalent.
|
||||
//
|
||||
// Ref: https://discourse.joplinapp.org/t/20470/4
|
||||
const newContent = content.replace(/<!\[CDATA\[>\]\]>/g, '>');
|
||||
if (content === newContent) return filePath;
|
||||
const newFilePath = `${Setting.value('tempDir')}/${md5(Date.now() + Math.random())}.enex`;
|
||||
await shim.fsDriver().writeFile(newFilePath, newContent, 'utf8');
|
||||
return newFilePath;
|
||||
};
|
||||
|
||||
export default async function importEnex(parentFolderId: string, filePath: string, importOptions: ImportOptions = null) {
|
||||
if (!importOptions) importOptions = {};
|
||||
if (!('fuzzyMatching' in importOptions)) importOptions.fuzzyMatching = false;
|
||||
if (!('onProgress' in importOptions)) importOptions.onProgress = function() {};
|
||||
if (!('onError' in importOptions)) importOptions.onError = function() {};
|
||||
|
||||
function handleSaxStreamEvent(fn) {
|
||||
return function(...args) {
|
||||
function handleSaxStreamEvent(fn: Function) {
|
||||
return function(...args: any[]) {
|
||||
// Pass the parser to the wrapped function for debugging purposes
|
||||
if (this._parser) fn._parser = this._parser;
|
||||
if (this._parser) (fn as any)._parser = this._parser;
|
||||
|
||||
try {
|
||||
fn.call(this, ...args);
|
||||
@@ -285,6 +348,9 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
};
|
||||
}
|
||||
|
||||
const fileToProcess = await preProcessFile(filePath);
|
||||
const needToDeleteFileToProcess = fileToProcess !== filePath;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const progressState = {
|
||||
loaded: 0,
|
||||
@@ -295,22 +361,22 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
notesTagged: 0,
|
||||
};
|
||||
|
||||
const stream = fs.createReadStream(filePath);
|
||||
const stream = fs.createReadStream(fileToProcess);
|
||||
|
||||
const options = {};
|
||||
const strict = true;
|
||||
const saxStream = require('@joplin/fork-sax').createStream(strict, options);
|
||||
|
||||
const nodes = []; // LIFO list of nodes so that we know in which node we are in the onText event
|
||||
let note = null;
|
||||
let noteAttributes = null;
|
||||
let noteResource = null;
|
||||
let noteResourceAttributes = null;
|
||||
let noteResourceRecognition = null;
|
||||
const notes = [];
|
||||
const nodes: Node[] = []; // LIFO list of nodes so that we know in which node we are in the onText event
|
||||
let note: ExtractedNote = null;
|
||||
let noteAttributes: Record<string, any> = null;
|
||||
let noteResource: ExtractedResource = null;
|
||||
let noteResourceAttributes: Record<string, any> = null;
|
||||
let noteResourceRecognition: NoteResourceRecognition = null;
|
||||
const notes: ExtractedNote[] = [];
|
||||
let processingNotes = false;
|
||||
|
||||
const createErrorWithNoteTitle = (fnThis, error) => {
|
||||
const createErrorWithNoteTitle = (fnThis: any, error: any) => {
|
||||
const line = [];
|
||||
|
||||
const parser = fnThis ? fnThis._parser : null;
|
||||
@@ -329,7 +395,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
return error;
|
||||
};
|
||||
|
||||
stream.on('error', function(error) {
|
||||
stream.on('error', function(error: any) {
|
||||
importOptions.onError(createErrorWithNoteTitle(this, error));
|
||||
});
|
||||
|
||||
@@ -417,11 +483,11 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
saxStream.on('error', function(error) {
|
||||
saxStream.on('error', function(error: any) {
|
||||
importOptions.onError(createErrorWithNoteTitle(this, error));
|
||||
});
|
||||
|
||||
saxStream.on('text', handleSaxStreamEvent(function(text) {
|
||||
saxStream.on('text', handleSaxStreamEvent(function(text: string) {
|
||||
const n = currentNodeName();
|
||||
|
||||
if (noteAttributes) {
|
||||
@@ -443,8 +509,8 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
|
||||
fs.appendFileSync(noteResource.dataFilePath, text);
|
||||
} else {
|
||||
if (!(n in noteResource)) noteResource[n] = '';
|
||||
noteResource[n] += text;
|
||||
if (!(n in noteResource)) (noteResource as any)[n] = '';
|
||||
(noteResource as any)[n] += text;
|
||||
}
|
||||
} else if (note) {
|
||||
if (n == 'title') {
|
||||
@@ -465,7 +531,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
}
|
||||
}));
|
||||
|
||||
saxStream.on('opentag', handleSaxStreamEvent(function(node) {
|
||||
saxStream.on('opentag', handleSaxStreamEvent(function(node: Node) {
|
||||
const n = node.name.toLowerCase();
|
||||
nodes.push(node);
|
||||
|
||||
@@ -488,7 +554,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
}
|
||||
}));
|
||||
|
||||
saxStream.on('cdata', handleSaxStreamEvent(function(data) {
|
||||
saxStream.on('cdata', handleSaxStreamEvent(function(data: any) {
|
||||
const n = currentNodeName();
|
||||
|
||||
if (noteResourceRecognition) {
|
||||
@@ -500,7 +566,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
}
|
||||
}));
|
||||
|
||||
saxStream.on('closetag', handleSaxStreamEvent(function(n) {
|
||||
saxStream.on('closetag', handleSaxStreamEvent(function(n: string) {
|
||||
nodes.pop();
|
||||
|
||||
if (n == 'note') {
|
||||
@@ -529,7 +595,7 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
note.longitude = noteAttributes.longitude;
|
||||
note.altitude = noteAttributes.altitude;
|
||||
note.author = noteAttributes.author ? noteAttributes.author.trim() : '';
|
||||
note.is_todo = noteAttributes['reminder-order'] !== '0' && !!noteAttributes['reminder-order'];
|
||||
note.is_todo = noteAttributes['reminder-order'] !== '0' && !!noteAttributes['reminder-order'] as any;
|
||||
note.todo_due = dateToTimestamp(noteAttributes['reminder-time'], 0);
|
||||
note.todo_completed = dateToTimestamp(noteAttributes['reminder-done-time'], 0);
|
||||
note.order = dateToTimestamp(noteAttributes['reminder-order'], 0);
|
||||
@@ -572,10 +638,11 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
saxStream.on('end', handleSaxStreamEvent(function() {
|
||||
// Wait till there is no more notes to process.
|
||||
const iid = shim.setInterval(() => {
|
||||
processNotes().then(allDone => {
|
||||
void processNotes().then(allDone => {
|
||||
if (allDone) {
|
||||
shim.clearTimeout(iid);
|
||||
resolve();
|
||||
if (needToDeleteFileToProcess) void shim.fsDriver().remove(fileToProcess);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
@@ -584,5 +651,3 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
stream.pipe(saxStream);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { importEnex };
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -41,45 +41,45 @@ locales['uk_UA'] = require('./uk_UA.json');
|
||||
locales['vi'] = require('./vi.json');
|
||||
locales['zh_CN'] = require('./zh_CN.json');
|
||||
locales['zh_TW'] = require('./zh_TW.json');
|
||||
stats['ar'] = {"percentDone":91};
|
||||
stats['ar'] = {"percentDone":99};
|
||||
stats['eu'] = {"percentDone":28};
|
||||
stats['bs_BA'] = {"percentDone":71};
|
||||
stats['bg_BG'] = {"percentDone":56};
|
||||
stats['ca'] = {"percentDone":95};
|
||||
stats['hr_HR'] = {"percentDone":96};
|
||||
stats['cs_CZ'] = {"percentDone":95};
|
||||
stats['bg_BG'] = {"percentDone":55};
|
||||
stats['ca'] = {"percentDone":99};
|
||||
stats['hr_HR'] = {"percentDone":95};
|
||||
stats['cs_CZ'] = {"percentDone":94};
|
||||
stats['da_DK'] = {"percentDone":99};
|
||||
stats['de_DE'] = {"percentDone":95};
|
||||
stats['de_DE'] = {"percentDone":99};
|
||||
stats['et_EE'] = {"percentDone":54};
|
||||
stats['en_GB'] = {"percentDone":100};
|
||||
stats['en_US'] = {"percentDone":100};
|
||||
stats['es_ES'] = {"percentDone":95};
|
||||
stats['eo'] = {"percentDone":31};
|
||||
stats['fi_FI'] = {"percentDone":90};
|
||||
stats['fi_FI'] = {"percentDone":99};
|
||||
stats['fr_FR'] = {"percentDone":96};
|
||||
stats['gl_ES'] = {"percentDone":36};
|
||||
stats['id_ID'] = {"percentDone":96};
|
||||
stats['it_IT'] = {"percentDone":96};
|
||||
stats['hu_HU'] = {"percentDone":84};
|
||||
stats['nl_BE'] = {"percentDone":87};
|
||||
stats['id_ID'] = {"percentDone":95};
|
||||
stats['it_IT'] = {"percentDone":95};
|
||||
stats['hu_HU'] = {"percentDone":83};
|
||||
stats['nl_BE'] = {"percentDone":86};
|
||||
stats['nl_NL'] = {"percentDone":90};
|
||||
stats['nb_NO'] = {"percentDone":96};
|
||||
stats['fa'] = {"percentDone":67};
|
||||
stats['pl_PL'] = {"percentDone":90};
|
||||
stats['pl_PL'] = {"percentDone":89};
|
||||
stats['pt_BR'] = {"percentDone":96};
|
||||
stats['pt_PT'] = {"percentDone":90};
|
||||
stats['ro'] = {"percentDone":63};
|
||||
stats['sl_SI'] = {"percentDone":91};
|
||||
stats['sv'] = {"percentDone":96};
|
||||
stats['pt_PT'] = {"percentDone":89};
|
||||
stats['ro'] = {"percentDone":62};
|
||||
stats['sl_SI'] = {"percentDone":90};
|
||||
stats['sv'] = {"percentDone":99};
|
||||
stats['th_TH'] = {"percentDone":42};
|
||||
stats['vi'] = {"percentDone":96};
|
||||
stats['tr_TR'] = {"percentDone":99};
|
||||
stats['uk_UA'] = {"percentDone":89};
|
||||
stats['el_GR'] = {"percentDone":92};
|
||||
stats['ru_RU'] = {"percentDone":89};
|
||||
stats['sr_RS'] = {"percentDone":81};
|
||||
stats['sr_RS'] = {"percentDone":80};
|
||||
stats['zh_CN'] = {"percentDone":99};
|
||||
stats['zh_TW'] = {"percentDone":94};
|
||||
stats['zh_TW'] = {"percentDone":95};
|
||||
stats['ja_JP'] = {"percentDone":95};
|
||||
stats['ko'] = {"percentDone":95};
|
||||
stats['ko'] = {"percentDone":94};
|
||||
module.exports = { locales: locales, stats: stats };
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
packages/lib/package-lock.json
generated
1
packages/lib/package-lock.json
generated
@@ -15703,6 +15703,7 @@
|
||||
},
|
||||
"uslug": {
|
||||
"version": "git+ssh://git@github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d",
|
||||
"integrity": "sha512-6zzxOsQp+hbOW4zeplEUhKXnBzYIrqYAVlPepBFz/u5q2OulN7tCmBKyWEzDxaiZOLYnUCTViDLazNoq1J6ciA==",
|
||||
"from": "uslug@git+https://github.com/laurent22/uslug.git#emoji-support",
|
||||
"requires": {
|
||||
"node-emoji": "^1.10.0",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { ImportExportResult } from './types';
|
||||
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
||||
import Folder from '../../models/Folder';
|
||||
import importEnex from '../../import-enex';
|
||||
const { filename } = require('../../path-utils');
|
||||
|
||||
export default class InteropService_Importer_EnexToHtml extends InteropService_Importer_Base {
|
||||
async exec(result: ImportExportResult): Promise<ImportExportResult> {
|
||||
const { importEnex } = require('../../import-enex');
|
||||
|
||||
public async exec(result: ImportExportResult): Promise<ImportExportResult> {
|
||||
let folder = this.options_.destinationFolder;
|
||||
|
||||
if (!folder) {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { ImportExportResult } from './types';
|
||||
|
||||
import importEnex from '../../import-enex';
|
||||
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
||||
import Folder from '../../models/Folder';
|
||||
const { filename } = require('../../path-utils');
|
||||
|
||||
export default class InteropService_Importer_EnexToMd extends InteropService_Importer_Base {
|
||||
async exec(result: ImportExportResult) {
|
||||
const { importEnex } = require('../../import-enex');
|
||||
|
||||
public async exec(result: ImportExportResult) {
|
||||
let folder = this.options_.destinationFolder;
|
||||
|
||||
if (!folder) {
|
||||
|
||||
@@ -16,6 +16,36 @@ interface Release {
|
||||
assets: ReleaseAsset[];
|
||||
}
|
||||
|
||||
const findWorkingGitHubUrl = async (defaultContentUrl: string): Promise<string> => {
|
||||
// From: https://github.com/laurent22/joplin/issues/5161#issuecomment-921642721
|
||||
|
||||
const mirrorUrls = [
|
||||
defaultContentUrl,
|
||||
'https://cdn.staticaly.com/gh/joplin/plugins/master',
|
||||
'https://ghproxy.com/https://raw.githubusercontent.com/joplin/plugins/master',
|
||||
'https://cdn.jsdelivr.net/gh/joplin/plugins@master',
|
||||
'https://raw.fastgit.org/joplin/plugins/master',
|
||||
];
|
||||
|
||||
for (const mirrorUrl of mirrorUrls) {
|
||||
try {
|
||||
// We try to fetch .gitignore, which is smaller than the whole manifest
|
||||
await fetch(`${mirrorUrl}/.gitignore`);
|
||||
} catch (error) {
|
||||
logger.info(`findWorkingMirror: Could not connect to ${mirrorUrl}:`, error);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info(`findWorkingMirror: Using: ${mirrorUrl}`);
|
||||
|
||||
return mirrorUrl;
|
||||
}
|
||||
|
||||
logger.info('findWorkingMirror: Could not find any working GitHub URL');
|
||||
|
||||
return defaultContentUrl;
|
||||
};
|
||||
|
||||
export default class RepositoryApi {
|
||||
|
||||
// As a base URL, this class can support either a remote repository or a
|
||||
@@ -29,6 +59,9 @@ export default class RepositoryApi {
|
||||
private tempDir_: string;
|
||||
private release_: Release = null;
|
||||
private manifests_: PluginManifest[] = null;
|
||||
private githubApiUrl_: string;
|
||||
private contentBaseUrl_: string;
|
||||
private isUsingDefaultContentUrl_: boolean = true;
|
||||
|
||||
public constructor(baseUrl: string, tempDir: string) {
|
||||
this.baseUrl_ = baseUrl;
|
||||
@@ -36,6 +69,14 @@ export default class RepositoryApi {
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
// https://github.com/joplin/plugins
|
||||
// https://api.github.com/repos/joplin/plugins/releases
|
||||
this.githubApiUrl_ = this.baseUrl_.replace(/^(https:\/\/)(github\.com\/)(.*)$/, '$1api.$2repos/$3');
|
||||
const defaultContentBaseUrl = `${this.baseUrl_.replace(/github\.com/, 'raw.githubusercontent.com')}/master`;
|
||||
this.contentBaseUrl_ = await findWorkingGitHubUrl(defaultContentBaseUrl);
|
||||
|
||||
this.isUsingDefaultContentUrl_ = this.contentBaseUrl_ === defaultContentBaseUrl;
|
||||
|
||||
await this.loadManifests();
|
||||
await this.loadRelease();
|
||||
}
|
||||
@@ -45,18 +86,33 @@ export default class RepositoryApi {
|
||||
try {
|
||||
const manifests = JSON.parse(manifestsText);
|
||||
if (!manifests) throw new Error('Invalid or missing JSON');
|
||||
|
||||
this.manifests_ = Object.keys(manifests).map(id => {
|
||||
return manifests[id];
|
||||
const m: PluginManifest = manifests[id];
|
||||
// If we don't control the repository, we can't recommend
|
||||
// anything on it since it could have been modified.
|
||||
if (!this.isUsingDefaultContentUrl) m._recommended = false;
|
||||
return m;
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Could not parse JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public get isUsingDefaultContentUrl() {
|
||||
return this.isUsingDefaultContentUrl_;
|
||||
}
|
||||
|
||||
private get githubApiUrl(): string {
|
||||
// https://github.com/joplin/plugins
|
||||
// https://api.github.com/repos/joplin/plugins/releases
|
||||
return this.baseUrl_.replace(/^(https:\/\/)(github\.com\/)(.*)$/, '$1api.$2repos/$3');
|
||||
return this.githubApiUrl_;
|
||||
}
|
||||
|
||||
public get contentBaseUrl(): string {
|
||||
if (this.isLocalRepo) {
|
||||
return this.baseUrl_;
|
||||
} else {
|
||||
return this.contentBaseUrl_;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadRelease() {
|
||||
@@ -78,14 +134,6 @@ export default class RepositoryApi {
|
||||
return this.baseUrl_.indexOf('http') !== 0;
|
||||
}
|
||||
|
||||
private get contentBaseUrl(): string {
|
||||
if (this.isLocalRepo) {
|
||||
return this.baseUrl_;
|
||||
} else {
|
||||
return `${this.baseUrl_.replace(/github\.com/, 'raw.githubusercontent.com')}/master`;
|
||||
}
|
||||
}
|
||||
|
||||
private assetFileUrl(pluginId: string): string {
|
||||
if (this.release_) {
|
||||
const asset = this.release_.assets.find(asset => {
|
||||
|
||||
@@ -386,6 +386,34 @@ describe('services_SearchFilter', function() {
|
||||
expect(ids(rows).sort()).toEqual(ids(notes0).concat(ids(notes00).concat(ids(notes1))).sort());
|
||||
}));
|
||||
|
||||
it('should support filtering and search term', (async () => {
|
||||
const notebook1 = await Folder.save({ title: 'notebook1' });
|
||||
const notebook2 = await Folder.save({ title: 'notebook2' });
|
||||
const note1 = await Note.save({ title: 'note1', body: 'abcdefg', parent_id: notebook1.id });
|
||||
await Note.save({ title: 'note2', body: 'body', parent_id: notebook1.id });
|
||||
await Note.save({ title: 'note3', body: 'abcdefg', parent_id: notebook2.id });
|
||||
await Note.save({ title: 'note4', body: 'body', parent_id: notebook2.id });
|
||||
|
||||
await engine.syncTables();
|
||||
|
||||
const testCases = [
|
||||
{ searchQuery: 'notebook:notebook1 abcdefg' },
|
||||
{ searchQuery: 'notebook:notebook1 "abcdefg"' },
|
||||
{ searchQuery: 'notebook:"notebook1" abcdefg' },
|
||||
{ searchQuery: 'notebook:"notebook1" "abcdefg"' },
|
||||
{ searchQuery: 'notebook:"notebook1" -tag:* "abcdefg"' },
|
||||
{ searchQuery: 'notebook:"notebook1" -tag:* abcdefg' },
|
||||
{ searchQuery: 'notebook:"notebook1" -tag:"*" abcdefg' },
|
||||
{ searchQuery: 'notebook:"notebook1" -tag:"*" "abcdefg"' },
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const rows = await engine.search(testCase.searchQuery, { searchType });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(ids(rows)).toContain(note1.id);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should support filtering by created date', (async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'I made this on', body: 'May 20 2020', user_created_time: Date.parse('2020-05-20') });
|
||||
|
||||
@@ -36,6 +36,7 @@ const getTerms = (query: string, validFilters: Set<string>): Term[] => {
|
||||
if (inQuote) {
|
||||
terms.push(makeTerm(currentCol, currentTerm));
|
||||
currentTerm = '';
|
||||
currentCol = '_';
|
||||
inQuote = false;
|
||||
} else {
|
||||
inQuote = true;
|
||||
|
||||
@@ -9,6 +9,7 @@ function formatCssSize(v: any): string {
|
||||
|
||||
export interface Options {
|
||||
contentMaxWidth?: number;
|
||||
contentMaxWidthTarget?: string;
|
||||
}
|
||||
|
||||
export default function(theme: any, options: Options = null) {
|
||||
@@ -21,8 +22,9 @@ export default function(theme: any, options: Options = null) {
|
||||
|
||||
const fontFamily = '\'Avenir\', \'Arial\', sans-serif';
|
||||
|
||||
const maxWidthTarget = options.contentMaxWidthTarget ? options.contentMaxWidthTarget : '#rendered-md';
|
||||
const maxWidthCss = options.contentMaxWidth ? `
|
||||
#rendered-md {
|
||||
${maxWidthTarget} {
|
||||
max-width: ${options.contentMaxWidth}px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
1
packages/renderer/package-lock.json
generated
1
packages/renderer/package-lock.json
generated
@@ -14935,6 +14935,7 @@
|
||||
},
|
||||
"uslug": {
|
||||
"version": "git+ssh://git@github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d",
|
||||
"integrity": "sha512-6zzxOsQp+hbOW4zeplEUhKXnBzYIrqYAVlPepBFz/u5q2OulN7tCmBKyWEzDxaiZOLYnUCTViDLazNoq1J6ciA==",
|
||||
"from": "uslug@git+https://github.com/laurent22/uslug.git#emoji-support",
|
||||
"requires": {
|
||||
"node-emoji": "^1.10.0",
|
||||
|
||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.4.7",
|
||||
"version": "2.4.9",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@joplin/server",
|
||||
"version": "2.4.7",
|
||||
"version": "2.4.9",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"@koa/cors": "^3.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.4.7",
|
||||
"version": "2.4.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
|
||||
|
||||
@@ -45,6 +45,10 @@ table.table th .sort-button i {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
table.table tr.is-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { initializeJoplinUtils } from './utils/joplinUtils';
|
||||
import startServices from './utils/startServices';
|
||||
import { credentialFile } from './utils/testing/testUtils';
|
||||
import apiVersionHandler from './middleware/apiVersionHandler';
|
||||
import clickJackingHandler from './middleware/clickJackingHandler';
|
||||
|
||||
const cors = require('@koa/cors');
|
||||
const nodeEnvFile = require('node-env-file');
|
||||
@@ -90,14 +91,6 @@ async function main() {
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
// app.use(async function responseTime(ctx:AppContext, next:Function) {
|
||||
// const start = Date.now();
|
||||
// await next();
|
||||
// const ms = Date.now() - start;
|
||||
// console.info('Response time', ms)
|
||||
// //ctx.set('X-Response-Time', `${ms}ms`);
|
||||
// });
|
||||
|
||||
// Note: the order of middlewares is important. For example, ownerHandler
|
||||
// loads the user, which is then used by notificationHandler. And finally
|
||||
// routeHandler uses data from both previous middlewares. It would be good to
|
||||
@@ -179,6 +172,7 @@ async function main() {
|
||||
app.use(apiVersionHandler);
|
||||
app.use(ownerHandler);
|
||||
app.use(notificationHandler);
|
||||
app.use(clickJackingHandler);
|
||||
app.use(routeHandler);
|
||||
|
||||
await initConfig(env, envVariables);
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface EnvVariables {
|
||||
APP_BASE_URL?: string;
|
||||
USER_CONTENT_BASE_URL?: string;
|
||||
API_BASE_URL?: string;
|
||||
JOPLINAPP_BASE_URL?: string;
|
||||
|
||||
APP_PORT?: string;
|
||||
DB_CLIENT?: string;
|
||||
@@ -59,11 +60,15 @@ export function runningInDocker(): boolean {
|
||||
return runningInDocker_;
|
||||
}
|
||||
|
||||
function envParseBool(s: string): boolean {
|
||||
function envReadString(s: string, defaultValue: string = ''): string {
|
||||
return s === undefined || s === null ? defaultValue : s;
|
||||
}
|
||||
|
||||
function envReadBool(s: string): boolean {
|
||||
return s === '1';
|
||||
}
|
||||
|
||||
function envParseInt(s: string, defaultValue: number = null): number {
|
||||
function envReadInt(s: string, defaultValue: number = null): number {
|
||||
if (!s) return defaultValue === null ? 0 : defaultValue;
|
||||
const output = Number(s);
|
||||
if (isNaN(output)) throw new Error(`Invalid number: ${s}`);
|
||||
@@ -89,8 +94,8 @@ function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): Dat
|
||||
const baseConfig: DatabaseConfig = {
|
||||
client: DatabaseConfigClient.Null,
|
||||
name: '',
|
||||
slowQueryLogEnabled: envParseBool(env.SLOW_QUERY_LOG_ENABLED),
|
||||
slowQueryLogMinDuration: envParseInt(env.SLOW_QUERY_LOG_MIN_DURATION, 10000),
|
||||
slowQueryLogEnabled: envReadBool(env.SLOW_QUERY_LOG_ENABLED),
|
||||
slowQueryLogMinDuration: envReadInt(env.SLOW_QUERY_LOG_MIN_DURATION, 10000),
|
||||
};
|
||||
|
||||
if (env.DB_CLIENT === 'pg') {
|
||||
@@ -187,6 +192,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
|
||||
showErrorStackTraces: (env.ERROR_STACK_TRACES === undefined && envType === Env.Dev) || env.ERROR_STACK_TRACES === '1',
|
||||
apiBaseUrl,
|
||||
userContentBaseUrl: env.USER_CONTENT_BASE_URL ? env.USER_CONTENT_BASE_URL : baseUrl,
|
||||
joplinAppBaseUrl: envReadString(env.JOPLINAPP_BASE_URL, 'https://joplinapp.org'),
|
||||
signupEnabled: env.SIGNUP_ENABLED === '1',
|
||||
termsEnabled: env.TERMS_ENABLED === '1',
|
||||
accountTypesEnabled: env.ACCOUNT_TYPES_ENABLED === '1',
|
||||
|
||||
@@ -15,6 +15,14 @@ require('pg').types.setTypeParser(20, function(val: any) {
|
||||
return parseInt(val, 10);
|
||||
});
|
||||
|
||||
// Also need this to get integers for count() queries.
|
||||
// https://knexjs.org/#Builder-count
|
||||
declare module 'knex/types/result' {
|
||||
interface Registry {
|
||||
Count: number;
|
||||
}
|
||||
}
|
||||
|
||||
const logger = Logger.create('db');
|
||||
|
||||
// To prevent error "SQLITE_ERROR: too many SQL variables", SQL statements with
|
||||
@@ -122,8 +130,16 @@ export function setupSlowQueryLog(connection: DbConnection, slowQueryLogMinDurat
|
||||
|
||||
const queryInfos: Record<any, QueryInfo> = {};
|
||||
|
||||
// These queries do not return a response, so "query-response" is not
|
||||
// called.
|
||||
const ignoredQueries = /^BEGIN|SAVEPOINT|RELEASE SAVEPOINT|COMMIT|ROLLBACK/gi;
|
||||
|
||||
connection.on('query', (data) => {
|
||||
const timeoutId = makeSlowQueryHandler(slowQueryLogMinDuration, connection, data.sql, data.bindings);
|
||||
const sql: string = data.sql;
|
||||
|
||||
if (!sql || sql.match(ignoredQueries)) return;
|
||||
|
||||
const timeoutId = makeSlowQueryHandler(slowQueryLogMinDuration, connection, sql, data.bindings);
|
||||
|
||||
queryInfos[data.__knexQueryUid] = {
|
||||
timeoutId,
|
||||
|
||||
8
packages/server/src/middleware/clickJackingHandler.ts
Normal file
8
packages/server/src/middleware/clickJackingHandler.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { AppContext, KoaNext } from '../utils/types';
|
||||
|
||||
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html
|
||||
ctx.response.set('Content-Security-Policy', 'frame-ancestors \'none\'');
|
||||
ctx.response.set('X-Frame-Options', 'DENY');
|
||||
return next();
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { defaultAdminEmail, defaultAdminPassword } from '../db';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import * as MarkdownIt from 'markdown-it';
|
||||
import config from '../config';
|
||||
import { NotificationKey } from '../models/NotificationModel';
|
||||
import { profileUrl } from '../utils/urlUtils';
|
||||
|
||||
@@ -29,17 +28,24 @@ async function handleChangeAdminPasswordNotification(ctx: AppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSqliteInProdNotification(ctx: AppContext) {
|
||||
if (!ctx.joplin.owner.is_admin) return;
|
||||
// async function handleSqliteInProdNotification(ctx: AppContext) {
|
||||
// if (!ctx.joplin.owner.is_admin) return;
|
||||
|
||||
const notificationModel = ctx.joplin.models.notification();
|
||||
// const notificationModel = ctx.joplin.models.notification();
|
||||
|
||||
if (config().database.client === 'sqlite3' && ctx.joplin.env === 'prod') {
|
||||
await notificationModel.add(
|
||||
ctx.joplin.owner.id,
|
||||
NotificationKey.UsingSqliteInProd
|
||||
);
|
||||
}
|
||||
// if (config().database.client === 'sqlite3' && ctx.joplin.env === 'prod') {
|
||||
// await notificationModel.add(
|
||||
// ctx.joplin.owner.id,
|
||||
// NotificationKey.UsingSqliteInProd
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
function levelClassName(level: NotificationLevel): string {
|
||||
if (level === NotificationLevel.Important) return 'is-warning';
|
||||
if (level === NotificationLevel.Normal) return 'is-info';
|
||||
if (level === NotificationLevel.Error) return 'is-danger';
|
||||
throw new Error(`Unknown level: ${level}`);
|
||||
}
|
||||
|
||||
async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> {
|
||||
@@ -52,7 +58,7 @@ async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[
|
||||
views.push({
|
||||
id: n.id,
|
||||
messageHtml: markdownIt.render(n.message),
|
||||
level: n.level === NotificationLevel.Important ? 'warning' : 'info',
|
||||
levelClassName: levelClassName(n.level),
|
||||
closeUrl: notificationModel.closeUrl(n.id),
|
||||
});
|
||||
}
|
||||
@@ -71,7 +77,7 @@ export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||
if (!ctx.joplin.owner) return next();
|
||||
|
||||
await handleChangeAdminPasswordNotification(ctx);
|
||||
await handleSqliteInProdNotification(ctx);
|
||||
// await handleSqliteInProdNotification(ctx);
|
||||
ctx.joplin.notifications = await makeNotificationViews(ctx);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { AppContext, KoaNext } from '../utils/types';
|
||||
import { contextSessionId } from '../utils/requestUtils';
|
||||
import { ErrorForbidden } from '../utils/errors';
|
||||
import { cookieSet } from '../utils/cookies';
|
||||
|
||||
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||
const sessionId = contextSessionId(ctx, false);
|
||||
const owner = sessionId ? await ctx.joplin.models.session().sessionUser(sessionId) : null;
|
||||
if (owner && !owner.enabled) throw new ErrorForbidden('This user account is disabled. Please contact support.');
|
||||
if (owner && !owner.enabled) {
|
||||
cookieSet(ctx, 'sessionId', ''); // Clear cookie, otherwise the user cannot login at all anymore
|
||||
throw new ErrorForbidden('This user account is disabled. Please contact support.');
|
||||
}
|
||||
ctx.joplin.owner = owner;
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { AppContext, Env } from '../utils/types';
|
||||
import { isView, View } from '../services/MustacheService';
|
||||
import config from '../config';
|
||||
import { userIp } from '../utils/requestUtils';
|
||||
import { createCsrfTag } from '../utils/csrf';
|
||||
import { getImpersonatorAdminSessionId } from '../routes/index/utils/users/impersonate';
|
||||
|
||||
export default async function(ctx: AppContext) {
|
||||
const requestStartTime = Date.now();
|
||||
@@ -13,6 +15,8 @@ export default async function(ctx: AppContext) {
|
||||
if (responseObject instanceof Response) {
|
||||
ctx.response = responseObject.response;
|
||||
} else if (isView(responseObject)) {
|
||||
const impersonatorAdminSessionId = getImpersonatorAdminSessionId(ctx);
|
||||
|
||||
const view = responseObject as View;
|
||||
ctx.response.status = view?.content?.error ? view?.content?.error?.httpCode || 500 : 200;
|
||||
ctx.response.body = await ctx.joplin.services.mustache.renderView(view, {
|
||||
@@ -20,6 +24,8 @@ export default async function(ctx: AppContext) {
|
||||
hasNotifications: !!ctx.joplin.notifications && !!ctx.joplin.notifications.length,
|
||||
owner: ctx.joplin.owner,
|
||||
supportEmail: config().supportEmail,
|
||||
impersonatorAdminSessionId,
|
||||
csrfTag: impersonatorAdminSessionId ? await createCsrfTag(ctx, false) : null,
|
||||
});
|
||||
} else {
|
||||
ctx.response.status = 200;
|
||||
|
||||
@@ -11,6 +11,8 @@ import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const logger = Logger.create('BaseModel');
|
||||
|
||||
type SavePoint = string;
|
||||
|
||||
export interface SaveOptions {
|
||||
isNew?: boolean;
|
||||
skipValidation?: boolean;
|
||||
@@ -49,6 +51,7 @@ export default abstract class BaseModel<T> {
|
||||
private modelFactory_: Function;
|
||||
private static eventEmitter_: EventEmitter = null;
|
||||
private config_: Config;
|
||||
private savePoints_: SavePoint[] = [];
|
||||
|
||||
public constructor(db: DbConnection, modelFactory: Function, config: Config) {
|
||||
this.db_ = db;
|
||||
@@ -289,6 +292,25 @@ export default abstract class BaseModel<T> {
|
||||
return this.db(this.tableName).select(options.fields || this.defaultFields).whereIn('id', ids);
|
||||
}
|
||||
|
||||
public async setSavePoint(): Promise<SavePoint> {
|
||||
const name = `sp_${uuidgen()}`;
|
||||
await this.db.raw(`SAVEPOINT ${name}`);
|
||||
this.savePoints_.push(name);
|
||||
return name;
|
||||
}
|
||||
|
||||
public async rollbackSavePoint(savePoint: SavePoint) {
|
||||
const last = this.savePoints_.pop();
|
||||
if (last !== savePoint) throw new Error('Rollback save point does not match');
|
||||
await this.db.raw(`ROLLBACK TO SAVEPOINT ${savePoint}`);
|
||||
}
|
||||
|
||||
public async releaseSavePoint(savePoint: SavePoint) {
|
||||
const last = this.savePoints_.pop();
|
||||
if (last !== savePoint) throw new Error('Rollback save point does not match');
|
||||
await this.db.raw(`RELEASE SAVEPOINT ${savePoint}`);
|
||||
}
|
||||
|
||||
public async exists(id: string): Promise<boolean> {
|
||||
const o = await this.load(id, { fields: ['id'] });
|
||||
return !!o;
|
||||
@@ -314,7 +336,7 @@ export default abstract class BaseModel<T> {
|
||||
}
|
||||
|
||||
const deletedCount = await query.del();
|
||||
if (!options.allowNoOp && deletedCount !== ids.length) throw new Error(`${ids.length} row(s) should have been deleted but ${deletedCount} row(s) were deleted`);
|
||||
if (!options.allowNoOp && deletedCount !== ids.length) throw new Error(`${ids.length} row(s) should have been deleted but ${deletedCount} row(s) were deleted. ID: ${id}`);
|
||||
}, 'BaseModel::delete');
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +196,34 @@ describe('ItemModel', function() {
|
||||
expect((await models().user().load(user3.id)).total_item_size).toBe(totalSize3);
|
||||
});
|
||||
|
||||
test('should update total size when an item is deleted', async function() {
|
||||
const { user: user1 } = await createUserAndSession(1);
|
||||
|
||||
await createItemTree3(user1.id, '', '', [
|
||||
{
|
||||
id: '000000000000000000000000000000F1',
|
||||
children: [
|
||||
{
|
||||
id: '00000000000000000000000000000001',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const folder1 = await models().item().loadByJopId(user1.id, '000000000000000000000000000000F1');
|
||||
const note1 = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');
|
||||
|
||||
await models().item().updateTotalSizes();
|
||||
|
||||
expect((await models().user().load(user1.id)).total_item_size).toBe(folder1.content_size + note1.content_size);
|
||||
|
||||
await models().item().delete(note1.id);
|
||||
|
||||
await models().item().updateTotalSizes();
|
||||
|
||||
expect((await models().user().load(user1.id)).total_item_size).toBe(folder1.content_size);
|
||||
});
|
||||
|
||||
test('should include shared items in total size calculation', async function() {
|
||||
const { user: user1, session: session1 } = await createUserAndSession(1);
|
||||
const { user: user2, session: session2 } = await createUserAndSession(2);
|
||||
|
||||
@@ -106,13 +106,18 @@ export default class ItemModel extends BaseModel<Item> {
|
||||
return path.replace(extractNameRegex, '$1');
|
||||
}
|
||||
|
||||
public async byShareId(shareId: Uuid, options: LoadOptions = {}): Promise<Item[]> {
|
||||
public byShareIdQuery(shareId: Uuid, options: LoadOptions = {}): Knex.QueryBuilder {
|
||||
return this
|
||||
.db('items')
|
||||
.select(this.selectFields(options, null, 'items'))
|
||||
.where('jop_share_id', '=', shareId);
|
||||
}
|
||||
|
||||
public async byShareId(shareId: Uuid, options: LoadOptions = {}): Promise<Item[]> {
|
||||
const query = this.byShareIdQuery(shareId, options);
|
||||
return await query;
|
||||
}
|
||||
|
||||
public async loadByJopIds(userId: Uuid | Uuid[], jopIds: string[], options: LoadOptions = {}): Promise<Item[]> {
|
||||
if (!jopIds.length) return [];
|
||||
|
||||
@@ -623,7 +628,11 @@ export default class ItemModel extends BaseModel<Item> {
|
||||
} else {
|
||||
const itemIds: Uuid[] = unique(changes.map(c => c.item_id));
|
||||
const userItems: UserItem[] = await this.db('user_items').select('user_id').whereIn('item_id', itemIds);
|
||||
const userIds: Uuid[] = unique(userItems.map(u => u.user_id));
|
||||
const userIds: Uuid[] = unique(
|
||||
userItems
|
||||
.map(u => u.user_id)
|
||||
.concat(changes.map(c => c.user_id))
|
||||
);
|
||||
|
||||
const totalSizes: TotalSizeRow[] = [];
|
||||
for (const userId of userIds) {
|
||||
|
||||
@@ -8,7 +8,7 @@ export enum ValueType {
|
||||
|
||||
type Value = number | string;
|
||||
|
||||
export default class NotificationModel extends BaseModel<KeyValue> {
|
||||
export default class KeyValueModel extends BaseModel<KeyValue> {
|
||||
|
||||
protected get tableName(): string {
|
||||
return 'key_values';
|
||||
@@ -61,4 +61,8 @@ export default class NotificationModel extends BaseModel<KeyValue> {
|
||||
await this.db(this.tableName).where('key', '=', key).delete();
|
||||
}
|
||||
|
||||
public async delete(_id: string | string[] | number | number[], _options: any = {}): Promise<void> {
|
||||
throw new Error('Call ::deleteValue()');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Notification, NotificationLevel, Uuid } from '../services/database/types';
|
||||
import { ErrorUnprocessableEntity } from '../utils/errors';
|
||||
import uuidgen from '../utils/uuidgen';
|
||||
import BaseModel, { ValidateOptions } from './BaseModel';
|
||||
|
||||
export enum NotificationKey {
|
||||
Any = 'any',
|
||||
ConfirmEmail = 'confirmEmail',
|
||||
PasswordSet = 'passwordSet',
|
||||
EmailConfirmed = 'emailConfirmed',
|
||||
ChangeAdminPassword = 'change_admin_password',
|
||||
UsingSqliteInProd = 'using_sqlite_in_prod',
|
||||
// UsingSqliteInProd = 'using_sqlite_in_prod',
|
||||
UpgradedToPro = 'upgraded_to_pro',
|
||||
}
|
||||
|
||||
@@ -28,9 +30,6 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
}
|
||||
|
||||
public async add(userId: Uuid, key: NotificationKey, level: NotificationLevel = null, message: string = null): Promise<Notification> {
|
||||
const n: Notification = await this.loadByKey(userId, key);
|
||||
if (n) return n;
|
||||
|
||||
const notificationTypes: Record<string, NotificationType> = {
|
||||
[NotificationKey.ConfirmEmail]: {
|
||||
level: NotificationLevel.Normal,
|
||||
@@ -44,16 +43,23 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
level: NotificationLevel.Normal,
|
||||
message: `Welcome to ${this.appName}! Your password has been set successfully.`,
|
||||
},
|
||||
[NotificationKey.UsingSqliteInProd]: {
|
||||
level: NotificationLevel.Important,
|
||||
message: 'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.',
|
||||
},
|
||||
// [NotificationKey.UsingSqliteInProd]: {
|
||||
// level: NotificationLevel.Important,
|
||||
// message: 'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.',
|
||||
// },
|
||||
[NotificationKey.UpgradedToPro]: {
|
||||
level: NotificationLevel.Normal,
|
||||
message: 'Thank you! Your account has been successfully upgraded to Pro.',
|
||||
},
|
||||
[NotificationKey.Any]: {
|
||||
level: NotificationLevel.Normal,
|
||||
message: '',
|
||||
},
|
||||
};
|
||||
|
||||
const n: Notification = await this.loadUnreadByKey(userId, key);
|
||||
if (n) return n;
|
||||
|
||||
const type = notificationTypes[key];
|
||||
|
||||
if (level === null) {
|
||||
@@ -72,7 +78,9 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
}
|
||||
}
|
||||
|
||||
return this.save({ key, message, level, owner_id: userId });
|
||||
const actualKey = key === NotificationKey.Any ? `any_${uuidgen()}` : key;
|
||||
|
||||
return this.save({ key: actualKey, message, level, owner_id: userId });
|
||||
}
|
||||
|
||||
public async markAsRead(userId: Uuid, key: NotificationKey): Promise<void> {
|
||||
@@ -93,6 +101,15 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
.first();
|
||||
}
|
||||
|
||||
public loadUnreadByKey(userId: Uuid, key: NotificationKey): Promise<Notification> {
|
||||
return this.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
.where('key', '=', key)
|
||||
.andWhere('read', '=', 0)
|
||||
.andWhere('owner_id', '=', userId)
|
||||
.first();
|
||||
}
|
||||
|
||||
public allUnreadByUserId(userId: Uuid): Promise<Notification[]> {
|
||||
return this.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user