You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-30 20:39:46 +02:00
Compare commits
63 Commits
android-v2
...
server-v2.
Author | SHA1 | Date | |
---|---|---|---|
|
21f9189000 | ||
|
d92032e634 | ||
|
5986710fc0 | ||
|
4d1e0cc21b | ||
|
7b42211581 | ||
|
2fc7bcec06 | ||
|
3e3d01d93e | ||
|
f634a1c731 | ||
|
1fe91b4808 | ||
|
d20c48855c | ||
|
38d310c0ad | ||
|
c06ca87573 | ||
|
d50d940f3c | ||
|
bde74d1f97 | ||
|
11f7915a54 | ||
|
4501ecff3b | ||
|
387ba2c50f | ||
|
7e085ef0bc | ||
|
cfb1f11956 | ||
|
6668a52478 | ||
|
92230fce72 | ||
|
f2269e9820 | ||
|
fbef66b65a | ||
|
8795da83d2 | ||
|
79f9b93b58 | ||
|
5fd4c19b8d | ||
|
7621dde8e7 | ||
|
02a797abe9 | ||
|
41d4734bd3 | ||
|
0c91e2c947 | ||
|
2d06fd9d13 | ||
|
5733017637 | ||
|
b1e1db7831 | ||
|
c5c03ab04e | ||
|
5cecfde085 | ||
|
0c701f59c7 | ||
|
05a4affd5a | ||
|
84ff840c15 | ||
|
4989d402a4 | ||
|
3ac25104c3 | ||
|
2a73010c9d | ||
|
0f005c5039 | ||
|
0402fa624d | ||
|
a98c323bf3 | ||
|
d975d8d626 | ||
|
19c694760f | ||
|
9236f68016 | ||
|
a052983fb6 | ||
|
089c6afd1a | ||
|
4b7f807b5b | ||
|
d4ccf06f98 | ||
|
2d9980dd36 | ||
|
db4e08b757 | ||
|
10cef6e146 | ||
|
d33df54969 | ||
|
90feb65b44 | ||
|
cbe260eed2 | ||
|
2f7801a267 | ||
|
d58f62ca9d | ||
|
d6f272c74f | ||
|
cd55a9a40f | ||
|
5cb54a57ac | ||
|
0e0c1d8395 |
@@ -38,6 +38,10 @@ packages/app-clipper/popup/config/webpack.config.js
|
||||
packages/app-clipper/popup/node_modules
|
||||
packages/app-clipper/popup/scripts/build.js
|
||||
packages/app-desktop/build/
|
||||
packages/app-desktop/test-results/
|
||||
packages/app-desktop/playwright-report/
|
||||
packages/app-desktop/playwright/.cache/
|
||||
packages/app-desktop/integration-tests/test-profile/
|
||||
packages/app-desktop/dist
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/supportedLocales.js
|
||||
@@ -371,6 +375,13 @@ packages/app-desktop/gui/style/StyledTextInput.js
|
||||
packages/app-desktop/gui/utils/NoteListUtils.js
|
||||
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/bridge.js
|
||||
packages/app-desktop/services/commands/stateToWhenClauseContext.js
|
||||
@@ -575,6 +586,7 @@ packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/index.js
|
||||
@@ -955,6 +967,7 @@ packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.js
|
||||
packages/tools/build-translation.js
|
||||
packages/tools/build-welcome.js
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.js
|
||||
|
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: contributor-assistant/github-action@v2.3.0
|
||||
uses: contributor-assistant/github-action@v2.3.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
|
3
.github/workflows/github-actions-main.yml
vendored
3
.github/workflows/github-actions-main.yml
vendored
@@ -55,6 +55,9 @@ jobs:
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
sudo apt-get install -y translate-toolkit
|
||||
sudo apt-get install -y rsync
|
||||
# Provides a virtual display on Linux. Used for Playwright integration
|
||||
# testing.
|
||||
sudo apt-get install -y xvfb
|
||||
|
||||
- name: Install Docker Engine
|
||||
# if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')
|
||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -357,6 +357,13 @@ packages/app-desktop/gui/style/StyledTextInput.js
|
||||
packages/app-desktop/gui/utils/NoteListUtils.js
|
||||
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/bridge.js
|
||||
packages/app-desktop/services/commands/stateToWhenClauseContext.js
|
||||
@@ -561,6 +568,7 @@ packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/index.js
|
||||
@@ -941,6 +949,7 @@ packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.js
|
||||
packages/tools/build-translation.js
|
||||
packages/tools/build-welcome.js
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.js
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-share-permissions.png" alt=""></p>
|
||||
<h2>Email to Note<a name="email-to-note" href="#email-to-note" class="heading-anchor">🔗</a></h2>
|
||||
<p>Joplin Cloud Pro and Teams also now include the Email to Note feature, allowing you to conveniently store your emails within Joplin Cloud. By simply forwarding your emails to your Joplin Cloud address, you can transform them into notes. The email's subject will serve as the note title, while the body of the email will be the note's content. These notes will be organized within a notebook named "Inbox."</p>
|
||||
<p>More information in the <a href="https://joplinapp.org/email%5C_to%5C_note/">Email to Note documentation</a>.</p>
|
||||
<p>More information in the <a href="https://joplinapp.org/email_to_note/">Email to Note documentation</a>.</p>
|
||||
<h2>Choose to resize an image or not<a name="choose-to-resize-an-image-or-not" href="#choose-to-resize-an-image-or-not" class="heading-anchor">🔗</a></h2>
|
||||
<p>By default, when you add a large image, Joplin will ask you if you would like to shrink it down or not. With this new release, you now have the option to always ask, to always resize, or to never resize the image, giving you more flexibility and reducing the number of prompts in the app.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-resize-note.png" alt=""></p>
|
||||
|
72
README.md
72
README.md
@@ -537,47 +537,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) | 77%
|
||||
<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 | 22%
|
||||
<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) | 56%
|
||||
<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) | | 44%
|
||||
<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) | 86%
|
||||
<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) | 98%
|
||||
<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) | Fejby | 96%
|
||||
<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) | ERYpTION | 96%
|
||||
<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) | [Mr-Kanister](mailto:viger_gtrc@simplelogin.com) | 98%
|
||||
<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) | | 43%
|
||||
<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) | 84%
|
||||
<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 | 21%
|
||||
<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) | 54%
|
||||
<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) | | 42%
|
||||
<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) | 92%
|
||||
<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) | 97%
|
||||
<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) | Fejby | 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) | ERYpTION | 97%
|
||||
<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) | [Mr-Kanister](mailto:viger_gtrc@simplelogin.com) | 97%
|
||||
<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) | | 42%
|
||||
<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 Villaverde](mailto:teko.gr@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 | 25%
|
||||
<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) | mrkaato0 | 96%
|
||||
<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 Villaverde](mailto:teko.gr@gmail.com) | 93%
|
||||
<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 | 24%
|
||||
<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) | mrkaato0 | 94%
|
||||
<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 | 100%
|
||||
<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) | 28%
|
||||
<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) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 86%
|
||||
<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) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 78%
|
||||
<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) | 75%
|
||||
<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) | | 76%
|
||||
<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) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 86%
|
||||
<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) | [Mats Estensen](mailto:code@mxe.no) | 85%
|
||||
<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) | 53%
|
||||
<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) | [X3NO](mailto:X3NO@disroot.org) | 88%
|
||||
<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) | [Douglas Leão](mailto:djlsplays@gmail.com) | 85%
|
||||
<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) | 70%
|
||||
<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) | 49%
|
||||
<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) | 78%
|
||||
<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) | 27%
|
||||
<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) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 84%
|
||||
<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) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 76%
|
||||
<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) | 73%
|
||||
<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) | | 74%
|
||||
<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) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 97%
|
||||
<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) | [Mats Estensen](mailto:code@mxe.no) | 83%
|
||||
<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) | 52%
|
||||
<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) | [X3NO](mailto:X3NO@disroot.org) | 85%
|
||||
<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) | [Fernando Nagase](mailto:nagase.fernando@gmail.com) | 97%
|
||||
<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) | 68%
|
||||
<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) | 48%
|
||||
<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) | 76%
|
||||
<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) | 97%
|
||||
<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) | | 35%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 75%
|
||||
<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) | | 34%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 73%
|
||||
<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) | 97%
|
||||
<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) | 98%
|
||||
<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) | 97%
|
||||
<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) | [Dmitriy K](mailto:dmitry@atsip.ru) | 97%
|
||||
<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) | | 63%
|
||||
<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) | [qx100](mailto:ztymaxwell@gmail.com) | 98%
|
||||
<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) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 86%
|
||||
<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) | 88%
|
||||
<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) | 86%
|
||||
<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) | 95%
|
||||
<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) | 94%
|
||||
<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) | [Титан](mailto:fignin@ya.ru) | 96%
|
||||
<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) | | 61%
|
||||
<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) | [qx100](mailto:ztymaxwell@gmail.com) | 97%
|
||||
<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) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 84%
|
||||
<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) | 97%
|
||||
<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) | 84%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Contributors
|
||||
|
@@ -79,7 +79,7 @@
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.1.1",
|
||||
"glob": "10.3.4",
|
||||
"glob": "10.3.10",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "3.1.0",
|
||||
"lerna": "3.22.1",
|
||||
|
@@ -57,7 +57,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"read-chunk": "2.1.0",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.32.5",
|
||||
"sharp": "0.32.6",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
@@ -73,7 +73,7 @@
|
||||
"@joplin/tools": "~2.13",
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.17",
|
||||
"@types/node": "18.17.19",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.6.4",
|
||||
|
114
packages/app-clipper/content_scripts/clipperUtils.js
Normal file
114
packages/app-clipper/content_scripts/clipperUtils.js
Normal file
@@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.getStyleSheets = exports.getImageSizes = void 0;
|
||||
function absoluteUrl(url) {
|
||||
if (!url) { return url; }
|
||||
const protocol = url.toLowerCase().split(':')[0];
|
||||
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) { return url; }
|
||||
if (url.indexOf('//') === 0) {
|
||||
return location.protocol + url;
|
||||
} else if (url[0] === '/') {
|
||||
return `${location.protocol}//${location.host}${url}`;
|
||||
} else {
|
||||
return `${baseUrl()}/${url}`;
|
||||
}
|
||||
}
|
||||
function pageLocationOrigin() {
|
||||
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
|
||||
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
|
||||
// in this case.
|
||||
if (location.protocol === 'file:') {
|
||||
return 'file://';
|
||||
} else {
|
||||
return location.origin;
|
||||
}
|
||||
}
|
||||
function baseUrl() {
|
||||
let output = pageLocationOrigin() + location.pathname;
|
||||
if (output[output.length - 1] !== '/') {
|
||||
const output2 = output.split('/');
|
||||
output2.pop();
|
||||
output = output2.join('/');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function getJoplinClipperSvgClassName(svg) {
|
||||
for (const className of svg.classList) {
|
||||
if (className.indexOf('joplin-clipper-svg-') === 0) { return className; }
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function getImageSizes(element, forceAbsoluteUrls = false) {
|
||||
const output = {};
|
||||
const images = element.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (img.classList && img.classList.contains('joplin-clipper-hidden')) { continue; }
|
||||
let src = imageSrc(img);
|
||||
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
|
||||
if (!output[src]) { output[src] = []; }
|
||||
output[src].push({
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
const svgs = element.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
const svg = svgs[i];
|
||||
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) { continue; }
|
||||
const className = getJoplinClipperSvgClassName(svg); // 'joplin-clipper-svg-' + i;
|
||||
if (!className) {
|
||||
console.warn('SVG without a Joplin class:', svg);
|
||||
continue;
|
||||
}
|
||||
if (!svg.classList.contains(className)) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
const rect = svg.getBoundingClientRect();
|
||||
if (!output[className]) { output[className] = []; }
|
||||
output[className].push({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
exports.getImageSizes = getImageSizes;
|
||||
// In general we should use currentSrc because that's the image that's currently displayed,
|
||||
// especially within <picture> tags or with srcset. In these cases there can be multiple
|
||||
// sources and the best one is probably the one being displayed, thus currentSrc.
|
||||
function imageSrc(image) {
|
||||
if (image.currentSrc) { return image.currentSrc; }
|
||||
return image.src;
|
||||
}
|
||||
// Given a document, return a <style> tag that contains all the styles
|
||||
// required to render the page. Not currently used but could be as an
|
||||
// option to clip pages as HTML.
|
||||
// eslint-disable-next-line
|
||||
function getStyleSheets(doc) {
|
||||
const output = [];
|
||||
for (let i = 0; i < doc.styleSheets.length; i++) {
|
||||
const sheet = doc.styleSheets[i];
|
||||
try {
|
||||
for (const cssRule of sheet.cssRules) {
|
||||
output.push({ type: 'text', value: cssRule.cssText });
|
||||
}
|
||||
} catch (error) {
|
||||
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
|
||||
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
|
||||
// by the desktop application, since it doesn't have CORS restrictions.
|
||||
// eslint-disable-next-line
|
||||
console.info('Could not retrieve stylesheet now:', sheet.href);
|
||||
// eslint-disable-next-line
|
||||
console.info('It will downloaded by the main application.');
|
||||
// eslint-disable-next-line
|
||||
console.info(error);
|
||||
output.push({ type: 'url', value: sheet.href });
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
exports.getStyleSheets = getStyleSheets;
|
||||
// # sourceMappingURL=clipperUtils.js.map
|
@@ -20,20 +20,6 @@
|
||||
browserSupportsPromises_ = false;
|
||||
}
|
||||
|
||||
function absoluteUrl(url) {
|
||||
if (!url) return url;
|
||||
const protocol = url.toLowerCase().split(':')[0];
|
||||
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) return url;
|
||||
|
||||
if (url.indexOf('//') === 0) {
|
||||
return location.protocol + url;
|
||||
} else if (url[0] === '/') {
|
||||
return `${location.protocol}//${location.host}${url}`;
|
||||
} else {
|
||||
return `${baseUrl()}/${url}`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
@@ -49,85 +35,6 @@
|
||||
return document.title.trim();
|
||||
}
|
||||
|
||||
function pageLocationOrigin() {
|
||||
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
|
||||
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
|
||||
// in this case.
|
||||
|
||||
if (location.protocol === 'file:') {
|
||||
return 'file://';
|
||||
} else {
|
||||
return location.origin;
|
||||
}
|
||||
}
|
||||
|
||||
function baseUrl() {
|
||||
let output = pageLocationOrigin() + location.pathname;
|
||||
if (output[output.length - 1] !== '/') {
|
||||
output = output.split('/');
|
||||
output.pop();
|
||||
output = output.join('/');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function getJoplinClipperSvgClassName(svg) {
|
||||
for (const className of svg.classList) {
|
||||
if (className.indexOf('joplin-clipper-svg-') === 0) return className;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getImageSizes(element, forceAbsoluteUrls = false) {
|
||||
const output = {};
|
||||
|
||||
const images = element.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (img.classList && img.classList.contains('joplin-clipper-hidden')) continue;
|
||||
|
||||
let src = imageSrc(img);
|
||||
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
|
||||
|
||||
if (!output[src]) output[src] = [];
|
||||
|
||||
output[src].push({
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
|
||||
const svgs = element.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
const svg = svgs[i];
|
||||
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) continue;
|
||||
|
||||
const className = getJoplinClipperSvgClassName(svg);// 'joplin-clipper-svg-' + i;
|
||||
|
||||
if (!className) {
|
||||
console.warn('SVG without a Joplin class:', svg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!svg.classList.contains(className)) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
|
||||
const rect = svg.getBoundingClientRect();
|
||||
|
||||
if (!output[className]) output[className] = [];
|
||||
|
||||
output[className].push({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function getAnchorNames(element) {
|
||||
const output = [];
|
||||
// Anchor names are normally in A tags but can be in SPAN too
|
||||
@@ -146,14 +53,6 @@
|
||||
return output;
|
||||
}
|
||||
|
||||
// In general we should use currentSrc because that's the image that's currently displayed,
|
||||
// especially within <picture> tags or with srcset. In these cases there can be multiple
|
||||
// sources and the best one is probably the one being displayed, thus currentSrc.
|
||||
function imageSrc(image) {
|
||||
if (image.currentSrc) return image.currentSrc;
|
||||
return image.src;
|
||||
}
|
||||
|
||||
// Cleans up element by removing all its invisible children (which we don't want to render as Markdown)
|
||||
// And hard-code the image dimensions so that the information can be used by the clipper server to
|
||||
// display them at the right sizes in the notes.
|
||||
@@ -181,6 +80,7 @@
|
||||
}
|
||||
|
||||
if (nodeName === 'img') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const src = absoluteUrl(imageSrc(node));
|
||||
node.setAttribute('src', src);
|
||||
if (!(src in imageIndexes)) imageIndexes[src] = 0;
|
||||
@@ -199,6 +99,7 @@
|
||||
}
|
||||
|
||||
if (nodeName === 'svg') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const className = getJoplinClipperSvgClassName(node);
|
||||
if (!(className in imageIndexes)) imageIndexes[className] = 0;
|
||||
|
||||
@@ -216,11 +117,13 @@
|
||||
}
|
||||
|
||||
if (nodeName === 'embed') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const src = absoluteUrl(node.src);
|
||||
node.setAttribute('src', src);
|
||||
}
|
||||
|
||||
if (nodeName === 'object') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const data = absoluteUrl(node.data);
|
||||
node.setAttribute('data', data);
|
||||
}
|
||||
@@ -300,6 +203,7 @@
|
||||
let svgId = 0;
|
||||
|
||||
for (const svg of svgs) {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (!getJoplinClipperSvgClassName(svg)) {
|
||||
svg.classList.add(`joplin-clipper-svg-${svgId}`);
|
||||
svgId++;
|
||||
@@ -307,30 +211,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Given a document, return a <style> tag that contains all the styles
|
||||
// required to render the page. Not currently used but could be as an
|
||||
// option to clip pages as HTML.
|
||||
function getStyleSheets(doc) {
|
||||
const output = [];
|
||||
for (let i = 0; i < doc.styleSheets.length; i++) {
|
||||
const sheet = doc.styleSheets[i];
|
||||
try {
|
||||
for (const cssRule of sheet.cssRules) {
|
||||
output.push({ type: 'text', value: cssRule.cssText });
|
||||
}
|
||||
} catch (error) {
|
||||
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
|
||||
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
|
||||
// by the desktop application, since it doesn't have CORS restrictions.
|
||||
console.info('Could not retrieve stylesheet now:', sheet.href);
|
||||
console.info('It will downloaded by the main application.');
|
||||
console.info(error);
|
||||
output.push({ type: 'url', value: sheet.href });
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function documentForReadability() {
|
||||
// Readability directly change the passed document so clone it so as
|
||||
// to preserve the original web page.
|
||||
@@ -372,7 +252,9 @@
|
||||
name: shouldSendToJoplin ? 'sendContentToJoplin' : 'clippedContent',
|
||||
title: title,
|
||||
html: html,
|
||||
// eslint-disable-next-line no-undef
|
||||
base_url: baseUrl(),
|
||||
// eslint-disable-next-line no-undef
|
||||
url: pageLocationOrigin() + location.pathname + location.search,
|
||||
parent_id: command.parent_id,
|
||||
tags: command.tags || '',
|
||||
@@ -397,6 +279,7 @@
|
||||
response.warning = 'Could not retrieve simplified version of page - full page has been saved instead.';
|
||||
return response;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(article.title, article.body, getImageSizes(document), getAnchorNames(document));
|
||||
|
||||
} else if (command.name === 'isProbablyReaderable') {
|
||||
@@ -408,6 +291,7 @@
|
||||
} else if (command.name === 'completePageHtml') {
|
||||
|
||||
if (isPagePdf()) {
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(pageTitle(), embedPageUrl(), getImageSizes(document), getAnchorNames(document));
|
||||
}
|
||||
|
||||
@@ -417,10 +301,12 @@
|
||||
// Because cleanUpElement is going to modify the DOM and remove elements we don't want to work
|
||||
// directly on the document, so we make a copy of it first.
|
||||
const cleanDocument = document.body.cloneNode(true);
|
||||
// eslint-disable-next-line no-undef
|
||||
const imageSizes = getImageSizes(document, true);
|
||||
const imageIndexes = {};
|
||||
cleanUpElement(convertToMarkup, cleanDocument, imageSizes, imageIndexes);
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const stylesheets = convertToMarkup === 'html' ? getStyleSheets(document) : null;
|
||||
|
||||
// The <BODY> tag may have a style in the CSS stylesheets. This
|
||||
@@ -462,9 +348,11 @@
|
||||
container.appendChild(range.cloneContents());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const imageSizes = getImageSizes(document, true);
|
||||
const imageIndexes = {};
|
||||
cleanUpElement(convertToMarkup, container, imageSizes, imageIndexes);
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(pageTitle(), container.innerHTML, getImageSizes(document), getAnchorNames(document));
|
||||
|
||||
} else if (command.name === 'screenshot') {
|
||||
@@ -567,6 +455,7 @@
|
||||
const content = {
|
||||
title: pageTitle(),
|
||||
crop_rect: selectionArea,
|
||||
// eslint-disable-next-line no-undef
|
||||
url: pageLocationOrigin() + location.pathname + location.search,
|
||||
parent_id: command.parent_id,
|
||||
tags: command.tags,
|
||||
@@ -591,7 +480,9 @@
|
||||
|
||||
} else if (command.name === 'pageUrl') {
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const url = pageLocationOrigin() + location.pathname + location.search;
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(pageTitle(), url, getImageSizes(document), getAnchorNames(document));
|
||||
|
||||
} else {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "2.13.0",
|
||||
"version": "2.13.1",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
@@ -1,6 +1,8 @@
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const sourcePath = `${__dirname}/../../lib/randomClipperPort.js`;
|
||||
const clipperUtilsPath = `${__dirname}/../../lib/clipperUtils.js`;
|
||||
|
||||
|
||||
// Mozilla insists on building the clipper from a tarball, not from the repository
|
||||
// so we add this check and only copy the file if it's present. Normally it rarely
|
||||
@@ -10,6 +12,10 @@ if (fs.pathExistsSync(sourcePath)) {
|
||||
fs.copySync(sourcePath, `${__dirname}/src/randomClipperPort.js`);
|
||||
}
|
||||
|
||||
if (fs.pathExistsSync(clipperUtilsPath)) {
|
||||
fs.copySync(clipperUtilsPath, `${__dirname}/../content_scripts/clipperUtils.js`);
|
||||
}
|
||||
|
||||
// These files give warnings when loading the extension in Chrome, in dev mode
|
||||
fs.removeSync(`${__dirname}/node_modules/public-encrypt/test/test_key.pem`);
|
||||
fs.removeSync(`${__dirname}/node_modules/public-encrypt/test/test_rsa_pubkey.pem`);
|
||||
|
@@ -182,6 +182,7 @@ class AppComponent extends Component {
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/JSDOMParser.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability-readerable.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/clipperUtils.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/index.js' });
|
||||
}
|
||||
|
||||
|
4
packages/app-desktop/.gitignore
vendored
4
packages/app-desktop/.gitignore
vendored
@@ -14,3 +14,7 @@ style.min.css
|
||||
build/lib/
|
||||
vendor/*
|
||||
!vendor/loadEmojiLib.js
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
integration-tests/test-profile/
|
||||
|
@@ -260,7 +260,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
<br/>
|
||||
<MacOSMissingPasswordHelpLink
|
||||
theme={theme}
|
||||
text={_('%s: Missing password', _('Help'))}
|
||||
text={_('%s: Missing password.', _('Help'))}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
|
@@ -2,17 +2,16 @@
|
||||
import { ContextMenuEvent, ContextMenuParams } from 'electron';
|
||||
import { useEffect, RefObject } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import convertToScreenCoordinates from '../../../../utils/convertToScreenCoordinates';
|
||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
|
||||
import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
@@ -35,7 +34,7 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
// It might be buggy, refer to the below issue
|
||||
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
|
||||
useEffect(() => {
|
||||
const isAncestorOfCodeMirrorEditor = (elem: HTMLElement) => {
|
||||
const isAncestorOfCodeMirrorEditor = (elem: Element) => {
|
||||
for (; elem.parentElement; elem = elem.parentElement) {
|
||||
if (elem.classList.contains(props.editorClassName)) {
|
||||
return true;
|
||||
@@ -45,14 +44,9 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
let lastInCodeMirrorContextMenuTimestamp = 0;
|
||||
|
||||
// The browser's contextmenu event provides additional information about the
|
||||
// target of the event, not provided by the Electron context-menu event.
|
||||
const onBrowserContextMenu = (event: Event) => {
|
||||
if (isAncestorOfCodeMirrorEditor(event.target as HTMLElement)) {
|
||||
lastInCodeMirrorContextMenuTimestamp = Date.now();
|
||||
}
|
||||
const convertFromScreenCoordinates = (zoomPercent: number, screenXY: number) => {
|
||||
const zoomFraction = zoomPercent / 100;
|
||||
return screenXY / zoomFraction;
|
||||
};
|
||||
|
||||
function pointerInsideEditor(params: ContextMenuParams) {
|
||||
@@ -64,13 +58,15 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
// params.inputFieldType is "plainText". Thus, such a check would be inconsistent.
|
||||
if (!elements.length || !isEditable) return false;
|
||||
|
||||
const maximumMsSinceBrowserEvent = 100;
|
||||
if (Date.now() - lastInCodeMirrorContextMenuTimestamp > maximumMsSinceBrowserEvent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = convertToScreenCoordinates(Setting.value('windowContentZoomFactor'), elements[0].getBoundingClientRect());
|
||||
return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y;
|
||||
// Checks whether the element the pointer clicked on is inside the editor.
|
||||
// This logic will need to be changed if the editor is eventually wrapped
|
||||
// in an iframe, as elementFromPoint will return the iframe container (and not
|
||||
// a child of the editor).
|
||||
const zoom = Setting.value('windowContentZoomFactor');
|
||||
const xScreen = convertFromScreenCoordinates(zoom, x);
|
||||
const yScreen = convertFromScreenCoordinates(zoom, y);
|
||||
const intersectingElement = document.elementFromPoint(xScreen, yScreen);
|
||||
return intersectingElement && isAncestorOfCodeMirrorEditor(intersectingElement);
|
||||
}
|
||||
|
||||
async function onContextMenu(event: ContextMenuEvent, params: ContextMenuParams) {
|
||||
@@ -160,11 +156,8 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
// the listener that shows the default menu.
|
||||
bridge().window().webContents.prependListener('context-menu', onContextMenu);
|
||||
|
||||
window.addEventListener('contextmenu', onBrowserContextMenu);
|
||||
|
||||
return () => {
|
||||
bridge().window().webContents.off('context-menu', onContextMenu);
|
||||
window.removeEventListener('contextmenu', onBrowserContextMenu);
|
||||
};
|
||||
}, [
|
||||
props.plugins, props.editorClassName, editorRef,
|
||||
|
17
packages/app-desktop/integration-tests/README.md
Normal file
17
packages/app-desktop/integration-tests/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Integration tests
|
||||
|
||||
The integration tests in this directory can be run with `yarn playwright test`.
|
||||
|
||||
- Tests use a `test-profile` directory that should be re-created before every test.
|
||||
- Only one Electron application should be instantiated per test file.
|
||||
- Files in the `models/` directory follow [the page object model](https://playwright.dev/docs/pom).
|
||||
|
||||
# References
|
||||
|
||||
The following sources are helpful for designing and implementing Electron integration tests
|
||||
with Playwright:
|
||||
- [A setup guide from an organisation that uses Playwright](https://dev.to/kubeshop/testing-electron-apps-with-playwright-3f89)
|
||||
and [that organisation's test suite](https://github.com/kubeshop/monokle/blob/main/tests/base.test.ts).
|
||||
- [The Playwright ElectronApp docs](https://playwright.dev/docs/api/class-electronapplication)
|
||||
- [Electron Playwright example repository](https://github.com/spaceagetv/electron-playwright-example)
|
||||
- [Playwright best practices](https://playwright.dev/docs/best-practices)
|
79
packages/app-desktop/integration-tests/main.spec.ts
Normal file
79
packages/app-desktop/integration-tests/main.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import activateMainMenuItem from './util/activateMainMenuItem';
|
||||
import SettingsScreen from './models/SettingsScreen';
|
||||
|
||||
|
||||
test.describe('main', () => {
|
||||
test('app should launch', async ({ mainWindow }) => {
|
||||
// A window should open with the correct title
|
||||
expect(await mainWindow.title()).toMatch(/^Joplin/);
|
||||
|
||||
const mainPage = new MainScreen(mainWindow);
|
||||
await mainPage.waitFor();
|
||||
});
|
||||
|
||||
test('should be able to create and edit a new note', async ({ mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.newNoteButton.click();
|
||||
|
||||
const editor = mainScreen.noteEditor;
|
||||
await editor.waitFor();
|
||||
|
||||
// Wait for the title input to have the correct placeholder
|
||||
await mainWindow.locator('input[placeholder^="Creating new note"]').waitFor();
|
||||
|
||||
// Fill the title
|
||||
await editor.noteTitleInput.click();
|
||||
await editor.noteTitleInput.fill('Test note');
|
||||
|
||||
// Note list should contain the new note
|
||||
await expect(mainScreen.noteListContainer.getByText('Test note')).toBeVisible();
|
||||
|
||||
// Focus the editor
|
||||
await editor.codeMirrorEditor.click();
|
||||
|
||||
// Type some text
|
||||
await mainWindow.keyboard.type('# Test note!');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('New note content!');
|
||||
|
||||
// Should render
|
||||
const viewerFrame = editor.getNoteViewerIframe();
|
||||
await expect(viewerFrame.locator('h1')).toHaveText('Test note!');
|
||||
});
|
||||
|
||||
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
|
||||
// Sort order buttons should be visible by default
|
||||
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).toBeVisible();
|
||||
|
||||
// Open settings (check both labels so that this works on MacOS)
|
||||
expect(
|
||||
await activateMainMenuItem(electronApp, 'Preferences...') || await activateMainMenuItem(electronApp, 'Options'),
|
||||
).toBe(true);
|
||||
|
||||
// Should be on the settings screen
|
||||
const settingsScreen = new SettingsScreen(mainWindow);
|
||||
await settingsScreen.waitFor();
|
||||
|
||||
// Open the appearance tab
|
||||
await settingsScreen.appearanceTabButton.click();
|
||||
|
||||
// Find the sort order visible checkbox
|
||||
const sortOrderVisibleCheckbox = mainWindow.getByLabel(/^Show sort order/);
|
||||
|
||||
await expect(sortOrderVisibleCheckbox).toBeChecked();
|
||||
await sortOrderVisibleCheckbox.click();
|
||||
await expect(sortOrderVisibleCheckbox).not.toBeChecked();
|
||||
|
||||
// Save settings & close
|
||||
await settingsScreen.okayButton.click();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).not.toBeVisible();
|
||||
});
|
||||
});
|
20
packages/app-desktop/integration-tests/models/MainScreen.ts
Normal file
20
packages/app-desktop/integration-tests/models/MainScreen.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import NoteEditorScreen from './NoteEditorScreen';
|
||||
|
||||
export default class MainScreen {
|
||||
public readonly newNoteButton: Locator;
|
||||
public readonly noteListContainer: Locator;
|
||||
public readonly noteEditor: NoteEditorScreen;
|
||||
|
||||
public constructor(page: Page) {
|
||||
this.newNoteButton = page.locator('.new-note-button');
|
||||
this.noteListContainer = page.locator('.rli-noteList');
|
||||
this.noteEditor = new NoteEditorScreen(page);
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.newNoteButton.waitFor();
|
||||
await this.noteEditor.waitFor();
|
||||
await this.noteListContainer.waitFor();
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
|
||||
export default class NoteEditorPage {
|
||||
public readonly codeMirrorEditor: Locator;
|
||||
public readonly noteTitleInput: Locator;
|
||||
private readonly containerLocator: Locator;
|
||||
|
||||
public constructor(private readonly page: Page) {
|
||||
this.containerLocator = page.locator('.rli-editor');
|
||||
this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor');
|
||||
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
||||
}
|
||||
|
||||
public getNoteViewerIframe() {
|
||||
// The note viewer can change content when the note re-renders. As such,
|
||||
// a new locator needs to be created after re-renders (and this can't be a
|
||||
// static property).
|
||||
return this.page.frame({ url: /.*note-viewer[/\\]index.html.*/ });
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.codeMirrorEditor.waitFor();
|
||||
await this.noteTitleInput.waitFor();
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
export default class SettingsScreen {
|
||||
public readonly okayButton: Locator;
|
||||
public readonly appearanceTabButton: Locator;
|
||||
|
||||
public constructor(page: Page) {
|
||||
this.okayButton = page.locator('button', { hasText: 'OK' });
|
||||
this.appearanceTabButton = page.getByText('Appearance');
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.okayButton.waitFor();
|
||||
await this.appearanceTabButton.waitFor();
|
||||
}
|
||||
}
|
13
packages/app-desktop/integration-tests/run-ci.sh
Executable file
13
packages/app-desktop/integration-tests/run-ci.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Running desktop integration tests..."
|
||||
|
||||
export CI=true
|
||||
|
||||
if test "$RUNNER_OS" = "Linux" ; then
|
||||
# The Ubuntu Github CI doesn't have a display server.
|
||||
# Start a virtual one with xvfb-run.
|
||||
xvfb-run -- yarn run playwright test
|
||||
else
|
||||
yarn run playwright test
|
||||
fi
|
@@ -0,0 +1,36 @@
|
||||
|
||||
import type { ElectronApplication } from '@playwright/test';
|
||||
import type { MenuItem } from 'electron';
|
||||
|
||||
|
||||
// Roughly based on
|
||||
// https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/menu_helpers.ts
|
||||
|
||||
// `menuItemPath` should be a list of menu labels (e.g. [["&JoplinMainMenu", "&File"], "Synchronise"]).
|
||||
const activateMainMenuItem = (electronApp: ElectronApplication, menuItemLabel: string) => {
|
||||
return electronApp.evaluate(async ({ Menu }, menuItemLabel) => {
|
||||
const activateItemInSubmenu = (submenu: MenuItem[]) => {
|
||||
for (const item of submenu) {
|
||||
if (item.label === menuItemLabel && item.visible) {
|
||||
// Found!
|
||||
item.click();
|
||||
return true;
|
||||
} else if (item.submenu) {
|
||||
const foundItem = activateItemInSubmenu(item.submenu.items);
|
||||
|
||||
if (foundItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No item found
|
||||
return false;
|
||||
};
|
||||
|
||||
const appMenu = Menu.getApplicationMenu();
|
||||
return activateItemInSubmenu(appMenu.items);
|
||||
}, menuItemLabel);
|
||||
};
|
||||
|
||||
export default activateMainMenuItem;
|
45
packages/app-desktop/integration-tests/util/test.ts
Normal file
45
packages/app-desktop/integration-tests/util/test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { resolve, join, dirname } from 'path';
|
||||
import { remove, mkdirp } from 'fs-extra';
|
||||
import { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
|
||||
|
||||
|
||||
type JoplinFixtures = {
|
||||
electronApp: ElectronApplication;
|
||||
mainWindow: Page;
|
||||
};
|
||||
|
||||
// A custom fixture that loads an electron app. See
|
||||
// https://playwright.dev/docs/test-fixtures
|
||||
|
||||
export const test = base.extend<JoplinFixtures>({
|
||||
// Playwright fails if we don't use the object destructuring
|
||||
// pattern in the first argument.
|
||||
//
|
||||
// See https://github.com/microsoft/playwright/issues/8798
|
||||
//
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
electronApp: async ({ }, use) => {
|
||||
const profilePath = resolve(join(dirname(__dirname), 'test-profile'));
|
||||
const profileSubdir = join(profilePath, uuid.createNano());
|
||||
await mkdirp(profileSubdir);
|
||||
|
||||
const startupArgs = ['main.js', '--env', 'dev', '--profile', profileSubdir];
|
||||
const electronApp = await electron.launch({ args: startupArgs });
|
||||
|
||||
await use(electronApp);
|
||||
|
||||
await electronApp.firstWindow();
|
||||
await electronApp.close();
|
||||
await remove(profileSubdir);
|
||||
},
|
||||
|
||||
mainWindow: async ({ electronApp }, use) => {
|
||||
const window = await electronApp.firstWindow();
|
||||
await use(window);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export { expect } from '@playwright/test';
|
@@ -14,7 +14,8 @@
|
||||
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
|
||||
"start": "gulp build && electron . --env dev --log-level debug --open-dev-tools",
|
||||
"test": "jest",
|
||||
"test-ci": "yarn test",
|
||||
"test-ui": "playwright test",
|
||||
"test-ci": "yarn test && sh ./integration-tests/run-ci.sh",
|
||||
"renameReleaseAssets": "node tools/renameReleaseAssets.js"
|
||||
},
|
||||
"repository": {
|
||||
@@ -116,15 +117,16 @@
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "3.3.0",
|
||||
"@joplin/tools": "~2.13",
|
||||
"@playwright/test": "1.38.1",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.17",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/react-redux": "7.1.26",
|
||||
"@types/styled-components": "5.1.27",
|
||||
"@types/node": "18.17.19",
|
||||
"@types/react": "18.2.23",
|
||||
"@types/react-redux": "7.1.27",
|
||||
"@types/styled-components": "5.1.28",
|
||||
"electron": "25.8.1",
|
||||
"electron-builder": "24.4.0",
|
||||
"glob": "10.3.4",
|
||||
"glob": "10.3.10",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.6.4",
|
||||
"jest-environment-jsdom": "29.6.4",
|
||||
@@ -147,7 +149,7 @@
|
||||
"@joplin/lib": "~2.13",
|
||||
"@joplin/renderer": "~2.13",
|
||||
"@joplin/utils": "~2.13",
|
||||
"@types/mustache": "4.2.2",
|
||||
"@types/mustache": "4.2.3",
|
||||
"async-mutex": "0.4.0",
|
||||
"codemirror": "5.65.9",
|
||||
"color": "3.2.1",
|
||||
@@ -173,7 +175,7 @@
|
||||
"react-datetime": "3.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-redux": "8.1.2",
|
||||
"react-select": "5.7.4",
|
||||
"react-select": "5.7.5",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
"redux": "4.2.1",
|
||||
|
34
packages/app-desktop/playwright.config.ts
Normal file
34
packages/app-desktop/playwright.config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
// See https://playwright.dev/docs/test-configuration.
|
||||
export default defineConfig({
|
||||
testDir: './integration-tests',
|
||||
|
||||
// Only match .ts files (no compiled .js files)
|
||||
testMatch: '*.spec.ts',
|
||||
|
||||
// Allow running tests in parallel (note: each Joplin instance
|
||||
// is given its own profile directory).
|
||||
fullyParallel: true,
|
||||
|
||||
// Fail the build on CI if you accidentally left test.only in the source code.
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
// Retry on CI only
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
|
||||
// Opt out of parallel tests on CI
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
|
||||
// Reporter to use. See https://playwright.dev/docs/test-reporters
|
||||
reporter: process.env.CI ? 'line' : 'html',
|
||||
|
||||
// Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions.
|
||||
use: {
|
||||
// Base URL to use in actions like `await page.goto('/')`.
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
// Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
});
|
@@ -110,8 +110,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097721
|
||||
versionName "2.13.1"
|
||||
versionCode 2097722
|
||||
versionName "2.13.2"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ const ActionButton = (props: ActionButtonProps) => {
|
||||
};
|
||||
}), [props.buttons]);
|
||||
|
||||
const closedIcon = useIcon(props.mainButton?.icon ?? 'md-add');
|
||||
const closedIcon = useIcon(props.mainButton?.icon ?? 'add');
|
||||
const openIcon = useIcon('close');
|
||||
|
||||
return (
|
||||
|
@@ -158,11 +158,11 @@ class CameraView extends Component {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const photoIcon = this.state.snapping ? 'md-checkmark' : 'md-camera';
|
||||
const photoIcon = this.state.snapping ? 'checkmark' : 'camera';
|
||||
|
||||
const displayRatios = this.supportsRatios() && this.state.ratios.length > 1;
|
||||
|
||||
const reverseCameraButton = this.renderButton(this.reverse_onPress, 'md-camera-reverse', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 });
|
||||
const reverseCameraButton = this.renderButton(this.reverse_onPress, 'camera-reverse', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 });
|
||||
const ratioButton = !displayRatios ? <View style={{ flex: 1 }}/> : this.renderButton(this.ratio_onPress, <Text style={{ fontWeight: 'bold', fontSize: 20 }}>{Setting.value('camera.ratio')}</Text>, { flex: 1, flexDirection: 'row', justifyContent: 'flex-end', marginRight: 20 });
|
||||
|
||||
let cameraRatio = '4:3';
|
||||
@@ -202,7 +202,7 @@ class CameraView extends Component {
|
||||
<TouchableOpacity onPress={this.back_onPress}>
|
||||
<View style={{ marginLeft: 5, marginTop: 5, borderColor: '#00000040', borderWidth: 1, borderStyle: 'solid', borderRadius: 90, width: 50, height: 50, display: 'flex', backgroundColor: '#ffffff77', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Icon
|
||||
name={'md-arrow-back'}
|
||||
name={'arrow-back'}
|
||||
style={{
|
||||
fontSize: 40,
|
||||
color: 'black',
|
||||
|
@@ -308,7 +308,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
accessibilityHint={_('Show/hide the sidebar')}
|
||||
accessibilityRole="button">
|
||||
<View style={styles.sideMenuButton}>
|
||||
<Icon name="md-menu" style={styles.topIcon} />
|
||||
<Icon name="menu" style={styles.topIcon} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -324,7 +324,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
accessibilityRole="button">
|
||||
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
|
||||
<Icon
|
||||
name="md-arrow-back"
|
||||
name="arrow-back"
|
||||
style={styles.topIcon}
|
||||
/>
|
||||
</View>
|
||||
@@ -337,7 +337,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
) {
|
||||
if (!show) return null;
|
||||
|
||||
const icon = disabled ? <Icon name="md-checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
|
||||
const icon = disabled ? <Icon name="checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -407,7 +407,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
description={_('Select all')}
|
||||
contentStyle={styles.iconButton}
|
||||
>
|
||||
<Icon name="md-checkmark-circle-outline" style={styles.topIcon} />
|
||||
<Icon name="checkmark-circle-outline" style={styles.topIcon} />
|
||||
</CustomButton>
|
||||
);
|
||||
}
|
||||
@@ -421,7 +421,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
themeId={themeId}
|
||||
contentStyle={styles.iconButton}
|
||||
>
|
||||
<Icon name="md-search" style={styles.topIcon} />
|
||||
<Icon name="search" style={styles.topIcon} />
|
||||
</CustomButton>
|
||||
);
|
||||
}
|
||||
@@ -439,7 +439,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
}
|
||||
contentStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
|
||||
>
|
||||
<Icon name="md-trash" style={styles.topIcon} />
|
||||
<Icon name="trash" style={styles.topIcon} />
|
||||
</CustomButton>
|
||||
);
|
||||
}
|
||||
@@ -457,7 +457,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
}
|
||||
contentStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
|
||||
>
|
||||
<Icon name="md-copy" style={styles.topIcon} />
|
||||
<Icon name="copy" style={styles.topIcon} />
|
||||
</CustomButton>
|
||||
);
|
||||
}
|
||||
@@ -602,7 +602,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
<Menu onSelect={value => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
<MenuTrigger style={contextMenuStyle}>
|
||||
<View accessibilityLabel={_('Actions')}>
|
||||
<Icon name="md-ellipsis-vertical" style={this.styles().contextMenuTrigger} />
|
||||
<Icon name="ellipsis-vertical" style={this.styles().contextMenuTrigger} />
|
||||
</View>
|
||||
</MenuTrigger>
|
||||
<MenuOptions>
|
||||
|
@@ -40,7 +40,7 @@ class Checkbox extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconName = this.state.checked ? 'md-checkbox-outline' : 'md-square-outline';
|
||||
const iconName = this.state.checked ? 'checkbox-outline' : 'square-outline';
|
||||
|
||||
const style = this.props.style ? { ...this.props.style } : {};
|
||||
style.justifyContent = 'center';
|
||||
|
@@ -1406,7 +1406,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
const editButton = {
|
||||
label: _('Edit'),
|
||||
icon: 'md-create',
|
||||
icon: 'create',
|
||||
onPress: () => {
|
||||
this.setState({ mode: 'edit' });
|
||||
|
||||
|
@@ -58,7 +58,7 @@ class NoteTagsDialogComponent extends React.Component {
|
||||
|
||||
this.renderTag = data => {
|
||||
const tag = data.item;
|
||||
const iconName = noteHasTag(tag.id) ? 'md-checkbox-outline' : 'md-square-outline';
|
||||
const iconName = noteHasTag(tag.id) ? 'checkbox-outline' : 'square-outline';
|
||||
return (
|
||||
<TouchableOpacity key={tag.id} onPress={() => this.tag_press(tag.id)} style={this.styles().tag}>
|
||||
<View style={this.styles().tagIconText}>
|
||||
|
@@ -236,7 +236,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
void this.newNoteNavigate(buttonFolderId, isTodo);
|
||||
},
|
||||
color: '#9b59b6',
|
||||
icon: 'md-checkbox-outline',
|
||||
icon: 'checkbox-outline',
|
||||
});
|
||||
|
||||
buttons.push({
|
||||
@@ -246,7 +246,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
void this.newNoteNavigate(buttonFolderId, isTodo);
|
||||
},
|
||||
color: '#9b59b6',
|
||||
icon: 'md-document',
|
||||
icon: 'document',
|
||||
});
|
||||
return <ActionButton buttons={buttons}/>;
|
||||
}
|
||||
|
@@ -190,7 +190,7 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
onPress={() => this.clearButton_press()}
|
||||
accessibilityLabel={_('Clear')}
|
||||
>
|
||||
<Icon name="md-close-circle" style={this.styles().clearIcon} />
|
||||
<Icon name="close-circle" style={this.styles().clearIcon} />
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
|
||||
|
@@ -101,7 +101,7 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
styles.sideButtonSelected = { ...styles.sideButton, backgroundColor: theme.selectedColor };
|
||||
styles.sideButtonText = { ...styles.buttonText };
|
||||
|
||||
styles.emptyFolderIcon = { ...styles.sidebarIcon, marginRight: folderIconRightMargin, width: 20 };
|
||||
styles.emptyFolderIcon = { ...styles.sidebarIcon, marginRight: folderIconRightMargin, width: 21 };
|
||||
|
||||
return StyleSheet.create(styles);
|
||||
}, [props.themeId]);
|
||||
@@ -421,15 +421,15 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
|
||||
items.push(makeDivider('divider_1'));
|
||||
|
||||
items.push(renderSidebarButton('newFolder_button', _('New Notebook'), 'md-folder-open', newFolderButton_press));
|
||||
items.push(renderSidebarButton('newFolder_button', _('New Notebook'), 'folder-open', newFolderButton_press));
|
||||
|
||||
items.push(renderSidebarButton('tag_button', _('Tags'), 'md-pricetag', tagButton_press));
|
||||
items.push(renderSidebarButton('tag_button', _('Tags'), 'pricetag', tagButton_press));
|
||||
|
||||
if (props.profileConfig && props.profileConfig.profiles.length > 1) {
|
||||
items.push(renderSidebarButton('switchProfile_button', _('Switch profile'), 'md-people-circle-outline', switchProfileButton_press));
|
||||
items.push(renderSidebarButton('switchProfile_button', _('Switch profile'), 'people-circle-outline', switchProfileButton_press));
|
||||
}
|
||||
|
||||
items.push(renderSidebarButton('config_button', _('Configuration'), 'md-settings', configButton_press));
|
||||
items.push(renderSidebarButton('config_button', _('Configuration'), 'settings', configButton_press));
|
||||
|
||||
items.push(makeDivider('divider_2'));
|
||||
|
||||
@@ -451,7 +451,7 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
if (resourceFetcherText) fullReport.push(resourceFetcherText);
|
||||
if (decryptionReportText) fullReport.push(decryptionReportText);
|
||||
|
||||
items.push(renderSidebarButton('synchronize_button', !props.syncStarted ? _('Synchronise') : _('Cancel'), 'md-sync', synchronize_press));
|
||||
items.push(renderSidebarButton('synchronize_button', !props.syncStarted ? _('Synchronise') : _('Cancel'), 'sync', synchronize_press));
|
||||
|
||||
if (fullReport.length) {
|
||||
items.push(
|
||||
@@ -480,11 +480,11 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
// using padding. So instead creating blank elements for padding bottom and top.
|
||||
items.push(<View style={{ height: theme.marginTop }} key="bottom_top_hack" />);
|
||||
|
||||
items.push(renderSidebarButton('all_notes', _('All notes'), 'md-document', allNotesButton_press, props.notesParentType === 'SmartFilter'));
|
||||
items.push(renderSidebarButton('all_notes', _('All notes'), 'document', allNotesButton_press, props.notesParentType === 'SmartFilter'));
|
||||
|
||||
items.push(makeDivider('divider_all'));
|
||||
|
||||
items.push(renderSidebarButton('folder_header', _('Notebooks'), 'md-folder'));
|
||||
items.push(renderSidebarButton('folder_header', _('Notebooks'), 'folder'));
|
||||
|
||||
if (props.folders.length) {
|
||||
const result = shared.renderFolders(props, renderFolderItem, false);
|
||||
|
@@ -288,7 +288,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-get-random-values (1.9.0):
|
||||
- React-Core
|
||||
- react-native-image-picker (5.6.1):
|
||||
- react-native-image-picker (5.7.0):
|
||||
- React-Core
|
||||
- react-native-image-resizer (3.0.7):
|
||||
- React-Core
|
||||
@@ -306,7 +306,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-version-info (1.1.1):
|
||||
- React-Core
|
||||
- react-native-webview (13.4.0):
|
||||
- react-native-webview (13.5.1):
|
||||
- React-Core
|
||||
- React-perflogger (0.71.10)
|
||||
- React-RCTActionSheet (0.71.10):
|
||||
@@ -398,9 +398,9 @@ PODS:
|
||||
- React-Core
|
||||
- RNCPushNotificationIOS (1.11.0):
|
||||
- React-Core
|
||||
- RNDateTimePicker (7.4.2):
|
||||
- RNDateTimePicker (7.5.0):
|
||||
- React-Core
|
||||
- RNDeviceInfo (10.8.0):
|
||||
- RNDeviceInfo (10.9.0):
|
||||
- React-Core
|
||||
- RNExitApp (2.0.0):
|
||||
- React-Core
|
||||
@@ -414,15 +414,15 @@ PODS:
|
||||
- React
|
||||
- RNSecureRandom (1.0.1):
|
||||
- React
|
||||
- RNShare (9.2.4):
|
||||
- RNShare (9.4.1):
|
||||
- React-Core
|
||||
- RNVectorIcons (10.0.0):
|
||||
- React-Core
|
||||
- RNZipArchive (6.0.9):
|
||||
- RNZipArchive (6.1.0):
|
||||
- React-Core
|
||||
- RNZipArchive/Core (= 6.0.9)
|
||||
- RNZipArchive/Core (= 6.1.0)
|
||||
- SSZipArchive (~> 2.2)
|
||||
- RNZipArchive/Core (6.0.9):
|
||||
- RNZipArchive/Core (6.1.0):
|
||||
- React-Core
|
||||
- SSZipArchive (~> 2.2)
|
||||
- SSZipArchive (2.4.3)
|
||||
@@ -669,7 +669,7 @@ SPEC CHECKSUMS:
|
||||
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||
react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903
|
||||
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
|
||||
react-native-image-picker: 5fcac5a5ffcb3737837f0617d43fd767249290de
|
||||
react-native-image-picker: 3269f75c251cdcd61ab51b911dd30d6fff8c6169
|
||||
react-native-image-resizer: 681f7607418b97c084ba2d0999b153b103040d8a
|
||||
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
|
||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||
@@ -678,7 +678,7 @@ SPEC CHECKSUMS:
|
||||
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
|
||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
||||
react-native-webview: 64c9bf9646e7377240fb87d70f74556af6433143
|
||||
react-native-webview: 8baa0f5c6d336d6ba488e942bcadea5bf51f050a
|
||||
React-perflogger: 217095464d5c4bb70df0742fa86bf2a363693468
|
||||
React-RCTActionSheet: 8deae9b85a4cbc6a2243618ea62a374880a2c614
|
||||
React-RCTAnimation: 59c62353a8b59ce206044786c5d30e4754bffa64
|
||||
@@ -695,17 +695,17 @@ SPEC CHECKSUMS:
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
|
||||
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
|
||||
RNDateTimePicker: a21bfcc694e3190f8ddb5aacb0dd16f97b6be79f
|
||||
RNDeviceInfo: 5795b418ed3451ebcaf39384e6cf51f60cb931c9
|
||||
RNDateTimePicker: b722c23030b744763cf51d80fc22f354653f65e0
|
||||
RNDeviceInfo: 02ea8b23e2280fa18e00a06d7e62804d74028579
|
||||
RNExitApp: 00036cabe7bacbb413d276d5520bf74ba39afa6a
|
||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNLocalize: dbea38dcb344bf80ff18a1757b1becf11f70cae4
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
||||
RNShare: 255701a138fcdef6c27dc93724548fd122e91f9e
|
||||
RNShare: 32e97adc8d8c97d4a26bcdd3c45516882184f8b6
|
||||
RNVectorIcons: 8b5bb0fa61d54cd2020af4f24a51841ce365c7e9
|
||||
RNZipArchive: 68a0c6db4b1c103f846f1559622050df254a3ade
|
||||
RNZipArchive: ef9451b849c45a29509bf44e65b788829ab07801
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
Yoga: e7ea9e590e27460d28911403b894722354d73479
|
||||
|
||||
|
@@ -26,8 +26,8 @@
|
||||
"@joplin/renderer": "~2.13",
|
||||
"@joplin/utils": "~2.13",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/datetimepicker": "7.4.2",
|
||||
"@react-native-community/geolocation": "3.0.6",
|
||||
"@react-native-community/datetimepicker": "7.5.0",
|
||||
"@react-native-community/geolocation": "3.1.0",
|
||||
"@react-native-community/netinfo": "9.4.1",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-community/slider": "4.4.3",
|
||||
@@ -46,7 +46,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.71.10",
|
||||
"react-native-camera": "4.2.1",
|
||||
"react-native-device-info": "10.8.0",
|
||||
"react-native-device-info": "10.9.0",
|
||||
"react-native-dialogbox": "0.6.10",
|
||||
"react-native-document-picker": "9.0.1",
|
||||
"react-native-dropdownalert": "4.5.1",
|
||||
@@ -55,24 +55,24 @@
|
||||
"react-native-fingerprint-scanner": "6.0.0",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-get-random-values": "1.9.0",
|
||||
"react-native-image-picker": "5.6.1",
|
||||
"react-native-image-picker": "5.7.0",
|
||||
"react-native-localize": "3.0.2",
|
||||
"react-native-modal-datetime-picker": "17.1.0",
|
||||
"react-native-paper": "5.10.4",
|
||||
"react-native-paper": "5.10.6",
|
||||
"react-native-popup-menu": "0.16.1",
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "4.7.2",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "9.2.4",
|
||||
"react-native-share": "9.4.1",
|
||||
"react-native-side-menu-updated": "1.3.2",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-vector-icons": "10.0.0",
|
||||
"react-native-version-info": "1.1.1",
|
||||
"react-native-vosk": "0.1.12",
|
||||
"react-native-webview": "13.4.0",
|
||||
"react-native-zip-archive": "6.0.9",
|
||||
"react-native-webview": "13.6.0",
|
||||
"react-native-zip-archive": "6.1.0",
|
||||
"react-redux": "8.1.2",
|
||||
"redux": "4.2.1",
|
||||
"rn-fetch-blob": "0.12.0",
|
||||
@@ -91,13 +91,13 @@
|
||||
"@js-draw/material-icons": "1.5.0",
|
||||
"@lezer/highlight": "1.1.4",
|
||||
"@testing-library/jest-native": "5.4.3",
|
||||
"@testing-library/react-native": "12.2.2",
|
||||
"@testing-library/react-native": "12.3.0",
|
||||
"@tsconfig/react-native": "2.0.2",
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/react": "18.2.23",
|
||||
"@types/react-native": "0.70.6",
|
||||
"@types/react-redux": "7.1.26",
|
||||
"@types/react-redux": "7.1.27",
|
||||
"@types/tar-stream": "2.2.3",
|
||||
"babel-jest": "29.6.4",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
|
@@ -18,9 +18,9 @@
|
||||
"@joplin/lib": "~2.13",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/react": "18.0.24",
|
||||
"@types/react-redux": "7.1.26",
|
||||
"@types/styled-components": "5.1.27",
|
||||
"@types/react": "18.2.23",
|
||||
"@types/react-redux": "7.1.27",
|
||||
"@types/styled-components": "5.1.28",
|
||||
"jest": "29.6.3",
|
||||
"jest-environment-jsdom": "29.6.3",
|
||||
"ts-jest": "29.1.1",
|
||||
|
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.17",
|
||||
"@types/node": "18.17.19",
|
||||
"@typescript-eslint/eslint-plugin": "6.0.0",
|
||||
"@typescript-eslint/parser": "6.0.0",
|
||||
"coveralls": "3.1.1",
|
||||
|
@@ -16,7 +16,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"standard": "17.1.0",
|
||||
"tap": "16.3.8"
|
||||
"tap": "16.3.9"
|
||||
},
|
||||
"gitHead": "eb4b0e64eab40a51b0895d3a40a9d8c3cb7b1b14"
|
||||
}
|
||||
|
135
packages/lib/clipperUtils.ts
Normal file
135
packages/lib/clipperUtils.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
function absoluteUrl(url: string) {
|
||||
if (!url) return url;
|
||||
const protocol = url.toLowerCase().split(':')[0];
|
||||
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) return url;
|
||||
|
||||
if (url.indexOf('//') === 0) {
|
||||
return location.protocol + url;
|
||||
} else if (url[0] === '/') {
|
||||
return `${location.protocol}//${location.host}${url}`;
|
||||
} else {
|
||||
return `${baseUrl()}/${url}`;
|
||||
}
|
||||
}
|
||||
|
||||
function pageLocationOrigin() {
|
||||
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
|
||||
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
|
||||
// in this case.
|
||||
|
||||
if (location.protocol === 'file:') {
|
||||
return 'file://';
|
||||
} else {
|
||||
return location.origin;
|
||||
}
|
||||
}
|
||||
|
||||
function baseUrl() {
|
||||
let output = pageLocationOrigin() + location.pathname;
|
||||
if (output[output.length - 1] !== '/') {
|
||||
const output2 = output.split('/');
|
||||
output2.pop();
|
||||
output = output2.join('/');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function getJoplinClipperSvgClassName(svg: SVGSVGElement) {
|
||||
for (const className of svg.classList) {
|
||||
if (className.indexOf('joplin-clipper-svg-') === 0) return className;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
type ImageObject = {
|
||||
width: number;
|
||||
height: number;
|
||||
naturalWidth?: number;
|
||||
naturalHeight?: number;
|
||||
};
|
||||
|
||||
export function getImageSizes(element: Document, forceAbsoluteUrls = false) {
|
||||
const output: Record<string, ImageObject[]> = {};
|
||||
|
||||
const images = element.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (img.classList && img.classList.contains('joplin-clipper-hidden')) continue;
|
||||
|
||||
let src = imageSrc(img);
|
||||
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
|
||||
|
||||
if (!output[src]) output[src] = [];
|
||||
|
||||
output[src].push({
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
|
||||
const svgs = element.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
const svg = svgs[i];
|
||||
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) continue;
|
||||
|
||||
const className = getJoplinClipperSvgClassName(svg);// 'joplin-clipper-svg-' + i;
|
||||
|
||||
if (!className) {
|
||||
console.warn('SVG without a Joplin class:', svg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!svg.classList.contains(className)) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
|
||||
const rect = svg.getBoundingClientRect();
|
||||
|
||||
if (!output[className]) output[className] = [];
|
||||
|
||||
output[className].push({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// In general we should use currentSrc because that's the image that's currently displayed,
|
||||
// especially within <picture> tags or with srcset. In these cases there can be multiple
|
||||
// sources and the best one is probably the one being displayed, thus currentSrc.
|
||||
function imageSrc(image: HTMLImageElement) {
|
||||
if (image.currentSrc) return image.currentSrc;
|
||||
return image.src;
|
||||
}
|
||||
|
||||
// Given a document, return a <style> tag that contains all the styles
|
||||
// required to render the page. Not currently used but could be as an
|
||||
// option to clip pages as HTML.
|
||||
// eslint-disable-next-line
|
||||
export function getStyleSheets(doc: Document) {
|
||||
const output = [];
|
||||
for (let i = 0; i < doc.styleSheets.length; i++) {
|
||||
const sheet = doc.styleSheets[i];
|
||||
try {
|
||||
for (const cssRule of sheet.cssRules) {
|
||||
output.push({ type: 'text', value: cssRule.cssText });
|
||||
}
|
||||
} catch (error) {
|
||||
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
|
||||
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
|
||||
// by the desktop application, since it doesn't have CORS restrictions.
|
||||
// eslint-disable-next-line
|
||||
console.info('Could not retrieve stylesheet now:', sheet.href);
|
||||
// eslint-disable-next-line
|
||||
console.info('It will downloaded by the main application.');
|
||||
// eslint-disable-next-line
|
||||
console.info(error);
|
||||
output.push({ type: 'url', value: sheet.href });
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { closestSupportedLocale } from './locale';
|
||||
import { closestSupportedLocale, parsePluralForm, setLocale, _n } from './locale';
|
||||
|
||||
describe('locale', () => {
|
||||
|
||||
@@ -15,4 +15,80 @@ describe('locale', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should translate plurals - en_GB', () => {
|
||||
setLocale('en_GB');
|
||||
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 1)).toBe('Copy Shareable Link');
|
||||
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 2)).toBe('Copy Shareable Links');
|
||||
});
|
||||
|
||||
it('should translate plurals - fr_FR', () => {
|
||||
setLocale('fr_FR');
|
||||
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 1)).toBe('Copier lien partageable');
|
||||
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 2)).toBe('Copier liens partageables');
|
||||
});
|
||||
|
||||
it('should translate plurals - pl_PL', () => {
|
||||
setLocale('pl_PL');
|
||||
// Not the best test since 5 is the same as 2, but it's all I could find
|
||||
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 1)).toBe('Kopiuj udostępnialny link');
|
||||
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 2)).toBe('Kopiuj udostępnialne linki');
|
||||
expect(_n('Copy Shareable Link', 'Copy Shareable Links', 5)).toBe('Kopiuj udostępnialne linki');
|
||||
});
|
||||
|
||||
it('should parse the plural form', async () => {
|
||||
const pluralForms = [
|
||||
'nplurals=1; plural=0;',
|
||||
'nplurals=2; plural=(n != 0);',
|
||||
'nplurals=2; plural=(n != 1);',
|
||||
'nplurals=2; plural=(n > 1);',
|
||||
'nplurals=2; plural=(n%10!=1 || n%100==11);',
|
||||
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);',
|
||||
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
||||
'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
||||
'nplurals=3; plural=(n==0 ? 0 : n==1 ? 1 : 2);',
|
||||
'nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2);',
|
||||
'nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);',
|
||||
'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;',
|
||||
'nplurals=3; plural=(n==1) ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;',
|
||||
'nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);',
|
||||
'nplurals=4; plural=(n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3);',
|
||||
'nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3;',
|
||||
'nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;',
|
||||
'nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3;',
|
||||
'nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :(n>6 && n<11) ? 3 : 4;',
|
||||
'nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);',
|
||||
];
|
||||
|
||||
const pluralValues = [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1],
|
||||
[2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1],
|
||||
[2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1],
|
||||
[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2],
|
||||
[0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2],
|
||||
[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2],
|
||||
[0, 1, 2, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3],
|
||||
[3, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
|
||||
[2, 0, 1, 2, 2, 2, 2, 2, 3, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
||||
[3, 0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
|
||||
[4, 0, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
|
||||
[0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
|
||||
];
|
||||
|
||||
for (let index = 0; index < pluralForms.length; index++) {
|
||||
const form = pluralForms[index];
|
||||
const pluralFn = parsePluralForm(form);
|
||||
for (let i = 0; i < 128; ++i) {
|
||||
expect(pluralValues[index][i]).toBe(pluralFn(i));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -7,6 +7,7 @@ interface StringToStringMap {
|
||||
interface CodeToCountryMap {
|
||||
[key: string]: string[];
|
||||
}
|
||||
type ParsePluralFormFunction = (n: number)=> number;
|
||||
|
||||
const codeToLanguageE_: StringToStringMap = {};
|
||||
codeToLanguageE_['aa'] = 'Afar';
|
||||
@@ -436,12 +437,51 @@ const codeToCountry_: CodeToCountryMap = {
|
||||
let supportedLocales_: any = null;
|
||||
let localeStats_: any = null;
|
||||
|
||||
const loadedLocales_: any = {};
|
||||
const loadedLocales_: Record<string, Record<string, string[]>> = {};
|
||||
|
||||
const pluralFunctions_: Record<string, ParsePluralFormFunction> = {};
|
||||
|
||||
const defaultLocale_ = 'en_GB';
|
||||
|
||||
let currentLocale_ = defaultLocale_;
|
||||
|
||||
// Copied from https://github.com/eugeny-dementev/parse-gettext-plural-form
|
||||
// along with the tests
|
||||
export const parsePluralForm = (form: string): ParsePluralFormFunction => {
|
||||
const pluralFormRegex = /^(\s*nplurals\s*=\s*[0-9]+\s*;\s*plural\s*=\s*(?:\s|[-?|&=!<>+*/%:;a-zA-Z0-9_()])+)$/m;
|
||||
|
||||
if (!pluralFormRegex.test(form)) throw new Error(`Plural-Forms is invalid: ${form}`);
|
||||
|
||||
if (!/;\s*$/.test(form)) {
|
||||
form += ';';
|
||||
}
|
||||
|
||||
const code = [
|
||||
'var plural;',
|
||||
'var nplurals;',
|
||||
form,
|
||||
'return (plural === true ? 1 : plural ? plural : 0);',
|
||||
].join('\n');
|
||||
|
||||
// eslint-disable-next-line no-new-func -- There's a regex to check the form but it's still slighlty unsafe, eventually we should automatically generate all the functions in advance in build-translations.ts
|
||||
return (new Function('n', code)) as ParsePluralFormFunction;
|
||||
};
|
||||
|
||||
const getPluralFunction = (lang: string) => {
|
||||
if (!(lang in pluralFunctions_)) {
|
||||
const locale = closestSupportedLocale(lang);
|
||||
const stats = localeStats()[locale];
|
||||
|
||||
if (!stats.pluralForms) {
|
||||
pluralFunctions_[lang] = null;
|
||||
} else {
|
||||
pluralFunctions_[lang] = parsePluralForm(stats.pluralForms);
|
||||
}
|
||||
}
|
||||
|
||||
return pluralFunctions_[lang];
|
||||
};
|
||||
|
||||
function defaultLocale() {
|
||||
return defaultLocale_;
|
||||
}
|
||||
@@ -589,18 +629,45 @@ function _(s: string, ...args: any[]): string {
|
||||
}
|
||||
|
||||
function _n(singular: string, plural: string, n: number, ...args: any[]) {
|
||||
if (n > 1) return _(plural, ...args);
|
||||
return _(singular, ...args);
|
||||
if (['en_GB', 'en_US'].includes(currentLocale_)) {
|
||||
if (n > 1) return _(plural, ...args);
|
||||
return _(singular, ...args);
|
||||
} else {
|
||||
const pluralFn = getPluralFunction(currentLocale_);
|
||||
const stringIndex = pluralFn ? pluralFn(n) : 0;
|
||||
const strings = localeStrings(currentLocale_);
|
||||
const result = strings[singular];
|
||||
|
||||
let translatedString = '';
|
||||
if (result === undefined || !result.join('')) {
|
||||
translatedString = singular;
|
||||
} else {
|
||||
translatedString = stringIndex < result.length ? result[stringIndex] : result[0];
|
||||
}
|
||||
|
||||
try {
|
||||
return sprintf(translatedString, ...args);
|
||||
} catch (error) {
|
||||
return `${translatedString} ${args.join(', ')} (Translation error: ${error.message})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stringByLocale = (locale: string, s: string, ...args: any[]): string => {
|
||||
const strings = localeStrings(locale);
|
||||
let result = strings[s];
|
||||
if (result === '' || result === undefined) result = s;
|
||||
const result = strings[s];
|
||||
let translatedString = '';
|
||||
|
||||
if (result === undefined || !result.join('')) {
|
||||
translatedString = s;
|
||||
} else {
|
||||
translatedString = result[0];
|
||||
}
|
||||
|
||||
try {
|
||||
return sprintf(result, ...args);
|
||||
return sprintf(translatedString, ...args);
|
||||
} catch (error) {
|
||||
return `${result} ${args.join(', ')} (Translation error: ${error.message})`;
|
||||
return `${translatedString} ${args.join(', ')} (Translation error: ${error.message})`;
|
||||
}
|
||||
};
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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":77};
|
||||
stats['eu'] = {"percentDone":22};
|
||||
stats['bs_BA'] = {"percentDone":56};
|
||||
stats['bg_BG'] = {"percentDone":44};
|
||||
stats['ca'] = {"percentDone":86};
|
||||
stats['hr_HR'] = {"percentDone":98};
|
||||
stats['cs_CZ'] = {"percentDone":96};
|
||||
stats['da_DK'] = {"percentDone":96};
|
||||
stats['de_DE'] = {"percentDone":98};
|
||||
stats['et_EE'] = {"percentDone":43};
|
||||
stats['ar'] = {"percentDone":84,"pluralForms":"nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);"};
|
||||
stats['eu'] = {"percentDone":21,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['bs_BA'] = {"percentDone":54,"pluralForms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"};
|
||||
stats['bg_BG'] = {"percentDone":42,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['ca'] = {"percentDone":92,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['hr_HR'] = {"percentDone":97,"pluralForms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"};
|
||||
stats['cs_CZ'] = {"percentDone":94,"pluralForms":"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"};
|
||||
stats['da_DK'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['de_DE'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['et_EE'] = {"percentDone":42,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['en_GB'] = {"percentDone":100};
|
||||
stats['en_US'] = {"percentDone":100};
|
||||
stats['es_ES'] = {"percentDone":95};
|
||||
stats['eo'] = {"percentDone":25};
|
||||
stats['fi_FI'] = {"percentDone":96};
|
||||
stats['fr_FR'] = {"percentDone":100};
|
||||
stats['gl_ES'] = {"percentDone":28};
|
||||
stats['id_ID'] = {"percentDone":86};
|
||||
stats['it_IT'] = {"percentDone":78};
|
||||
stats['hu_HU'] = {"percentDone":75};
|
||||
stats['nl_BE'] = {"percentDone":76};
|
||||
stats['nl_NL'] = {"percentDone":86};
|
||||
stats['nb_NO'] = {"percentDone":85};
|
||||
stats['fa'] = {"percentDone":53};
|
||||
stats['pl_PL'] = {"percentDone":88};
|
||||
stats['pt_BR'] = {"percentDone":85};
|
||||
stats['pt_PT'] = {"percentDone":70};
|
||||
stats['ro'] = {"percentDone":49};
|
||||
stats['sl_SI'] = {"percentDone":78};
|
||||
stats['sv'] = {"percentDone":97};
|
||||
stats['th_TH'] = {"percentDone":35};
|
||||
stats['vi'] = {"percentDone":75};
|
||||
stats['tr_TR'] = {"percentDone":97};
|
||||
stats['uk_UA'] = {"percentDone":98};
|
||||
stats['el_GR'] = {"percentDone":97};
|
||||
stats['ru_RU'] = {"percentDone":97};
|
||||
stats['sr_RS'] = {"percentDone":63};
|
||||
stats['zh_CN'] = {"percentDone":98};
|
||||
stats['zh_TW'] = {"percentDone":86};
|
||||
stats['ja_JP'] = {"percentDone":88};
|
||||
stats['ko'] = {"percentDone":86};
|
||||
stats['en_US'] = {"percentDone":100,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['es_ES'] = {"percentDone":93,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['eo'] = {"percentDone":24,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['fi_FI'] = {"percentDone":94,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['fr_FR'] = {"percentDone":100,"pluralForms":"nplurals=2; plural=(n > 1);"};
|
||||
stats['gl_ES'] = {"percentDone":27,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['id_ID'] = {"percentDone":84,"pluralForms":"nplurals=1; plural=0;"};
|
||||
stats['it_IT'] = {"percentDone":76,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['hu_HU'] = {"percentDone":73,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['nl_BE'] = {"percentDone":74,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['nl_NL'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['nb_NO'] = {"percentDone":83,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['fa'] = {"percentDone":52,"pluralForms":"nplurals=2; plural=(n==0 || n==1);"};
|
||||
stats['pl_PL'] = {"percentDone":85,"pluralForms":"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);"};
|
||||
stats['pt_BR'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n > 1);"};
|
||||
stats['pt_PT'] = {"percentDone":68,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['ro'] = {"percentDone":48,"pluralForms":"nplurals=3; plural=(n==1 ? 0 : n==0 || (n!=1 && n%100>=1 && n%100<=19) ? 1 : 2);"};
|
||||
stats['sl_SI'] = {"percentDone":76,"pluralForms":"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"};
|
||||
stats['sv'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['th_TH'] = {"percentDone":34,"pluralForms":"nplurals=1; plural=0;"};
|
||||
stats['vi'] = {"percentDone":73,"pluralForms":"nplurals=1; plural=0;"};
|
||||
stats['tr_TR'] = {"percentDone":97,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['uk_UA'] = {"percentDone":95,"pluralForms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);"};
|
||||
stats['el_GR'] = {"percentDone":94,"pluralForms":"nplurals=2; plural=(n != 1);"};
|
||||
stats['ru_RU'] = {"percentDone":96,"pluralForms":"nplurals=2; plural=(n > 1);"};
|
||||
stats['sr_RS'] = {"percentDone":61,"pluralForms":"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);"};
|
||||
stats['zh_CN'] = {"percentDone":97,"pluralForms":"nplurals=1; plural=0;"};
|
||||
stats['zh_TW'] = {"percentDone":84,"pluralForms":"nplurals=1; plural=0;"};
|
||||
stats['ja_JP'] = {"percentDone":97,"pluralForms":"nplurals=1; plural=0;"};
|
||||
stats['ko'] = {"percentDone":84,"pluralForms":"nplurals=1; plural=0;"};
|
||||
module.exports = { locales: locales, stats: stats };
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1115,7 +1115,7 @@ class Setting extends BaseModel {
|
||||
public: true,
|
||||
appTypes: [AppType.Mobile, AppType.Desktop],
|
||||
label: () => _('Resize large images:'),
|
||||
description: () => _('Shrink large images before adding them to notes to save storage space.'),
|
||||
description: () => _('Shrink large images before adding them to notes.'),
|
||||
options: () => {
|
||||
return {
|
||||
alwaysAsk: _('Always ask'),
|
||||
|
@@ -19,13 +19,13 @@
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/js-yaml": "4.0.6",
|
||||
"@types/node": "18.17.17",
|
||||
"@types/node-rsa": "1.1.1",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/node": "18.17.19",
|
||||
"@types/node-rsa": "1.1.2",
|
||||
"@types/react": "18.2.23",
|
||||
"@types/uuid": "9.0.4",
|
||||
"clean-html": "1.5.0",
|
||||
"jest": "29.6.4",
|
||||
"sharp": "0.32.5",
|
||||
"sharp": "0.32.6",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -52,7 +52,7 @@
|
||||
"es6-promise-pool": "2.5.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fast-xml-parser": "3.21.1",
|
||||
"follow-redirects": "1.15.2",
|
||||
"follow-redirects": "1.15.3",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"hpagent": "1.2.0",
|
||||
@@ -63,7 +63,7 @@
|
||||
"immer": "7.0.15",
|
||||
"js-yaml": "4.1.0",
|
||||
"levenshtein": "1.0.5",
|
||||
"markdown-it": "13.0.1",
|
||||
"markdown-it": "13.0.2",
|
||||
"md5": "2.3.0",
|
||||
"md5-file": "5.0.0",
|
||||
"moment": "2.29.4",
|
||||
@@ -86,7 +86,7 @@
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
"string-to-stream": "3.0.1",
|
||||
"tar": "6.1.15",
|
||||
"tar": "6.2.0",
|
||||
"tcp-port-used": "1.0.2",
|
||||
"uglifycss": "0.0.29",
|
||||
"url-parse": "1.5.10",
|
||||
|
@@ -27,6 +27,7 @@ const { fileExtension, safeFileExtension, safeFilename, filename } = require('..
|
||||
const { MarkupToHtml } = require('@joplin/renderer');
|
||||
const { ErrorNotFound } = require('../utils/errors');
|
||||
import { fileUriToPath } from '@joplin/utils/url';
|
||||
import { NoteEntity } from '../../database/types';
|
||||
|
||||
const logger = Logger.create('routes/notes');
|
||||
|
||||
@@ -38,7 +39,31 @@ function htmlToMdParser() {
|
||||
return htmlToMdParser_;
|
||||
}
|
||||
|
||||
async function requestNoteToNote(requestNote: any) {
|
||||
type RequestNote = {
|
||||
id?: any;
|
||||
parent_id?: string;
|
||||
title: string;
|
||||
body?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
altitude?: number;
|
||||
author?: string;
|
||||
source_url?: string;
|
||||
is_todo?: number;
|
||||
todo_due?: number;
|
||||
todo_completed?: number;
|
||||
user_updated_time?: number;
|
||||
user_created_time?: number;
|
||||
markup_language?: number;
|
||||
body_html: string;
|
||||
base_url?: string;
|
||||
convert_to: string;
|
||||
anchor_names?: any[];
|
||||
image_sizes?: object;
|
||||
stylesheets: any;
|
||||
};
|
||||
|
||||
async function requestNoteToNote(requestNote: RequestNote): Promise<NoteEntity> {
|
||||
const output: any = {
|
||||
title: requestNote.title ? requestNote.title : '',
|
||||
body: requestNote.body ? requestNote.body : '',
|
||||
@@ -337,6 +362,34 @@ async function attachImageFromDataUrl(note: any, imageDataUrl: string, cropRect:
|
||||
return await shim.attachFileToNote(note, tempFilePath);
|
||||
}
|
||||
|
||||
export const extractNoteFromHTML = async (requestNote: RequestNote, requestId: number, imageSizes: any) => {
|
||||
const note = await requestNoteToNote(requestNote);
|
||||
|
||||
const mediaUrls = extractMediaUrls(note.markup_language, note.body);
|
||||
|
||||
logger.info(`Request (${requestId}): Downloading media files: ${mediaUrls.length}`);
|
||||
|
||||
const mediaFiles = await downloadMediaFiles(mediaUrls); // , allowFileProtocolImages);
|
||||
|
||||
logger.info(`Request (${requestId}): Creating resources from paths: ${Object.getOwnPropertyNames(mediaFiles).length}`);
|
||||
|
||||
const resources = await createResourcesFromPaths(mediaFiles);
|
||||
await removeTempFiles(resources);
|
||||
note.body = replaceUrlsByResources(note.markup_language, note.body, resources, imageSizes);
|
||||
|
||||
logger.info(`Request (${requestId}): Saving note...`);
|
||||
|
||||
const saveOptions = defaultSaveOptions('POST', note.id);
|
||||
saveOptions.autoTimestamp = false; // No auto-timestamp because user may have provided them
|
||||
const timestamp = Date.now();
|
||||
note.updated_time = timestamp;
|
||||
note.created_time = timestamp;
|
||||
if (!('user_updated_time' in note)) note.user_updated_time = timestamp;
|
||||
if (!('user_created_time' in note)) note.user_created_time = timestamp;
|
||||
|
||||
return { note, saveOptions, resources };
|
||||
};
|
||||
|
||||
export default async function(request: Request, id: string = null, link: string = null) {
|
||||
if (request.method === 'GET') {
|
||||
if (link && link === 'tags') {
|
||||
@@ -368,31 +421,9 @@ export default async function(request: Request, id: string = null, link: string
|
||||
|
||||
logger.info('Images:', imageSizes);
|
||||
|
||||
let note: any = await requestNoteToNote(requestNote);
|
||||
const extracted = await extractNoteFromHTML(requestNote, requestId, imageSizes);
|
||||
|
||||
const mediaUrls = extractMediaUrls(note.markup_language, note.body);
|
||||
|
||||
logger.info(`Request (${requestId}): Downloading media files: ${mediaUrls.length}`);
|
||||
|
||||
let result = await downloadMediaFiles(mediaUrls); // , allowFileProtocolImages);
|
||||
|
||||
logger.info(`Request (${requestId}): Creating resources from paths: ${Object.getOwnPropertyNames(result).length}`);
|
||||
|
||||
result = await createResourcesFromPaths(result);
|
||||
await removeTempFiles(result);
|
||||
note.body = replaceUrlsByResources(note.markup_language, note.body, result, imageSizes);
|
||||
|
||||
logger.info(`Request (${requestId}): Saving note...`);
|
||||
|
||||
const saveOptions = defaultSaveOptions('POST', note.id);
|
||||
saveOptions.autoTimestamp = false; // No auto-timestamp because user may have provided them
|
||||
const timestamp = Date.now();
|
||||
note.updated_time = timestamp;
|
||||
note.created_time = timestamp;
|
||||
if (!('user_updated_time' in note)) note.user_updated_time = timestamp;
|
||||
if (!('user_created_time' in note)) note.user_created_time = timestamp;
|
||||
|
||||
note = await Note.save(note, saveOptions);
|
||||
let note = await Note.save(extracted.note, extracted.saveOptions);
|
||||
|
||||
if (requestNote.tags) {
|
||||
const tagTitles = requestNote.tags.split(',');
|
||||
|
@@ -42,7 +42,7 @@ const shim = {
|
||||
isNode: () => {
|
||||
if (typeof process === 'undefined') return false;
|
||||
if (shim.isElectron()) return true;
|
||||
return process.title === 'node' || (process.title && process.title.indexOf('gulp') === 0);
|
||||
return !shim.mobilePlatform();
|
||||
},
|
||||
|
||||
isReactNative: () => {
|
||||
|
@@ -21,9 +21,9 @@
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/pdfjs-dist": "2.10.378",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/styled-components": "5.1.27",
|
||||
"@types/react": "18.2.23",
|
||||
"@types/react-dom": "18.2.8",
|
||||
"@types/styled-components": "5.1.28",
|
||||
"babel-jest": "29.6.4",
|
||||
"css-loader": "6.8.1",
|
||||
"jest": "29.6.4",
|
||||
|
@@ -30,7 +30,7 @@
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.17",
|
||||
"@types/node": "18.17.19",
|
||||
"jest": "29.6.4",
|
||||
"source-map-loader": "4.0.1",
|
||||
"typescript": "5.1.6",
|
||||
|
@@ -20,7 +20,7 @@
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.17",
|
||||
"@types/node": "18.17.19",
|
||||
"jest": "29.6.4",
|
||||
"jest-environment-jsdom": "29.6.4",
|
||||
"ts-jest": "29.1.1",
|
||||
@@ -36,7 +36,7 @@
|
||||
"html-entities": "1.4.0",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"katex": "0.16.8",
|
||||
"markdown-it": "13.0.1",
|
||||
"markdown-it": "13.0.2",
|
||||
"markdown-it-abbr": "1.0.4",
|
||||
"markdown-it-anchor": "5.3.0",
|
||||
"markdown-it-deflist": "2.1.0",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "yarn run build && JOPLIN_IS_TESTING=1 nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
|
||||
@@ -17,6 +17,7 @@
|
||||
"test-ci": "yarn test",
|
||||
"test-debug": "node --inspect node_modules/.bin/jest -- --verbose=false",
|
||||
"clean": "gulp clean",
|
||||
"populateDatabase": "JOPLIN_TESTS_SERVER_DB=pg node dist/utils/testing/populateDatabase",
|
||||
"stripeListen": "stripe listen --forward-to http://joplincloud.local:22300/stripe/webhook",
|
||||
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json"
|
||||
},
|
||||
@@ -32,14 +33,14 @@
|
||||
"bulma": "0.9.4",
|
||||
"bulma-prefers-dark": "0.1.0-beta.1",
|
||||
"compare-versions": "6.1.0",
|
||||
"dayjs": "1.11.9",
|
||||
"dayjs": "1.11.10",
|
||||
"formidable": "3.5.1",
|
||||
"fs-extra": "11.1.1",
|
||||
"html-entities": "1.4.0",
|
||||
"jquery": "3.7.1",
|
||||
"knex": "2.5.1",
|
||||
"koa": "2.14.2",
|
||||
"markdown-it": "13.0.1",
|
||||
"markdown-it": "13.0.2",
|
||||
"mustache": "4.2.0",
|
||||
"nanoid": "2.1.11",
|
||||
"node-cron": "3.0.2",
|
||||
@@ -60,6 +61,7 @@
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~2.13",
|
||||
"@rmp135/sql-ts": "1.18.0",
|
||||
"@types/bcryptjs": "2.4.5",
|
||||
"@types/formidable": "3.4.3",
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/jest": "29.5.4",
|
||||
@@ -67,9 +69,9 @@
|
||||
"@types/jsdom": "21.1.3",
|
||||
"@types/koa": "2.13.9",
|
||||
"@types/markdown-it": "12.2.3",
|
||||
"@types/mustache": "4.2.2",
|
||||
"@types/nodemailer": "6.4.10",
|
||||
"@types/yargs": "17.0.24",
|
||||
"@types/mustache": "4.2.3",
|
||||
"@types/nodemailer": "6.4.11",
|
||||
"@types/yargs": "17.0.26",
|
||||
"@types/zxcvbn": "4.4.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.6.4",
|
||||
|
@@ -98,7 +98,7 @@ export const up = async (db: DbConnection) => {
|
||||
await db('users').insert({
|
||||
id: adminId,
|
||||
email: defaultAdminEmail,
|
||||
password: hashPassword(defaultAdminPassword),
|
||||
password: await hashPassword(defaultAdminPassword),
|
||||
full_name: 'Admin',
|
||||
is_admin: 1,
|
||||
updated_time: now,
|
||||
|
@@ -197,7 +197,7 @@ export default abstract class BaseModel<T> {
|
||||
// The `name` argument is only for debugging, so that any stuck transaction
|
||||
// can be more easily identified.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
protected async withTransaction<T>(fn: Function, name: string): Promise<T> {
|
||||
protected async withTransaction<T>(fn: Function, name = ''): Promise<T> {
|
||||
const debugSteps = false;
|
||||
const debugTimeout = true;
|
||||
const timeoutMs = 10000;
|
||||
|
@@ -6,7 +6,7 @@ import { md5 } from '../utils/crypto';
|
||||
import { ErrorResyncRequired } from '../utils/errors';
|
||||
import { Day, formatDateTime } from '../utils/time';
|
||||
import BaseModel, { SaveOptions } from './BaseModel';
|
||||
import { PaginatedResults, Pagination, PaginationOrderDir } from './utils/pagination';
|
||||
import { PaginatedResults } from './utils/pagination';
|
||||
|
||||
const logger = Logger.create('ChangeModel');
|
||||
|
||||
@@ -88,7 +88,44 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
};
|
||||
}
|
||||
|
||||
private changesForUserQuery(userId: Uuid, count: boolean): Knex.QueryBuilder {
|
||||
// private changesForUserQuery(userId: Uuid, count: boolean): Knex.QueryBuilder {
|
||||
// // When need to get:
|
||||
// //
|
||||
// // - All the CREATE and DELETE changes associated with the user
|
||||
// // - All the UPDATE changes that applies to items associated with the
|
||||
// // user.
|
||||
// //
|
||||
// // UPDATE changes do not have the user_id set because they are specific
|
||||
// // to the item, not to a particular user.
|
||||
|
||||
// const query = this
|
||||
// .db('changes')
|
||||
// .where(function() {
|
||||
// void this.whereRaw('((type = ? OR type = ?) AND user_id = ?)', [ChangeType.Create, ChangeType.Delete, userId])
|
||||
// // Need to use a RAW query here because Knex has a "not a
|
||||
// // bug" bug that makes it go into infinite loop in some
|
||||
// // contexts, possibly only when running inside Jest (didn't
|
||||
// // test outside).
|
||||
// // https://github.com/knex/knex/issues/1851
|
||||
// .orWhereRaw('type = ? AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)', [ChangeType.Update, userId]);
|
||||
// });
|
||||
|
||||
// if (count) {
|
||||
// void query.countDistinct('id', { as: 'total' });
|
||||
// } else {
|
||||
// void query.select([
|
||||
// 'id',
|
||||
// 'item_id',
|
||||
// 'item_name',
|
||||
// 'type',
|
||||
// 'updated_time',
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// return query;
|
||||
// }
|
||||
|
||||
public async changesForUserQuery(userId: Uuid, fromCounter: number, limit: number, doCountQuery: boolean): Promise<Change[]> {
|
||||
// When need to get:
|
||||
//
|
||||
// - All the CREATE and DELETE changes associated with the user
|
||||
@@ -98,61 +135,125 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
// UPDATE changes do not have the user_id set because they are specific
|
||||
// to the item, not to a particular user.
|
||||
|
||||
const query = this
|
||||
.db('changes')
|
||||
.where(function() {
|
||||
void this.whereRaw('((type = ? OR type = ?) AND user_id = ?)', [ChangeType.Create, ChangeType.Delete, userId])
|
||||
// Need to use a RAW query here because Knex has a "not a
|
||||
// bug" bug that makes it go into infinite loop in some
|
||||
// contexts, possibly only when running inside Jest (didn't
|
||||
// test outside).
|
||||
// https://github.com/knex/knex/issues/1851
|
||||
.orWhereRaw('type = ? AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)', [ChangeType.Update, userId]);
|
||||
});
|
||||
// This used to be just one query but it kept getting slower and slower
|
||||
// as the `changes` table grew. So it is now split into two queries
|
||||
// merged by a UNION ALL.
|
||||
|
||||
if (count) {
|
||||
void query.countDistinct('id', { as: 'total' });
|
||||
const fields = [
|
||||
'id',
|
||||
'item_id',
|
||||
'item_name',
|
||||
'type',
|
||||
'updated_time',
|
||||
'counter',
|
||||
];
|
||||
|
||||
const fieldsSql = `"${fields.join('", "')}"`;
|
||||
|
||||
const subQuery1 = `
|
||||
SELECT ${fieldsSql}
|
||||
FROM "changes"
|
||||
WHERE counter > ?
|
||||
AND (type = ? OR type = ?)
|
||||
AND user_id = ?
|
||||
ORDER BY "counter" ASC
|
||||
${doCountQuery ? '' : 'LIMIT ?'}
|
||||
`;
|
||||
|
||||
const subParams1 = [
|
||||
fromCounter,
|
||||
ChangeType.Create,
|
||||
ChangeType.Delete,
|
||||
userId,
|
||||
];
|
||||
|
||||
if (!doCountQuery) subParams1.push(limit);
|
||||
|
||||
const subQuery2 = `
|
||||
SELECT ${fieldsSql}
|
||||
FROM "changes"
|
||||
WHERE counter > ?
|
||||
AND type = ?
|
||||
AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)
|
||||
ORDER BY "counter" ASC
|
||||
${doCountQuery ? '' : 'LIMIT ?'}
|
||||
`;
|
||||
|
||||
const subParams2 = [
|
||||
fromCounter,
|
||||
ChangeType.Update,
|
||||
userId,
|
||||
];
|
||||
|
||||
if (!doCountQuery) subParams2.push(limit);
|
||||
|
||||
let query: Knex.Raw<any> = null;
|
||||
|
||||
const finalParams = subParams1.concat(subParams2);
|
||||
|
||||
if (!doCountQuery) {
|
||||
finalParams.push(limit);
|
||||
|
||||
query = this.db.raw(`
|
||||
SELECT ${fieldsSql} FROM (${subQuery1}) as sub1
|
||||
UNION ALL
|
||||
SELECT ${fieldsSql} FROM (${subQuery2}) as sub2
|
||||
ORDER BY counter ASC
|
||||
LIMIT ?
|
||||
`, finalParams);
|
||||
} else {
|
||||
void query.select([
|
||||
'id',
|
||||
'item_id',
|
||||
'item_name',
|
||||
'type',
|
||||
'updated_time',
|
||||
]);
|
||||
query = this.db.raw(`
|
||||
SELECT count(*) as total
|
||||
FROM (
|
||||
(${subQuery1})
|
||||
UNION ALL
|
||||
(${subQuery2})
|
||||
) AS merged
|
||||
`, finalParams);
|
||||
}
|
||||
|
||||
return query;
|
||||
const results = await query;
|
||||
|
||||
// Because it's a raw query, we need to handle the results manually:
|
||||
// Postgres returns an object with a "rows" property, while SQLite
|
||||
// returns the rows directly;
|
||||
const output: Change[] = results.rows ? results.rows : results;
|
||||
|
||||
// This property is present only for the purpose of ordering the results
|
||||
// and can be removed afterwards.
|
||||
for (const change of output) delete change.counter;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async allByUser(userId: Uuid, pagination: Pagination = null): Promise<PaginatedDeltaChanges> {
|
||||
pagination = {
|
||||
page: 1,
|
||||
limit: 100,
|
||||
order: [{ by: 'counter', dir: PaginationOrderDir.ASC }],
|
||||
...pagination,
|
||||
};
|
||||
// public async allByUser(userId: Uuid, pagination: Pagination = null): Promise<PaginatedDeltaChanges> {
|
||||
// pagination = {
|
||||
// page: 1,
|
||||
// limit: 100,
|
||||
// order: [{ by: 'counter', dir: PaginationOrderDir.ASC }],
|
||||
// ...pagination,
|
||||
// };
|
||||
|
||||
const query = this.changesForUserQuery(userId, false);
|
||||
const countQuery = this.changesForUserQuery(userId, true);
|
||||
const itemCount = (await countQuery.first()).total;
|
||||
// const query = this.changesForUserQuery(userId, false);
|
||||
// const countQuery = this.changesForUserQuery(userId, true);
|
||||
// const itemCount = (await countQuery.first()).total;
|
||||
|
||||
void query
|
||||
.orderBy(pagination.order[0].by, pagination.order[0].dir)
|
||||
.offset((pagination.page - 1) * pagination.limit)
|
||||
.limit(pagination.limit) as any[];
|
||||
// void query
|
||||
// .orderBy(pagination.order[0].by, pagination.order[0].dir)
|
||||
// .offset((pagination.page - 1) * pagination.limit)
|
||||
// .limit(pagination.limit) as any[];
|
||||
|
||||
const changes = await query;
|
||||
// const changes = await query;
|
||||
|
||||
return {
|
||||
items: changes,
|
||||
// If we have changes, we return the ID of the latest changes from which delta sync can resume.
|
||||
// If there's no change, we return the previous cursor.
|
||||
cursor: changes.length ? changes[changes.length - 1].id : pagination.cursor,
|
||||
has_more: changes.length >= pagination.limit,
|
||||
page_count: itemCount !== null ? Math.ceil(itemCount / pagination.limit) : undefined,
|
||||
};
|
||||
}
|
||||
// return {
|
||||
// items: changes,
|
||||
// // If we have changes, we return the ID of the latest changes from which delta sync can resume.
|
||||
// // If there's no change, we return the previous cursor.
|
||||
// cursor: changes.length ? changes[changes.length - 1].id : pagination.cursor,
|
||||
// has_more: changes.length >= pagination.limit,
|
||||
// page_count: itemCount !== null ? Math.ceil(itemCount / pagination.limit) : undefined,
|
||||
// };
|
||||
// }
|
||||
|
||||
public async delta(userId: Uuid, pagination: ChangePagination = null): Promise<PaginatedDeltaChanges> {
|
||||
pagination = {
|
||||
@@ -167,18 +268,12 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
if (!changeAtCursor) throw new ErrorResyncRequired();
|
||||
}
|
||||
|
||||
const query = this.changesForUserQuery(userId, false);
|
||||
|
||||
// If a cursor was provided, apply it to the query.
|
||||
if (changeAtCursor) {
|
||||
void query.where('counter', '>', changeAtCursor.counter);
|
||||
}
|
||||
|
||||
void query
|
||||
.orderBy('counter', 'asc')
|
||||
.limit(pagination.limit) as any[];
|
||||
|
||||
const changes: Change[] = await query;
|
||||
const changes = await this.changesForUserQuery(
|
||||
userId,
|
||||
changeAtCursor ? changeAtCursor.counter : -1,
|
||||
pagination.limit,
|
||||
false,
|
||||
);
|
||||
|
||||
const items: Item[] = await this.db('items').select('id', 'jop_updated_time').whereIn('items.id', changes.map(c => c.item_id));
|
||||
|
||||
|
@@ -21,13 +21,15 @@ export default class TaskStateModel extends BaseModel<TaskState> {
|
||||
}
|
||||
|
||||
public async init(taskId: TaskId) {
|
||||
const taskState: TaskState = await this.loadByTaskId(taskId);
|
||||
if (taskState) return taskState;
|
||||
return this.withTransaction(async () => {
|
||||
const taskState: TaskState = await this.loadByTaskId(taskId);
|
||||
if (taskState) return taskState;
|
||||
|
||||
return this.save({
|
||||
task_id: taskId,
|
||||
enabled: 1,
|
||||
running: 0,
|
||||
return this.save({
|
||||
task_id: taskId,
|
||||
enabled: 1,
|
||||
running: 0,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -186,6 +186,9 @@ export default class UserItemModel extends BaseModel<UserItem> {
|
||||
for (const userItem of userItems) {
|
||||
const item = items.find(i => i.id === userItem.item_id);
|
||||
|
||||
// The item may have been deleted between the async calls above
|
||||
if (!item) continue;
|
||||
|
||||
if (options.recordChanges && this.models().item().shouldRecordChange(item.name)) {
|
||||
await this.models().change().save({
|
||||
item_type: ItemType.UserItem,
|
||||
|
@@ -428,9 +428,14 @@ describe('UserModel', () => {
|
||||
test('should throw an error if the password being saved seems to be hashed', async () => {
|
||||
const passwordSimilarToHash = '$2a$10';
|
||||
|
||||
const error = await checkThrowAsync(async () => await models().user().save({ password: passwordSimilarToHash }));
|
||||
const user = await models().user().save({
|
||||
email: 'test@example.com',
|
||||
password: '111111',
|
||||
});
|
||||
|
||||
expect(error.message).toBe('Unable to save user because password already seems to be hashed. User id: undefined');
|
||||
const error = await checkThrowAsync(async () => await models().user().save({ id: user.id, password: passwordSimilarToHash }));
|
||||
|
||||
expect(error.message).toBe(`Unable to save user because password already seems to be hashed. User id: ${user.id}`);
|
||||
expect(error instanceof ErrorBadRequest).toBe(true);
|
||||
});
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user