Compare commits
83 Commits
server-v3.
...
server-v3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e80db6afb5 | ||
|
|
6a06922633 | ||
|
|
fd02d88739 | ||
|
|
dacd460f64 | ||
|
|
3279485f44 | ||
|
|
eaf8d15be7 | ||
|
|
6b186b965a | ||
|
|
7a8ac14c99 | ||
|
|
73291fa355 | ||
|
|
27ff8be432 | ||
|
|
0904838311 | ||
|
|
2798cc6027 | ||
|
|
1ede5bc499 | ||
|
|
418a660a66 | ||
|
|
5bc073e888 | ||
|
|
87b443e051 | ||
|
|
8e36644068 | ||
|
|
1833de789a | ||
|
|
0b18fd988b | ||
|
|
2ce65b9315 | ||
|
|
8f4f0ee321 | ||
|
|
6a83cc95ee | ||
|
|
5134b63075 | ||
|
|
74527d7006 | ||
|
|
ad909ac6f0 | ||
|
|
5ff0285b85 | ||
|
|
bcb509a965 | ||
|
|
075c98175e | ||
|
|
212112d4b6 | ||
|
|
74bf0cb655 | ||
|
|
b2bdf84f06 | ||
|
|
a2156a0548 | ||
|
|
620afdaab1 | ||
|
|
3f8928000e | ||
|
|
5caec161f2 | ||
|
|
daab2223e7 | ||
|
|
f96071870c | ||
|
|
5e08abb7a9 | ||
|
|
2c71557d88 | ||
|
|
d551963669 | ||
|
|
7dae90c9f3 | ||
|
|
46820fb21b | ||
|
|
a18e49ab54 | ||
|
|
2c6eaca442 | ||
|
|
44de1246d9 | ||
|
|
ab3a0ab69f | ||
|
|
896f0e0bc5 | ||
|
|
e2c933db82 | ||
|
|
30c5031611 | ||
|
|
e7f14a0995 | ||
|
|
319bf79bc1 | ||
|
|
02f94adb96 | ||
|
|
2370c12129 | ||
|
|
8d074a563b | ||
|
|
1014edfdeb | ||
|
|
364bdd9bb0 | ||
|
|
8d6b219191 | ||
|
|
2455245f86 | ||
|
|
c669a3986e | ||
|
|
5f1a1e50d9 | ||
|
|
819a591cc0 | ||
|
|
421b82c86d | ||
|
|
16169b2780 | ||
|
|
49ed4ae920 | ||
|
|
13777d261c | ||
|
|
1c7b0e6266 | ||
|
|
4589670126 | ||
|
|
b6ab6e0b46 | ||
|
|
9b28b618bb | ||
|
|
bf7cc6be03 | ||
|
|
e5e5b342a7 | ||
|
|
9709721a73 | ||
|
|
a34010ef62 | ||
|
|
9a6043e6a6 | ||
|
|
992bf683c4 | ||
|
|
b40c2b8a41 | ||
|
|
8dcd08e21d | ||
|
|
cb2b32520d | ||
|
|
315b1d8275 | ||
|
|
8018f1269a | ||
|
|
c2d186188b | ||
|
|
d5798e558b | ||
|
|
224bcd54f1 |
@@ -181,6 +181,7 @@ packages/app-desktop/commands/openProfileDirectory.js
|
||||
packages/app-desktop/commands/openSecondaryAppInstance.js
|
||||
packages/app-desktop/commands/replaceMisspelling.js
|
||||
packages/app-desktop/commands/restoreNoteRevision.js
|
||||
packages/app-desktop/commands/showProfileEditor.js
|
||||
packages/app-desktop/commands/startExternalEditing.js
|
||||
packages/app-desktop/commands/stopExternalEditing.js
|
||||
packages/app-desktop/commands/switchProfile.js
|
||||
@@ -390,6 +391,7 @@ packages/app-desktop/gui/PopupNotification/NotificationItem.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
||||
packages/app-desktop/gui/PopupNotification/types.js
|
||||
packages/app-desktop/gui/ProfileEditor.js
|
||||
packages/app-desktop/gui/PromptDialog.js
|
||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
@@ -1028,6 +1030,7 @@ packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
|
||||
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
|
||||
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
|
||||
packages/editor/CodeMirror/extensions/editorSettingsExtension.js
|
||||
packages/editor/CodeMirror/extensions/highlightActiveLineExtension.js
|
||||
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
|
||||
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
|
||||
@@ -1152,6 +1155,7 @@ packages/editor/ProseMirror/utils/dom/showModal.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||
packages/editor/ProseMirror/utils/forEachHeading.js
|
||||
packages/editor/ProseMirror/utils/getTextBetween.js
|
||||
packages/editor/ProseMirror/utils/insertRenderedMarkdown.js
|
||||
packages/editor/ProseMirror/utils/jumpToHash.js
|
||||
packages/editor/ProseMirror/utils/makeLinksClickableInElement.js
|
||||
@@ -1853,6 +1857,7 @@ packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.test.js
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/licenses/buildReport.js
|
||||
packages/tools/licenses/getLicenses.js
|
||||
packages/tools/licenses/licenseChecker.js
|
||||
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js
|
||||
|
||||
5
.gitignore
vendored
@@ -153,6 +153,7 @@ packages/app-desktop/commands/openProfileDirectory.js
|
||||
packages/app-desktop/commands/openSecondaryAppInstance.js
|
||||
packages/app-desktop/commands/replaceMisspelling.js
|
||||
packages/app-desktop/commands/restoreNoteRevision.js
|
||||
packages/app-desktop/commands/showProfileEditor.js
|
||||
packages/app-desktop/commands/startExternalEditing.js
|
||||
packages/app-desktop/commands/stopExternalEditing.js
|
||||
packages/app-desktop/commands/switchProfile.js
|
||||
@@ -362,6 +363,7 @@ packages/app-desktop/gui/PopupNotification/NotificationItem.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
||||
packages/app-desktop/gui/PopupNotification/types.js
|
||||
packages/app-desktop/gui/ProfileEditor.js
|
||||
packages/app-desktop/gui/PromptDialog.js
|
||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
@@ -1000,6 +1002,7 @@ packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
|
||||
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
|
||||
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
|
||||
packages/editor/CodeMirror/extensions/editorSettingsExtension.js
|
||||
packages/editor/CodeMirror/extensions/highlightActiveLineExtension.js
|
||||
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
|
||||
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
|
||||
@@ -1124,6 +1127,7 @@ packages/editor/ProseMirror/utils/dom/showModal.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||
packages/editor/ProseMirror/utils/forEachHeading.js
|
||||
packages/editor/ProseMirror/utils/getTextBetween.js
|
||||
packages/editor/ProseMirror/utils/insertRenderedMarkdown.js
|
||||
packages/editor/ProseMirror/utils/jumpToHash.js
|
||||
packages/editor/ProseMirror/utils/makeLinksClickableInElement.js
|
||||
@@ -1825,6 +1829,7 @@ packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.test.js
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/licenses/buildReport.js
|
||||
packages/tools/licenses/getLicenses.js
|
||||
packages/tools/licenses/licenseChecker.js
|
||||
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js
|
||||
|
||||
BIN
Assets/Forum/Christmas/ForumChristmasBackgroundDark.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
Assets/Forum/Christmas/ForumChristmasBackgroundLight.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
Assets/ImageSources/Android/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
Assets/ImageSources/Android/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
Assets/ImageSources/Android/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Assets/ImageSources/Android/ic_launcher_foreground_drawable.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
Assets/ImageSources/Android/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -198,12 +198,21 @@ else
|
||||
fi
|
||||
|
||||
# Check if it's in the latest version
|
||||
if [[ -e "${INSTALL_DIR}/VERSION" ]] && [[ $(< "${INSTALL_DIR}/VERSION") == "${RELEASE_VERSION}" ]]; then
|
||||
print "${COLOR_GREEN}You already have the latest version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
|
||||
([[ "$FORCE" == true ]] && print "Forcing installation...") || exit 0
|
||||
if [[ -e "${INSTALL_DIR}/VERSION" ]]; then
|
||||
CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
|
||||
VERSION_COMPARISON=$(compareVersions "$CURRENT_VERSION" "$RELEASE_VERSION")
|
||||
|
||||
if [[ "$VERSION_COMPARISON" == "0" ]]; then
|
||||
print "${COLOR_GREEN}You already have the latest version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
|
||||
([[ "$FORCE" == true ]] && print "Forcing installation...") || exit 0
|
||||
elif [[ "$VERSION_COMPARISON" == "1" ]]; then
|
||||
print "${COLOR_YELLOW}You have version ${CURRENT_VERSION} installed, which is newer than the latest published version ${RELEASE_VERSION}.${COLOR_RESET}"
|
||||
print "${COLOR_YELLOW}Skipping installation to avoid downgrade.${COLOR_RESET}"
|
||||
else
|
||||
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION} installed."
|
||||
fi
|
||||
else
|
||||
[[ -e "${INSTALL_DIR}/VERSION" ]] && CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
|
||||
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION:-no version} installed."
|
||||
print "The latest version is ${RELEASE_VERSION}, but you have no version installed."
|
||||
fi
|
||||
|
||||
# Check if it's an update or a new install
|
||||
@@ -275,7 +284,7 @@ if command -v lsb_release &> /dev/null; then
|
||||
# without writing the AppImage to a non-user-writable location (without invalidating other security
|
||||
# controls). See https://discourse.joplinapp.org/t/possible-future-requirement-for-no-sandbox-flag-for-ubuntu-23-10/.
|
||||
HAS_USERNS_RESTRICTIONS=false
|
||||
if [[ "$DISTVER" =~ ^Ubuntu && $DISTMAJOR -ge 23 ]]; then
|
||||
if [[ "$DISTVER" =~ ^(Ubuntu|Tuxedo) && $DISTMAJOR -ge 23 ]]; then
|
||||
HAS_USERNS_RESTRICTIONS=true
|
||||
fi
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"vips.dev": {
|
||||
"platforms": ["aarch64-darwin"],
|
||||
},
|
||||
"nodejs": "24.4.1",
|
||||
"nodejs": "24.5.0",
|
||||
"pkg-config": "latest",
|
||||
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
|
||||
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
@@ -95,7 +95,7 @@
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"eslint-plugin-github": "4.10.2",
|
||||
"http-server": "14.1.1",
|
||||
"node-gyp": "11.3.0",
|
||||
"node-gyp": "11.4.2",
|
||||
"nodemon": "3.1.10"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
|
||||
@@ -107,6 +107,7 @@ class Command extends BaseCommand {
|
||||
userContentBaseUrl: () => joplinServerAuth.userContentBaseUrl,
|
||||
username: () => joplinServerAuth.email,
|
||||
password: () => joplinServerAuth.password,
|
||||
apiKey: () => '',
|
||||
session: (): Session => null,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import app from '../app';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import BaseCommand from '../base-command';
|
||||
import setupCommand from '../setupCommand';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
||||
export const setupCommandForTesting = (CommandClass: any, stdout: Function = null): BaseCommand => {
|
||||
@@ -18,4 +19,9 @@ export const setupApplication = async () => {
|
||||
|
||||
// Some tests also need access to the Redux store
|
||||
app().initRedux();
|
||||
|
||||
// Since the settings need to be loaded before the store is created, it will never
|
||||
// receive the SETTING_UPDATE_ALL event, which means state.settings will not be
|
||||
// initialised. So we manually call dispatchUpdateAll() to force an update.
|
||||
Setting.dispatchUpdateAll();
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"chalk": "4.1.2",
|
||||
"compare-version": "0.1.2",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "11.2.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"html-entities": "1.4.0",
|
||||
"keytar": "7.9.0",
|
||||
"md5": "2.3.0",
|
||||
@@ -57,7 +57,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"redux": "4.2.1",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.34.3",
|
||||
"sharp": "0.34.4",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<ul>
|
||||
<li><a href="https://example.com/" title="This
|
||||
|
||||
is a test title
|
||||
testing!
|
||||
|
||||
Test...">Test!</a></li>
|
||||
<li><a href="http://example.com" title="
|
||||
Test
|
||||
">Another test...</a></li>
|
||||
</ul>
|
||||
@@ -0,0 +1,5 @@
|
||||
- [Test!](https://example.com/ "This
|
||||
is a test title
|
||||
testing!
|
||||
Test...")
|
||||
- [Another test...](http://example.com "Test")
|
||||
74
packages/app-cli/tests/html_to_md/wikipedia_math_inline.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!-- From https://en.wikipedia.org/wiki/Collatz_conjecture -->
|
||||
<math display="block" xmlns="http://www.w3.org/1998/Math/MathML" alttext="{\displaystyle f(n)={\begin{cases}n/2&{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&{\text{if }}n\equiv 1{\pmod {2}}.\end{cases}}}">
|
||||
<semantics>
|
||||
<mrow class="MJX-TeXAtom-ORD">
|
||||
<mstyle displaystyle="true" scriptlevel="0">
|
||||
<mi>f</mi>
|
||||
<mo stretchy="false">(</mo>
|
||||
<mi>n</mi>
|
||||
<mo stretchy="false">)</mo>
|
||||
<mo>=</mo>
|
||||
<mrow class="MJX-TeXAtom-ORD">
|
||||
<mrow>
|
||||
<mo>{</mo>
|
||||
<mtable columnalign="left left" rowspacing=".2em" columnspacing="1em" displaystyle="false">
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mi>n</mi>
|
||||
<mrow class="MJX-TeXAtom-ORD">
|
||||
<mo>/</mo>
|
||||
</mrow>
|
||||
<mn>2</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mrow class="MJX-TeXAtom-ORD">
|
||||
<mtext>if </mtext>
|
||||
</mrow>
|
||||
<mi>n</mi>
|
||||
<mo>\u2261</mo>
|
||||
<mn>0</mn>
|
||||
<mrow class="MJX-TeXAtom-ORD">
|
||||
<mspace width="0.444em"></mspace>
|
||||
<mo stretchy="false">(</mo>
|
||||
<mi>mod</mi>
|
||||
<mspace width="0.333em"></mspace>
|
||||
<mn>2</mn>
|
||||
<mo stretchy="false">)</mo>
|
||||
</mrow>
|
||||
<mo>,</mo>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>3</mn>
|
||||
<mi>n</mi>
|
||||
<mo>+</mo>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mrow class="MJX-TeXAtom-ORD">
|
||||
<mtext>if </mtext>
|
||||
</mrow>
|
||||
<mi>n</mi>
|
||||
<mo>\u2261</mo>
|
||||
<mn>1</mn>
|
||||
<mrow class="MJX-TeXAtom-ORD">
|
||||
<mspace width="0.444em"></mspace>
|
||||
<mo stretchy="false">(</mo>
|
||||
<mi>mod</mi>
|
||||
<mspace width="0.333em"></mspace>
|
||||
<mn>2</mn>
|
||||
<mo stretchy="false">)</mo>
|
||||
</mrow>
|
||||
<mo>.</mo>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo fence="true" stretchy="true" symmetric="true"></mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">{\displaystyle f(n)={\begin{cases}n/2&{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&{\text{if }}n\equiv 1{\pmod {2}}.\end{cases}}}</annotation>
|
||||
</semantics>
|
||||
</math></span><img src="/some/src/here" class="mwe-math-fallback-image-display mw-invert skin-invert" aria-hidden="true" style="vertical-align: -3.171ex; width:45.735ex; height:7.509ex;"/>
|
||||
@@ -0,0 +1 @@
|
||||
${\displaystyle f(n)={\begin{cases}n/2&{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&{\text{if }}n\equiv 1{\pmod {2}}.\end{cases}}}$
|
||||
BIN
packages/app-cli/tests/support/onenote/Math.one
Normal file
3
packages/app-cli/tests/support/test_notes/md/long-url.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# test for joplin import
|
||||
|
||||
[https://l.facebook.com/l.php?u=https%3A%2F%2Fix.sk%2FNiBZH%3Futm\_source%3DYouTube%2520Instagram%26utm\_medium%3D2HIqFSGVVB2mFsVTJClrQ7ZnuGJaUt6hu1MNH0vUMjcrgWnUsK%26utm\_campaign%3D%25F0%259F%2598%25A9%25F0%259F%2598%258E%25F0%259F%2598%25BF%25F0%259F%25A4%2594%25F0%259F%2598%25A9%25F0%259F%2599%2583%25F0%259F%25A4%25AF%25F0%259F%25A5%25B0%25F0%259F%2598%25AB%25F0%259F%2598%25BA%26utm\_id%3D%25F0%259F%2598%258B%25F0%259F%2598%25A5%25F0%259F%25A4%25A1%25F0%259F%2598%25A0%25F0%259F%2598%2587%25F0%259F%25A5%25B4%25F0%259F%25A7%2590%25F0%259F%2598%258E%25F0%259F%2598%2582%25F0%259F%2598%259E%26utm\_term%3D%25F0%259F%2598%2584%25F0%259F%25A4%25A9%25F0%259F%2599%2580%25F0%259F%2598%2593%25F0%259F%25A4%25AF%25F0%259F%25A4%25A5%25F0%259F%2591%25BE%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%25A4%25A5%26utm\_content%3D%25F0%259F%2591%25BD%25F0%259F%2598%25AB%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%2598%25A9%25F0%259F%2599%2589%26fbclid%3DIwAR0I3l5DBLypLaTjDTCGPQ1i1MmPB2-pE8iqrxrgUK9Kkvq3OX5Mjejibzw&h=AT3nNxW4G-9nAkhXU1EVN-aVGl1o\_-DzDAaWFx9xbprpN3JRBOh17lCQQHNAlIMv6iE4P2vobBAAivLvdzy00K8xqIqb-CvGj6YnnBX6R9wwtj5Y&\_\_tn\_\_=H-y-R&c[0]=AT0eE6OXx\_t9HzpPmMgTdOWAw2ZRNPRDIHJWf699NZYkYzugbWS6g3rOndhPA8fwrCIgk1zn2D1To7phLW9wXkqfgZU1ayT3887\_dxrfN-x822Pos0lCjTIhoQcxfBl516pTz1XrRG\_MbtPpLzUFAGu4nw5W86UR1EkBCZhustNbgTX4wVReiVSuwAWu7Sp1yiWvUm5JXlo76663333hhsgsu](<https://l.facebook.com/l.php?u=https%3A%2F%2Fix.sk%2FNiBZH%3Futm_source%3DYouTube%2520Instagram%26utm_medium%3D2HIqFSGVVB2mFsVTJClrQ7ZnuGJaUt6hu1MNH0vUMjcrgWnUsK%26utm_campaign%3D%25F0%259F%2598%25A9%25F0%259F%2598%258E%25F0%259F%2598%25BF%25F0%259F%25A4%2594%25F0%259F%2598%25A9%25F0%259F%2599%2583%25F0%259F%25A4%25AF%25F0%259F%25A5%25B0%25F0%259F%2598%25AB%25F0%259F%2598%25BA%26utm_id%3D%25F0%259F%2598%258B%25F0%259F%2598%25A5%25F0%259F%25A4%25A1%25F0%259F%2598%25A0%25F0%259F%2598%2587%25F0%259F%25A5%25B4%25F0%259F%25A7%2590%25F0%259F%2598%258E%25F0%259F%2598%2582%25F0%259F%2598%259E%26utm_term%3D%25F0%259F%2598%2584%25F0%259F%25A4%25A9%25F0%259F%2599%2580%25F0%259F%2598%2593%25F0%259F%25A4%25AF%25F0%259F%25A4%25A5%25F0%259F%2591%25BE%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%25A4%25A5%26utm_content%3D%25F0%259F%2591%25BD%25F0%259F%2598%25AB%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%2598%25A9%25F0%259F%2599%2589%26fbclid%3DIwAR0I3l5DBLypLaTjDTCGPQ1i1MmPB2-pE8iqrxrgUK9Kkvq3OX5Mjejibzw&h=AT3nNxW4G-9nAkhXU1EVN-aVGl1o_-DzDAaWFx9xbprpN3JRBOh17lCQQHNAlIMv6iE4P2vobBAAivLvdzy00K8xqIqb-CvGj6YnnBX6R9wwtj5Y&__tn__=H-y-R&c[0]=AT0eE6OXx_t9HzpPmMgTdOWAw2ZRNPRDIHJWf699NZYkYzugbWS6g3rOndhPA8fwrCIgk1zn2D1To7phLW9wXkqfgZU1ayT3887_dxrfN-x822Pos0lCjTIhoQcxfBl516pTz1XrRG_MbtPpLzUFAGu4nw5W86UR1EkBCZhustNbgTX4wVReiVSuwAWu7Sp1yiWvUm5JXlo>)
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
id: 20250821081408
|
||||
date: 2025-08-21
|
||||
keywords:
|
||||
---
|
||||
|
||||
# A test file for Joplin importer
|
||||
|
||||
Test
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: test
|
||||
created: 2025-07-22 17:30:44Z
|
||||
updated: 2025-07-22 17:37:48Z
|
||||
---
|
||||
|
||||
test
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Frontmatter test"
|
||||
created_at: 01-01-2024 01:23 AM
|
||||
updated_at: 02-01-2024 04:56 AM
|
||||
updated_at: 01-01-2024 04:56 AM
|
||||
---
|
||||
|
||||
# Frontmatter test
|
||||
|
||||
@@ -165,6 +165,10 @@
|
||||
if (a && a.toLowerCase().indexOf('math/tex') >= 0) isVisible = true;
|
||||
}
|
||||
|
||||
if (nodeName === 'annotation') {
|
||||
if (node.getAttribute('encoding') === 'application/x-tex') isVisible = true;
|
||||
}
|
||||
|
||||
if (nodeName === 'source' && nodeParentName === 'picture') {
|
||||
isVisible = false;
|
||||
}
|
||||
|
||||
@@ -281,14 +281,16 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
// As of Joplin 3.5.7, the ABC rendering is part of the app so we automatically disable the plugin
|
||||
pluginSettings = {
|
||||
...pluginSettings,
|
||||
['org.joplinapp.plugins.AbcSheetMusic']: {
|
||||
enabled: false,
|
||||
deleted: false,
|
||||
hasBeenUpdated: false,
|
||||
},
|
||||
};
|
||||
if (pluginSettings['org.joplinapp.plugins.AbcSheetMusic']) {
|
||||
pluginSettings = {
|
||||
...pluginSettings,
|
||||
['org.joplinapp.plugins.AbcSheetMusic']: {
|
||||
enabled: false,
|
||||
deleted: false,
|
||||
hasBeenUpdated: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as openProfileDirectory from './openProfileDirectory';
|
||||
import * as openSecondaryAppInstance from './openSecondaryAppInstance';
|
||||
import * as replaceMisspelling from './replaceMisspelling';
|
||||
import * as restoreNoteRevision from './restoreNoteRevision';
|
||||
import * as showProfileEditor from './showProfileEditor';
|
||||
import * as startExternalEditing from './startExternalEditing';
|
||||
import * as stopExternalEditing from './stopExternalEditing';
|
||||
import * as switchProfile from './switchProfile';
|
||||
@@ -38,6 +39,7 @@ const index: any[] = [
|
||||
openSecondaryAppInstance,
|
||||
replaceMisspelling,
|
||||
restoreNoteRevision,
|
||||
showProfileEditor,
|
||||
startExternalEditing,
|
||||
stopExternalEditing,
|
||||
switchProfile,
|
||||
|
||||
20
packages/app-desktop/commands/showProfileEditor.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'showProfileEditor',
|
||||
label: () => _('Manage profiles'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext) => {
|
||||
context.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'ProfileEditor',
|
||||
});
|
||||
},
|
||||
enabledCondition: 'hasMultiProfiles',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -852,7 +852,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
notesColumns: validateColumns(state.settings['notes.columns']),
|
||||
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
|
||||
toast: state.toast,
|
||||
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && process.arch !== 'arm64',
|
||||
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && shim.isMac() && process.arch !== 'arm64',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ const useSwitchProfileMenuItems = (profileConfig: ProfileConfig, menuItemDic: an
|
||||
|
||||
switchProfileMenuItems.push({ type: 'separator' });
|
||||
switchProfileMenuItems.push(menuItemDic.addProfile);
|
||||
switchProfileMenuItems.push(menuItemDic.editProfileConfig);
|
||||
switchProfileMenuItems.push(menuItemDic.showProfileEditor);
|
||||
|
||||
return switchProfileMenuItems;
|
||||
}, [profileConfig, menuItemDic]);
|
||||
|
||||
@@ -31,13 +31,15 @@ function markupToHtml() {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
function countElements(text: string, wordSetter: Function, characterSetter: Function, characterNoSpaceSetter: Function, lineSetter: Function) {
|
||||
function countElements(text: string, wordSetter: Function, characterSetter: Function, characterNoSpaceSetter: Function, cjkCharacterSetter: React.Dispatch<React.SetStateAction<number>>, lineSetter: Function) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
Countable.count(text, (counter: any) => {
|
||||
wordSetter(counter.words);
|
||||
characterSetter(counter.all);
|
||||
characterNoSpaceSetter(counter.characters);
|
||||
});
|
||||
const cjkMatches = text.match(/[\p{Script=Han}\p{Script=Bopomofo}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/gu);
|
||||
cjkCharacterSetter(cjkMatches ? cjkMatches.length : 0);
|
||||
lineSetter(text === '' ? 0 : text.split('\n').length);
|
||||
}
|
||||
|
||||
@@ -58,23 +60,25 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
||||
const [words, setWords] = useState<number>(0);
|
||||
const [characters, setCharacters] = useState<number>(0);
|
||||
const [charactersNoSpace, setCharactersNoSpace] = useState<number>(0);
|
||||
const [cjkCharacters, setCjkCharacters] = useState<number>(0);
|
||||
// For source with Markdown syntax stripped out
|
||||
const [strippedLines, setStrippedLines] = useState<number>(0);
|
||||
const [strippedWords, setStrippedWords] = useState<number>(0);
|
||||
const [strippedCharacters, setStrippedCharacters] = useState<number>(0);
|
||||
const [strippedCharactersNoSpace, setStrippedCharactersNoSpace] = useState<number>(0);
|
||||
const [strippedCjkCharacters, setStrippedCjkCharacters] = useState<number>(0);
|
||||
const [strippedReadTime, setStrippedReadTime] = useState<number>(0);
|
||||
// This amount based on the following paper:
|
||||
// https://www.researchgate.net/publication/332380784_How_many_words_do_we_read_per_minute_A_review_and_meta-analysis_of_reading_rate
|
||||
const wordsPerMinute = 250;
|
||||
|
||||
useEffect(() => {
|
||||
countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setLines);
|
||||
countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setCjkCharacters, setLines);
|
||||
}, [props.text]);
|
||||
|
||||
useEffect(() => {
|
||||
const strippedText: string = markupToHtml().stripMarkup(props.markupLanguage, props.text);
|
||||
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedLines);
|
||||
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedCjkCharacters, setStrippedLines);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.text]);
|
||||
|
||||
@@ -88,6 +92,7 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
||||
words: words,
|
||||
characters: characters,
|
||||
charactersNoSpace: charactersNoSpace,
|
||||
cjkCharacters: cjkCharacters,
|
||||
};
|
||||
|
||||
const strippedTextProperties: TextPropertiesMap = {
|
||||
@@ -99,12 +104,14 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
||||
words: strippedWords,
|
||||
characters: strippedCharacters,
|
||||
charactersNoSpace: strippedCharactersNoSpace,
|
||||
cjkCharacters: strippedCjkCharacters,
|
||||
};
|
||||
|
||||
const keyToLabel: KeyToLabelMap = {
|
||||
words: _('Words'),
|
||||
characters: _('Characters'),
|
||||
charactersNoSpace: _('Characters excluding spaces'),
|
||||
cjkCharacters: _('Chinese/Japanese/Korean characters'),
|
||||
lines: _('Lines'),
|
||||
};
|
||||
|
||||
@@ -147,6 +154,7 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
||||
);
|
||||
|
||||
for (const key in textProperties) {
|
||||
if (key === 'cjkCharacters' && textProperties[key] === 0 && strippedTextProperties[key] === 0) continue;
|
||||
const comp = createTableBodyRow(key, textProperties[key], strippedTextProperties[key]);
|
||||
tableBodyComps.push(comp);
|
||||
}
|
||||
|
||||
@@ -366,6 +366,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
ignoreModifiers: true,
|
||||
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
|
||||
keymap: keyboardMode,
|
||||
preferMacShortcuts: shim.isMac(),
|
||||
indentWithTabs: true,
|
||||
tabMovesFocus: props.tabMovesFocus,
|
||||
editorLabel: _('Markdown editor'),
|
||||
|
||||
@@ -1051,6 +1051,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
editor,
|
||||
});
|
||||
|
||||
const noteChangeTimeRef = useRef(Date.now());
|
||||
const lastNoteIdRef = useRef(props.noteId);
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
@@ -1068,6 +1069,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
// Use nextOnChangeEventInfo's noteId -- lastOnChangeEventInfo can be slightly out-of-date.
|
||||
const differentNoteId = lastNoteIdRef.current !== props.noteId;
|
||||
const differentContent = lastOnChangeEventInfo.current.content !== props.content;
|
||||
|
||||
if (differentNoteId) noteChangeTimeRef.current = Date.now();
|
||||
|
||||
if (differentNoteId || differentContent || !resourcesEqual) {
|
||||
const result = await props.markupToHtml(
|
||||
props.contentMarkupLanguage,
|
||||
@@ -1340,7 +1344,15 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
// keep it this way for now.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function onKeyUp(event: any) {
|
||||
if (['Backspace', 'Delete', 'Enter', 'Tab'].includes(event.key)) {
|
||||
const timeSinceNoteChange = Date.now() - noteChangeTimeRef.current;
|
||||
|
||||
// A key that is pressed before the editor is opened, and that is released after it is
|
||||
// opened is going to be processed here. For example if the user presses Enter in
|
||||
// GotoAnything to arrive here. But in that case, we don't want the change handler to be
|
||||
// activated, because that would change the note timestamp. So we take into account how
|
||||
// long the note has been loaded before we process the key. Fixes
|
||||
// https://github.com/laurent22/joplin/issues/12367
|
||||
if (['Backspace', 'Delete', 'Enter', 'Tab'].includes(event.key) && timeSinceNoteChange > 200) {
|
||||
onChangeHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ import WarningBanner from './WarningBanner/WarningBanner';
|
||||
import UserWebview from '../../services/plugins/UserWebview';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import usePluginEditorView from './utils/usePluginEditorView';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { defaultWindowId, stateUtils } from '@joplin/lib/reducer';
|
||||
import { WindowIdContext } from '../NewWindowOrIFrame';
|
||||
import useResourceUnwatcher from './utils/useResourceUnwatcher';
|
||||
import StatusBar from './StatusBar';
|
||||
@@ -722,6 +722,8 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
bodyEditor = 'CodeMirror5';
|
||||
}
|
||||
|
||||
const mainWindowState = stateUtils.windowStateById(state, defaultWindowId);
|
||||
|
||||
return {
|
||||
noteId,
|
||||
bodyEditor,
|
||||
@@ -740,7 +742,9 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
customCss: state.customViewerCss,
|
||||
noteVisiblePanes: windowState.noteVisiblePanes,
|
||||
watchedResources: windowState.watchedResources,
|
||||
highlightedWords: state.highlightedWords,
|
||||
// For now, only the main window has search UI. Show the same search markers in all
|
||||
// windows:
|
||||
highlightedWords: mainWindowState.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
|
||||
47
packages/app-desktop/gui/ProfileEditor.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
.profile-management {
|
||||
font-family: var(--joplin-font-family);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .tableContainer {
|
||||
overflow-y: scroll;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 0%;
|
||||
color: var(--joplin-color);
|
||||
|
||||
> .notification {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-table {
|
||||
width: 100%;
|
||||
|
||||
> thead > tr > .headerCell {
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
> tbody > tr {
|
||||
> .nameCell {
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
max-width: 1px;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> .dataCell {
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
color: var(--joplin-color-faded);
|
||||
}
|
||||
|
||||
> .profileActions > button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
193
packages/app-desktop/gui/ProfileEditor.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, CSSProperties } from 'react';
|
||||
import ButtonBar from './ConfigScreen/ButtonBar';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { connect } from 'react-redux';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import bridge from '../services/bridge';
|
||||
import dialogs from './dialogs';
|
||||
import { Profile, ProfileConfig } from '@joplin/lib/services/profileConfig/types';
|
||||
import { deleteProfileById, saveProfileConfig } from '@joplin/lib/services/profileConfig';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
const logger = Logger.create('ProfileEditor');
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Dispatch;
|
||||
style: CSSProperties;
|
||||
profileConfig: ProfileConfig;
|
||||
}
|
||||
|
||||
interface ProfileTableProps {
|
||||
profiles: Profile[];
|
||||
currentProfileId: string;
|
||||
onProfileRename: (profile: Profile)=> void;
|
||||
onProfileDelete: (profile: Profile)=> void;
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
const ProfileTableComp: React.FC<ProfileTableProps> = props => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
return (
|
||||
<table className="profile-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="headerCell">{_('Profile name')}</th>
|
||||
<th className="headerCell">{_('ID')}</th>
|
||||
<th className="headerCell">{_('Status')}</th>
|
||||
<th className="headerCell">{_('Actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.profiles.map((profile: Profile, index: number) => {
|
||||
const isCurrentProfile = profile.id === props.currentProfileId;
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td id={`name-${profile.id}`} className="nameCell">
|
||||
<span style={{ fontWeight: isCurrentProfile ? 'bold' : 'normal' }}>
|
||||
{profile.name || `(${_('Untitled')})`}
|
||||
</span>
|
||||
</td>
|
||||
<td className="dataCell">{profile.id}</td>
|
||||
<td className="dataCell">
|
||||
{isCurrentProfile ? _('Active') : ''}
|
||||
</td>
|
||||
<td className="dataCell profileActions">
|
||||
<button
|
||||
id={`rename-${profile.id}`}
|
||||
aria-labelledby={`rename-${profile.id} name-${profile.id}`}
|
||||
style={theme.buttonStyle}
|
||||
onClick={() => props.onProfileRename(profile)}
|
||||
>
|
||||
{_('Rename')}
|
||||
</button>
|
||||
{!isCurrentProfile && (
|
||||
<button
|
||||
id={`delete-${profile.id}`}
|
||||
aria-labelledby={`delete-${profile.id} name-${profile.id}`}
|
||||
style={theme.buttonStyle}
|
||||
onClick={() => props.onProfileDelete(profile)}
|
||||
>
|
||||
{_('Delete')}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileEditorComponent: React.FC<Props> = props => {
|
||||
const { profileConfig, themeId, dispatch } = props;
|
||||
const theme = themeStyle(themeId);
|
||||
const style = props.style;
|
||||
const containerHeight = style.height;
|
||||
|
||||
const [profiles, setProfiles] = useState<Profile[]>(profileConfig.profiles);
|
||||
|
||||
useEffect(() => {
|
||||
setProfiles(profileConfig.profiles);
|
||||
}, [profileConfig]);
|
||||
|
||||
const saveNewProfileConfig = async (makeNewProfileConfig: ()=> ProfileConfig) => {
|
||||
try {
|
||||
const newProfileConfig = makeNewProfileConfig();
|
||||
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newProfileConfig);
|
||||
dispatch({
|
||||
type: 'PROFILE_CONFIG_SET',
|
||||
value: newProfileConfig,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onProfileRename = async (profile: Profile) => {
|
||||
const newName = await dialogs.prompt(_('Profile name:'), '', profile.name);
|
||||
if (newName === null || newName === undefined || newName === profile.name) return;
|
||||
|
||||
if (!newName.trim()) {
|
||||
bridge().showErrorMessageBox(_('Profile name cannot be empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
const makeNewProfileConfig = () => {
|
||||
const newProfiles = profileConfig.profiles.map(p =>
|
||||
p.id === profile.id ? { ...p, name: newName.trim() } : p,
|
||||
);
|
||||
|
||||
const newProfileConfig = {
|
||||
...profileConfig,
|
||||
profiles: newProfiles,
|
||||
};
|
||||
|
||||
return newProfileConfig;
|
||||
};
|
||||
|
||||
await saveNewProfileConfig(makeNewProfileConfig);
|
||||
};
|
||||
|
||||
const onProfileDelete = async (profile: Profile) => {
|
||||
const isCurrentProfile = profile.id === profileConfig.currentProfileId;
|
||||
if (isCurrentProfile) {
|
||||
bridge().showErrorMessageBox(_('The active profile cannot be deleted. Switch to a different profile and try again.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = bridge().showConfirmMessageBox(_('Delete profile "%s"?\n\nAll data, including notes, notebooks and tags will be permanently deleted.', profile.name), {
|
||||
buttons: [_('Delete'), _('Cancel')],
|
||||
defaultId: 1,
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
const rootDir = Setting.value('rootProfileDir');
|
||||
const profileDir = `${rootDir}/profile-${profile.id}`;
|
||||
|
||||
try {
|
||||
await shim.fsDriver().remove(profileDir);
|
||||
logger.info('Deleted profile directory: ', profileDir);
|
||||
} catch (error) {
|
||||
logger.error('Error deleting profile directory: ', error);
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
|
||||
await saveNewProfileConfig(() => deleteProfileById(profileConfig, profile.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="profile-management" style={{ ...theme.containerStyle, height: containerHeight }}>
|
||||
<div className="tableContainer">
|
||||
<div className="notification" style={theme.notificationBox}>
|
||||
{_('Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.')}
|
||||
</div>
|
||||
<ProfileTableComp
|
||||
themeId={themeId}
|
||||
profiles={profiles}
|
||||
currentProfileId={profileConfig.currentProfileId}
|
||||
onProfileRename={onProfileRename}
|
||||
onProfileDelete={onProfileDelete}
|
||||
/>
|
||||
</div>
|
||||
<ButtonBar
|
||||
onCancelClick={() => dispatch({ type: 'NAV_BACK' })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
themeId: state.settings.theme,
|
||||
profileConfig: state.profileConfig,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(ProfileEditorComponent);
|
||||
@@ -84,7 +84,13 @@ const ResourceTableComp = (props: ResourceTable) => {
|
||||
};
|
||||
|
||||
const filteredResources = props.resources.filter(
|
||||
(resource: InnerResource) => !props.filter || resource.title?.includes(props.filter) || resource.id.includes(props.filter),
|
||||
(resource: InnerResource) => {
|
||||
if (props.filter) {
|
||||
const filterLowerCase = props.filter.toLowerCase();
|
||||
return resource.title?.toLowerCase().includes(filterLowerCase) || resource.id.toLowerCase().includes(filterLowerCase);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
const renderSortableHeader = (title: string, order: SortingOrder) => {
|
||||
@@ -297,7 +303,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
<div style={{ ...theme.notificationBox, marginBottom: 10 }}>{
|
||||
_('This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.')
|
||||
}</div>
|
||||
<div style={{ float: 'right' }}>
|
||||
<p style={{ float: 'left', paddingRight: 10 }}>
|
||||
<input
|
||||
style={theme.inputStyle}
|
||||
type="search"
|
||||
@@ -305,7 +311,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
onChange={this.onFilterUpdate}
|
||||
placeholder={_('Search...')}
|
||||
/>
|
||||
</div>
|
||||
</p>
|
||||
{this.state.isLoading && <div>{_('Please wait...')}</div>}
|
||||
{!this.state.isLoading && <div>
|
||||
{!this.state.resources && <div>
|
||||
|
||||
@@ -20,6 +20,7 @@ import Dialog from './Dialog';
|
||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||
import ImportScreen from './ImportScreen';
|
||||
import ResourceScreen from './ResourceScreen';
|
||||
import ProfileEditor from './ProfileEditor';
|
||||
import Navigator from './Navigator';
|
||||
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
||||
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
|
||||
@@ -165,6 +166,7 @@ class RootComponent extends React.Component<Props, any> {
|
||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||
Config: { screen: ConfigScreen, title: () => _('Options') },
|
||||
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },
|
||||
ProfileEditor: { screen: ProfileEditor, title: () => _('Manage profiles') },
|
||||
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import DialogButtonRow from './DialogButtonRow';
|
||||
import { themeStyle, buildStyle } from '@joplin/lib/theme';
|
||||
import Dialog from './Dialog';
|
||||
import DialogTitle from './DialogTitle';
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
@@ -29,47 +28,6 @@ interface Props {
|
||||
syncTargetId: number;
|
||||
}
|
||||
|
||||
function styles_(props: Props) {
|
||||
return buildStyle('ShareNoteDialog', props.themeId, theme => {
|
||||
return {
|
||||
root: {
|
||||
minWidth: 500,
|
||||
},
|
||||
noteList: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
note: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
padding: '0.5em',
|
||||
marginBottom: 5,
|
||||
},
|
||||
noteTitle: {
|
||||
...theme.textStyle,
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
color: theme.color,
|
||||
},
|
||||
noteRemoveButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
},
|
||||
noteRemoveButtonIcon: {
|
||||
color: theme.color,
|
||||
fontSize: '1.4em',
|
||||
},
|
||||
copyShareLinkButton: {
|
||||
...theme.buttonStyle,
|
||||
marginBottom: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function ShareNoteDialog(props: Props) {
|
||||
const [notes, setNotes] = useState<NoteEntity[]>([]);
|
||||
const [recursiveShare, setRecursiveShare] = useState<boolean>(false);
|
||||
@@ -77,8 +35,6 @@ export function ShareNoteDialog(props: Props) {
|
||||
|
||||
const syncTargetInfo = useMemo(() => SyncTargetRegistry.infoById(props.syncTargetId), [props.syncTargetId]);
|
||||
const noteCount = notes.length;
|
||||
const theme = themeStyle(props.themeId);
|
||||
const styles = styles_(props);
|
||||
|
||||
useEffect(() => {
|
||||
void ShareService.instance().refreshShares();
|
||||
@@ -117,8 +73,8 @@ export function ShareNoteDialog(props: Props) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={note.id} style={styles.note}>
|
||||
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
|
||||
<div key={note.id} className='shared-note-list-item'>
|
||||
<span className='title'>{note.title}</span>{unshareButton}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -128,7 +84,7 @@ export function ShareNoteDialog(props: Props) {
|
||||
for (const note of notes) {
|
||||
noteComps.push(renderNote(note));
|
||||
}
|
||||
return <div style={styles.noteList}>{noteComps}</div>;
|
||||
return <div className="notes">{noteComps}</div>;
|
||||
};
|
||||
|
||||
const statusMessage = useShareStatusMessage({ sharesState, noteCount });
|
||||
@@ -136,7 +92,7 @@ export function ShareNoteDialog(props: Props) {
|
||||
|
||||
function renderEncryptionWarningMessage() {
|
||||
if (!encryptionWarning) return null;
|
||||
return <div style={theme.textStyle}>{encryptionWarning}<hr/></div>;
|
||||
return <div className="message">{encryptionWarning}<hr/></div>;
|
||||
}
|
||||
|
||||
const onRecursiveShareChange = useCallback(() => {
|
||||
@@ -155,12 +111,16 @@ export function ShareNoteDialog(props: Props) {
|
||||
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<div style={styles.root} className="form">
|
||||
<div className="form share-note-dialog">
|
||||
<DialogTitle title={_('Publish Notes')}/>
|
||||
{renderNoteList(notes)}
|
||||
{renderRecursiveShareCheckbox()}
|
||||
<button disabled={[SharingStatus.Creating, SharingStatus.Synchronizing].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
|
||||
<div style={theme.textStyle}>{statusMessage}</div>
|
||||
<button
|
||||
disabled={[SharingStatus.Creating, SharingStatus.Synchronizing].indexOf(sharesState) >= 0}
|
||||
className="share"
|
||||
onClick={shareLinkButton_click}
|
||||
>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
|
||||
<div className="message">{statusMessage}</div>
|
||||
{renderEncryptionWarningMessage()}
|
||||
<DialogButtonRow
|
||||
themeId={props.themeId}
|
||||
|
||||
@@ -10,8 +10,11 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import { PackageInfo } from '@joplin/lib/versionInfo';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { ImportModule } from '@joplin/lib/services/interop/Module';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
const packageInfo: PackageInfo = require('../../../packageInfo.js');
|
||||
|
||||
const logger = Logger.create('importFrom');
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'importFrom',
|
||||
label: () => _('Import...'),
|
||||
@@ -135,6 +138,7 @@ export const runtime = (control: WindowControl): CommandRuntime => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Import result: ', result);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function() {
|
||||
'commandPalette',
|
||||
'openMasterPasswordDialog',
|
||||
'addProfile',
|
||||
'editProfileConfig',
|
||||
'showProfileEditor',
|
||||
'switchProfile1',
|
||||
'switchProfile2',
|
||||
'switchProfile3',
|
||||
|
||||
16
packages/app-desktop/gui/styles/base-button.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
// Corresponds to theme.buttonStyle
|
||||
.base-button {
|
||||
border: 1px solid;
|
||||
min-height: 26px;
|
||||
min-width: 80px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
border-radius: 4px;
|
||||
|
||||
color: var(--joplin-color4);
|
||||
background-color: var(--joplin-background-color4);
|
||||
border-color: var(--joplin-border-color4);
|
||||
user-select: none;
|
||||
}
|
||||
8
packages/app-desktop/gui/styles/base-text.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
// Corresponds to theme.textStyle
|
||||
.base-text {
|
||||
font-family: var(--joplin-font-family);
|
||||
font-size: var(--joplin-font-size);
|
||||
line-height: 1.6em;
|
||||
color: var(--joplin-color);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
@use './base-text.scss';
|
||||
@use './base-button.scss';
|
||||
@use './dialog-modal-layer.scss';
|
||||
@use './user-webview-dialog.scss';
|
||||
@use './prompt-dialog.scss';
|
||||
@@ -19,3 +21,5 @@
|
||||
@use './popup-notification-list.scss';
|
||||
@use './popup-notification-item.scss';
|
||||
@use './multi-note-actions.scss';
|
||||
@use './share-note-dialog.scss';
|
||||
@use './shared-note-list-item.scss';
|
||||
|
||||
19
packages/app-desktop/gui/styles/share-note-dialog.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
@use "./base-button.scss";
|
||||
@use "./base-text.scss";
|
||||
|
||||
.share-note-dialog {
|
||||
min-width: 500px;
|
||||
|
||||
> .message {
|
||||
@extend .base-text;
|
||||
}
|
||||
|
||||
> .notes {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
> .share {
|
||||
@extend .base-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
20
packages/app-desktop/gui/styles/shared-note-list-item.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
@use "./base-text.scss";
|
||||
|
||||
.shared-note-list-item {
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid;
|
||||
border-color: var(--joplin-divider-color);
|
||||
padding: 0.5em;
|
||||
margin-bottom: 5px;
|
||||
|
||||
> .title {
|
||||
@extend .base-text;
|
||||
|
||||
flex: 1;
|
||||
display: flex;
|
||||
color: var(--joplin-color);
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@playwright/test": "1.54.2",
|
||||
"@playwright/test": "1.55.0",
|
||||
"@sentry/electron": "4.24.0",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.14",
|
||||
@@ -209,7 +209,7 @@
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.3",
|
||||
"@joplin/onenote-converter": "~3.5",
|
||||
"fs-extra": "11.2.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"keytar": "7.9.0",
|
||||
"node-fetch": "2.6.7",
|
||||
"sqlite3": "5.1.6"
|
||||
|
||||
@@ -29,12 +29,9 @@ export interface Props {
|
||||
borderBottom?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
theme?: any;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onSubmit?: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onDismiss?: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onReady?: Function;
|
||||
onSubmit?: ()=> void;
|
||||
onDismiss?: ()=> void;
|
||||
onReady?: ()=> void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { RefObject } from 'react';
|
||||
import useMessageHandler from './useMessageHandler';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
||||
export default function(viewRef: RefObject<HTMLIFrameElement>, onSubmit: Function, onDismiss: Function) {
|
||||
type OnEvent = ()=> void;
|
||||
|
||||
export default function(viewRef: RefObject<HTMLIFrameElement>, onSubmit: OnEvent, onDismiss: OnEvent) {
|
||||
useMessageHandler(viewRef, event => {
|
||||
const message = event.data?.message;
|
||||
if (message === 'form-submit') {
|
||||
if (message === 'form-submit' && onSubmit) {
|
||||
onSubmit();
|
||||
} else if (message === 'dismiss') {
|
||||
} else if (message === 'dismiss' && onDismiss) {
|
||||
onDismiss();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@use 'gui/Sidebar/style.scss' as sidebar-styles;
|
||||
@use 'gui/NoteEditor/style.scss' as note-editor-styles;
|
||||
@use 'gui/KeymapConfig/style.scss' as keymap-styles;
|
||||
@use 'gui/ProfileEditor.scss' as profile-editor;
|
||||
@use 'services/plugins/styles/index.scss' as plugins-styles;
|
||||
@use 'gui/styles/index.scss' as gui-styles;
|
||||
@use 'main.scss' as main;
|
||||
|
||||
@@ -89,8 +89,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097781
|
||||
versionName "3.5.1"
|
||||
versionCode 2097783
|
||||
versionName "3.5.3"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
@@ -145,5 +145,3 @@ dependencies {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -36,8 +36,5 @@ rootProject.name = 'Joplin'
|
||||
|
||||
expoAutolinking.useExpoVersionCatalog()
|
||||
|
||||
include ':react-native-vector-icons'
|
||||
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
|
||||
|
||||
include ':app'
|
||||
includeBuild(expoAutolinking.reactNativeGradlePlugin)
|
||||
@@ -29,7 +29,7 @@ const useStyles = () => {
|
||||
|
||||
const TextInputDialog: React.FC<Props> = ({ dialog, containerStyle, themeId }) => {
|
||||
const styles = useStyles();
|
||||
const [text, setText] = useState('');
|
||||
const [text, setText] = useState(dialog.initialValue ?? '');
|
||||
const labelId = useId();
|
||||
|
||||
return (
|
||||
|
||||
@@ -91,7 +91,7 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
|
||||
});
|
||||
});
|
||||
},
|
||||
promptForText: (message: string) => {
|
||||
promptForText: (message: string, initialValue?: string) => {
|
||||
return new Promise<string|null>((resolve) => {
|
||||
const dismiss = () => {
|
||||
onDismiss(dialog);
|
||||
@@ -101,6 +101,7 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
|
||||
type: DialogType.TextInput,
|
||||
key: `prompt-dialog-${nextDialogIdRef.current++}`,
|
||||
message,
|
||||
initialValue,
|
||||
onSubmit: (text) => {
|
||||
resolve(text);
|
||||
dismiss();
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface DialogControl {
|
||||
info(message: string): Promise<void>;
|
||||
error(message: string): Promise<void>;
|
||||
prompt(title: string, message: string, buttons?: PromptButtonSpec[], options?: PromptOptions): void;
|
||||
promptForText(message: string): Promise<string>;
|
||||
promptForText(message: string, initialValue?: string): Promise<string>;
|
||||
showMenu<IdType>(title: string, choices: MenuChoice<IdType>[]): Promise<IdType>;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface TextInputDialogData {
|
||||
type: DialogType.TextInput;
|
||||
key: string;
|
||||
message: string;
|
||||
initialValue?: string;
|
||||
onSubmit: (text: string)=> void;
|
||||
onDismiss: ()=> void;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
import * as React from 'react';
|
||||
import { TextStyle, Text, StyleProp } from 'react-native';
|
||||
|
||||
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
|
||||
const AntIcon = require('react-native-vector-icons/AntDesign').default;
|
||||
const MaterialCommunityIcon = require('react-native-vector-icons/MaterialCommunityIcons').default;
|
||||
const Ionicon = require('react-native-vector-icons/Ionicons').default;
|
||||
import { FontAwesome5 } from '@react-native-vector-icons/fontawesome5';
|
||||
import { MaterialDesignIcons } from '@react-native-vector-icons/material-design-icons';
|
||||
import { Ionicons } from '@react-native-vector-icons/ionicons';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
@@ -43,20 +42,24 @@ const Icon: React.FC<Props> = props => {
|
||||
};
|
||||
|
||||
if (namePrefix.match(/^fa[bsr]?$/)) {
|
||||
let iconStyle = 'solid';
|
||||
if (namePrefix.startsWith('fab')) {
|
||||
iconStyle = 'brand';
|
||||
} else if (namePrefix.startsWith('fas')) {
|
||||
iconStyle = 'solid';
|
||||
}
|
||||
|
||||
return (
|
||||
<FontAwesomeIcon
|
||||
brand={namePrefix.startsWith('fab')}
|
||||
solid={namePrefix.startsWith('fas')}
|
||||
<FontAwesome5
|
||||
name={nameSuffix}
|
||||
iconStyle={iconStyle}
|
||||
{...sharedProps}
|
||||
/>
|
||||
);
|
||||
} else if (namePrefix === 'ant') {
|
||||
return <AntIcon name={nameSuffix} {...sharedProps}/>;
|
||||
} else if (namePrefix === 'material') {
|
||||
return <MaterialCommunityIcon name={nameSuffix} {...sharedProps}/>;
|
||||
return <MaterialDesignIcons name={nameSuffix} {...sharedProps}/>;
|
||||
} else if (namePrefix === 'ionicon') {
|
||||
return <Ionicon name={nameSuffix} {...sharedProps}/>;
|
||||
return <Ionicons name={nameSuffix} {...sharedProps}/>;
|
||||
} else if (namePrefix === 'text') {
|
||||
return (
|
||||
<Text
|
||||
@@ -69,7 +72,7 @@ const Icon: React.FC<Props> = props => {
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
return <FontAwesomeIcon name='cog' {...sharedProps}/>;
|
||||
return <FontAwesome5 name='cog' {...sharedProps}/>;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -274,6 +274,7 @@ function NoteEditor(props: Props) {
|
||||
highlightActiveLine,
|
||||
|
||||
keymap: EditorKeymap.Default,
|
||||
preferMacShortcuts: shim.mobilePlatform() === 'ios',
|
||||
|
||||
automatchBraces: false,
|
||||
ignoreModifiers: false,
|
||||
|
||||
@@ -215,8 +215,7 @@ describe('RichTextEditor', () => {
|
||||
firstCheckbox.click();
|
||||
|
||||
await waitFor(async () => {
|
||||
// At present, lists are saved as non-tight lists:
|
||||
expect(body.trim()).toBe('- [x] Test\n \n- [x] Another test');
|
||||
expect(body.trim()).toBe('- [x] Test\n- [x] Another test');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,8 +432,14 @@ describe('RichTextEditor', () => {
|
||||
expect(editor.textContent).toContain('3^2 + 4^2 = 5^2');
|
||||
});
|
||||
|
||||
it('should save lists as single-spaced', async () => {
|
||||
let body = 'Test:\n\n- this\n- is\n- a\n- test.';
|
||||
it.each(['-', '- [ ]'])('should save lists as single-spaced (list markers: %j)', async (marker) => {
|
||||
let body = [
|
||||
'Test:\n',
|
||||
'this',
|
||||
'is',
|
||||
'a',
|
||||
'test.',
|
||||
].join(`\n${marker} `);
|
||||
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
@@ -445,7 +450,13 @@ describe('RichTextEditor', () => {
|
||||
mockTyping(window, ' Testing');
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(body.trim()).toBe('Test:\n\n- this\n- is\n- a\n- test. Testing');
|
||||
expect(body.trim()).toBe([
|
||||
'Test:\n',
|
||||
'this',
|
||||
'is',
|
||||
'a',
|
||||
'test. Testing',
|
||||
].join(`\n${marker} `));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -126,12 +126,12 @@ const declarations: CommandDeclaration[] = [
|
||||
{
|
||||
name: EditorCommandType.IndentLess,
|
||||
label: () => _('Decrease indent level'),
|
||||
iconName: 'ant indent-left',
|
||||
iconName: 'material format-indent-decrease',
|
||||
},
|
||||
{
|
||||
name: EditorCommandType.IndentMore,
|
||||
label: () => _('Increase indent level'),
|
||||
iconName: 'ant indent-right',
|
||||
iconName: 'material format-indent-increase',
|
||||
},
|
||||
{
|
||||
name: `editor.${EditorCommandType.SwapLineDown}`,
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
import { PureComponent, ReactElement } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, ViewStyle, Platform } from 'react-native';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import BackButtonService from '../../services/BackButtonService';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
@@ -26,6 +25,7 @@ import WebBetaButton from './WebBetaButton';
|
||||
import Menu, { MenuOptionType } from './Menu';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Icon from '../Icon';
|
||||
export { MenuOptionType };
|
||||
|
||||
// Rather than applying a padding to the whole bar, it is applied to each
|
||||
@@ -282,7 +282,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
accessibilityHint={_('Show/hide the sidebar')}
|
||||
accessibilityRole="button">
|
||||
<View style={styles.sideMenuButton}>
|
||||
<Icon name="menu" style={styles.topIcon} />
|
||||
<Icon name="ionicon menu" style={styles.topIcon} accessibilityLabel={null} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -299,8 +299,9 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
accessibilityRole="button">
|
||||
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
|
||||
<Icon
|
||||
name="arrow-back"
|
||||
name="ionicon arrow-back"
|
||||
style={styles.topIcon}
|
||||
accessibilityLabel={null}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -688,7 +689,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
!menuOptions.length || !showContextMenuButton ? null : (
|
||||
<Menu themeId={this.props.themeId} options={menuOptions}>
|
||||
<View style={contextMenuStyle} accessibilityLabel={_('Actions')}>
|
||||
<Icon name="ellipsis-vertical" style={this.styles().contextMenuTrigger} />
|
||||
<Icon name="ionicon ellipsis-vertical" style={this.styles().contextMenuTrigger} accessibilityLabel={null}/>
|
||||
</View>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { TouchableOpacity, Text, StyleSheet, ScrollView, View, ViewStyle } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import { Ionicons as Icon } from '@react-native-vector-icons/ionicons';
|
||||
import { themeStyle } from './global-style';
|
||||
import { AppState } from '../utils/types';
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export enum InstallState {
|
||||
|
||||
interface Props {
|
||||
onPress: ()=> void;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
children: React.ReactNode;
|
||||
style?: ViewStyle;
|
||||
testID?: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Dispatch } from 'redux';
|
||||
import { AccessibilityActionEvent, AccessibilityActionInfo, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import BottomDrawer from '../BottomDrawer';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import { Ionicons as Icon } from '@react-native-vector-icons/ionicons';
|
||||
|
||||
type OnButtonPress = ()=> void;
|
||||
interface ButtonSpec {
|
||||
|
||||
@@ -632,6 +632,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
if (Platform.OS !== 'ios') addSettingLink('donate_link', _('Make a donation'), 'https://joplinapp.org/donate/');
|
||||
addSettingLink('website_link', _('Joplin website'), 'https://joplinapp.org/');
|
||||
addSettingLink('privacy_link', _('Privacy Policy'), 'https://joplinapp.org/privacy/');
|
||||
addSettingLink('license_link', _('Open-source licences'), 'https://raw.githubusercontent.com/laurent22/joplin/refs/heads/dev/readme/licenses.md');
|
||||
|
||||
const versionInfoText = getVersionInfoText(settings['plugins.states']);
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ const PluginBox: React.FC<Props> = props => {
|
||||
style={styles.cardContainer}
|
||||
onPress={props.onShowPluginInfo ? onPress : null}
|
||||
testID='plugin-card'
|
||||
disabled={!props.isCompatible}
|
||||
>
|
||||
<Card.Content style={styles.content}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
|
||||
@@ -11,9 +11,9 @@ import { Button } from 'react-native-paper';
|
||||
import createRootStyle from '../../utils/createRootStyle';
|
||||
import ScreenHeader from '../ScreenHeader';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Icon from '../Icon';
|
||||
|
||||
const logger = Logger.create('JoplinCloudLoginScreen');
|
||||
|
||||
@@ -179,7 +179,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
|
||||
</Text>
|
||||
{state.active === 'LINK_USED' ? (
|
||||
<Animated.View style={{ transform: [{ rotate: syncIconRotation }] }}>
|
||||
<Icon name='sync' style={styles.loadingIcon}/>
|
||||
<Icon name='ionicon sync' style={styles.loadingIcon} accessibilityLabel={_('Waiting for authorisation...')}/>
|
||||
</Animated.View>
|
||||
) : null }
|
||||
</View>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { View, Text, FlatList, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { View, Text, FlatList, StyleSheet, AccessibilityRole } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import { themeStyle } from '../global-style';
|
||||
@@ -8,10 +8,15 @@ import { ScreenHeader } from '../ScreenHeader';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { TagEntity } from '@joplin/lib/services/database/types';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState, useContext } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect';
|
||||
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
|
||||
import { DialogContext } from '../DialogManager';
|
||||
import useOnLongPressProps from '../../utils/hooks/useOnLongPressProps';
|
||||
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
||||
import { PromptButtonSpec } from '../DialogManager/types';
|
||||
import MultiTouchableOpacity from '../buttons/MultiTouchableOpacity';
|
||||
import SearchBar from './SearchScreen/SearchBar';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
@@ -47,12 +52,47 @@ const useStyles = (themeId: number) => {
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
interface TagItemProps {
|
||||
tag: TagEntity;
|
||||
themeId: number;
|
||||
onPress: (id: string)=> void;
|
||||
onLongPress: (tag: TagEntity)=> void;
|
||||
}
|
||||
|
||||
const TagItem: React.FC<TagItemProps> = ({ tag, themeId, onPress, onLongPress }) => {
|
||||
const styles = useStyles(themeId);
|
||||
const onLongPressProps = useOnLongPressProps({ onLongPress: () => onLongPress(tag), actionDescription: _('Edit tag') });
|
||||
const accessibilityRole: AccessibilityRole = 'button';
|
||||
const pressableProps = {
|
||||
accessibilityRole,
|
||||
accessibilityHint: _('Shows notes for tag'),
|
||||
...onLongPressProps,
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiTouchableOpacity
|
||||
{...pressableProps}
|
||||
containerProps={{
|
||||
style: {},
|
||||
}}
|
||||
onPress={() => onPress(tag.id)}
|
||||
beforePressable={null}
|
||||
>
|
||||
<View style={styles.listItem}>
|
||||
<Text style={styles.listItemText}>{tag.title}</Text>
|
||||
</View>
|
||||
</MultiTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const TagsScreenComponent: React.FC<Props> = props => {
|
||||
const [tags, setTags] = useState<TagEntity[]>([]);
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const styles = useStyles(props.themeId);
|
||||
const dialogs = useContext(DialogContext);
|
||||
const collatorLocale = getCollatorLocale();
|
||||
const collator = useMemo(() => {
|
||||
return getCollator(collatorLocale);
|
||||
@@ -86,7 +126,7 @@ const TagsScreenComponent: React.FC<Props> = props => {
|
||||
setTags([]);
|
||||
}
|
||||
}
|
||||
}, [searchQuery, collator], { interval: 200 });
|
||||
}, [searchQuery, collator, refreshTrigger], { interval: 200 });
|
||||
|
||||
const onSearchButtonPress = useCallback(() => {
|
||||
setShowSearch(!showSearch);
|
||||
@@ -111,20 +151,74 @@ const TagsScreenComponent: React.FC<Props> = props => {
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
const onTagItemLongPress = useCallback(async (tag: TagEntity) => {
|
||||
const menuItems: PromptButtonSpec[] = [];
|
||||
|
||||
const generateTagDeletion = () => {
|
||||
return () => {
|
||||
dialogs.prompt('', _('Delete tag "%s"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.', substrWithEllipsis(tag.title, 0, 32)), [
|
||||
{
|
||||
text: _('OK'),
|
||||
onPress: async () => {
|
||||
await Tag.delete(tag.id, { sourceDescription: 'tags-screen (long-press)' });
|
||||
setRefreshTrigger(prev => prev + 1);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: _('Cancel'),
|
||||
onPress: () => { },
|
||||
style: 'cancel',
|
||||
},
|
||||
]);
|
||||
};
|
||||
};
|
||||
|
||||
menuItems.push({
|
||||
text: _('Rename'),
|
||||
onPress: async () => {
|
||||
const newName = await dialogs.promptForText(_('Rename tag:'), tag.title);
|
||||
if (newName && newName.trim() && newName.trim() !== tag.title) {
|
||||
try {
|
||||
const updatedTag = { ...tag, title: newName };
|
||||
await Tag.save(updatedTag, { fields: ['title'], userSideValidation: true });
|
||||
setRefreshTrigger(prev => prev + 1);
|
||||
} catch (error) {
|
||||
await dialogs.error(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
menuItems.push({
|
||||
text: _('Delete'),
|
||||
onPress: generateTagDeletion(),
|
||||
style: 'destructive',
|
||||
});
|
||||
|
||||
menuItems.push({
|
||||
text: _('Cancel'),
|
||||
onPress: () => {},
|
||||
style: 'cancel',
|
||||
});
|
||||
|
||||
dialogs.prompt(
|
||||
'',
|
||||
_('Tag: %s', tag.title),
|
||||
menuItems,
|
||||
);
|
||||
}, [dialogs]);
|
||||
|
||||
type RenderItemEvent = { item: TagEntity };
|
||||
const onRenderItem = useCallback(({ item }: RenderItemEvent) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => onTagItemPress({ id: item.id })}
|
||||
accessibilityRole='button'
|
||||
accessibilityHint={_('Shows notes for tag')}
|
||||
>
|
||||
<View style={styles.listItem}>
|
||||
<Text style={styles.listItemText}>{item.title}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TagItem
|
||||
tag={item}
|
||||
themeId={props.themeId}
|
||||
onPress={(id) => onTagItemPress({ id })}
|
||||
onLongPress={onTagItemLongPress}
|
||||
/>
|
||||
);
|
||||
}, [onTagItemPress, styles]);
|
||||
}, [onTagItemPress, onTagItemLongPress, props.themeId]);
|
||||
|
||||
return (
|
||||
<View style={styles.rootStyle}>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useMemo, useEffect, useCallback, useContext } from 'react';
|
||||
import { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Image, ImageStyle } from 'react-native';
|
||||
import { Dispatch } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
const IonIcon = require('react-native-vector-icons/Ionicons').default;
|
||||
import Icon from './Icon';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Synchronizer from '@joplin/lib/Synchronizer';
|
||||
@@ -26,6 +25,7 @@ import { StateDecryptionWorker, StateResourceFetcher } from '@joplin/lib/reducer
|
||||
import useOnLongPressProps from '../utils/hooks/useOnLongPressProps';
|
||||
import { TouchableRipple } from 'react-native-paper';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import getConflictFolderId from '@joplin/lib/models/utils/getConflictFolderId';
|
||||
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
|
||||
|
||||
interface Props {
|
||||
@@ -202,8 +202,8 @@ const FolderItem: React.FC<FolderItemProps> = props => {
|
||||
const baseStyles = props.styles;
|
||||
|
||||
const collapsed = props.collapsed;
|
||||
const iconName = collapsed ? 'chevron-down' : 'chevron-up';
|
||||
const iconComp = <IonIcon name={iconName} style={baseStyles.folderToggleIcon} />;
|
||||
const iconName = collapsed ? 'ionicon chevron-down' : 'ionicon chevron-up';
|
||||
const iconComp = <Icon name={iconName} style={baseStyles.folderToggleIcon} accessibilityLabel={null} />;
|
||||
|
||||
const onTogglePress = useCallback(() => {
|
||||
props.onTogglePress(props.folder);
|
||||
@@ -232,7 +232,7 @@ const FolderItem: React.FC<FolderItemProps> = props => {
|
||||
if (folderId === getTrashFolderId()) {
|
||||
folderIcon = getTrashFolderIcon(FolderIconType.FontAwesome);
|
||||
} else if (props.alwaysShowFolderIcons) {
|
||||
return <IonIcon name="folder-outline" style={baseStyles.folderBaseIcon} />;
|
||||
return <Icon name="ionicon folder-outline" style={baseStyles.folderBaseIcon} accessibilityLabel={null} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -338,6 +338,8 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const menuItems: any[] = [];
|
||||
|
||||
if (folder && folder.id === getConflictFolderId()) return;
|
||||
|
||||
if (folder && folder.id === getTrashFolderId()) {
|
||||
menuItems.push({
|
||||
text: _('Empty trash'),
|
||||
|
||||
@@ -2,26 +2,6 @@
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
// Mock react-native-vector-icons -- it uses ESM imports, which, by default, are not
|
||||
// supported by jest.
|
||||
jest.doMock('react-native-vector-icons/Ionicons', () => {
|
||||
return {
|
||||
default: {
|
||||
getImageSourceSync: () => {
|
||||
// Create an empty file that can be read/used as an image resource.
|
||||
const iconPath = join(Setting.value('cacheDir'), 'test-icon.png');
|
||||
writeFileSync(iconPath, '', 'utf-8');
|
||||
|
||||
return { uri: iconPath };
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import lightTheme from '@joplin/lib/themes/light';
|
||||
import { editPopupClass, getEditPopupSource } from './useEditPopup';
|
||||
import { describe, it, expect, beforeAll, jest } from '@jest/globals';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useMemo } from 'react';
|
||||
import { extname } from 'path';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { Platform } from 'react-native';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import { Ionicons as Icon } from '@react-native-vector-icons/ionicons';
|
||||
|
||||
export const editPopupClass = 'joplin-editPopup';
|
||||
|
||||
|
||||
@@ -342,30 +342,17 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Brands.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Regular.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Solid.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Solid.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/ionicons/fonts/Ionicons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/material-design-icons/fonts/MaterialDesignIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/material-icons/fonts/MaterialIcons.ttf",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
@@ -375,30 +362,17 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Brands.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Regular.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Solid.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Fontisto.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialCommunityIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialDesignIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
|
||||
@@ -42,7 +42,6 @@ target 'Joplin' do
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
|
||||
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
|
||||
pod 'JoplinRNShareExtension', :path => 'ShareExtension'
|
||||
|
||||
post_install do |installer|
|
||||
|
||||
@@ -1516,10 +1516,38 @@ PODS:
|
||||
- React
|
||||
- react-native-saf-x (3.5.1):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (5.5.2):
|
||||
- react-native-safe-area-context (5.6.1):
|
||||
- React-Core
|
||||
- react-native-sqlite-storage (6.0.1):
|
||||
- React-Core
|
||||
- react-native-vector-icons (12.3.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-hermes
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- react-native-vector-icons-fontawesome5 (12.3.0)
|
||||
- react-native-vector-icons-ionicons (12.3.0)
|
||||
- react-native-vector-icons-material-design-icons (12.4.0)
|
||||
- react-native-vector-icons-material-icons (12.4.0)
|
||||
- react-native-version-info (1.1.1):
|
||||
- React-Core
|
||||
- react-native-webview (13.15.0):
|
||||
@@ -1890,7 +1918,7 @@ PODS:
|
||||
- React
|
||||
- RNSecureRandom (1.0.1):
|
||||
- React
|
||||
- RNShare (12.1.2):
|
||||
- RNShare (12.2.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1916,30 +1944,6 @@ PODS:
|
||||
- Yoga
|
||||
- RNSVG (15.13.0):
|
||||
- React-Core
|
||||
- RNVectorIcons (10.3.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-hermes
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- SocketRocket (0.7.1)
|
||||
- Yoga (0.0.0)
|
||||
- ZXingObjC/Core (3.6.9)
|
||||
@@ -2012,6 +2016,11 @@ DEPENDENCIES:
|
||||
- "react-native-saf-x (from `../node_modules/@joplin/react-native-saf-x`)"
|
||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||
- react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`)
|
||||
- "react-native-vector-icons (from `../node_modules/@react-native-vector-icons/get-image`)"
|
||||
- "react-native-vector-icons-fontawesome5 (from `../node_modules/@react-native-vector-icons/fontawesome5`)"
|
||||
- "react-native-vector-icons-ionicons (from `../node_modules/@react-native-vector-icons/ionicons`)"
|
||||
- "react-native-vector-icons-material-design-icons (from `../node_modules/@react-native-vector-icons/material-design-icons`)"
|
||||
- "react-native-vector-icons-material-icons (from `../node_modules/@react-native-vector-icons/material-icons`)"
|
||||
- react-native-version-info (from `../node_modules/react-native-version-info`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||
@@ -2058,7 +2067,6 @@ DEPENDENCIES:
|
||||
- RNSecureRandom (from `../node_modules/react-native-securerandom`)
|
||||
- RNShare (from `../node_modules/react-native-share`)
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -2191,6 +2199,16 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-safe-area-context"
|
||||
react-native-sqlite-storage:
|
||||
:path: "../node_modules/react-native-sqlite-storage"
|
||||
react-native-vector-icons:
|
||||
:path: "../node_modules/@react-native-vector-icons/get-image"
|
||||
react-native-vector-icons-fontawesome5:
|
||||
:path: "../node_modules/@react-native-vector-icons/fontawesome5"
|
||||
react-native-vector-icons-ionicons:
|
||||
:path: "../node_modules/@react-native-vector-icons/ionicons"
|
||||
react-native-vector-icons-material-design-icons:
|
||||
:path: "../node_modules/@react-native-vector-icons/material-design-icons"
|
||||
react-native-vector-icons-material-icons:
|
||||
:path: "../node_modules/@react-native-vector-icons/material-icons"
|
||||
react-native-version-info:
|
||||
:path: "../node_modules/react-native-version-info"
|
||||
react-native-webview:
|
||||
@@ -2283,8 +2301,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-share"
|
||||
RNSVG:
|
||||
:path: "../node_modules/react-native-svg"
|
||||
RNVectorIcons:
|
||||
:path: "../node_modules/react-native-vector-icons"
|
||||
Yoga:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
@@ -2349,8 +2365,13 @@ SPEC CHECKSUMS:
|
||||
react-native-quick-crypto: b475b71e7fa4dbf3446be55e8ad4ef2c58ac4f7f
|
||||
react-native-rsa-native: a7931cdda1f73a8576a46d7f431378c5550f0c38
|
||||
react-native-saf-x: 404f0f9a29cc6bf21d88582e054c45a11b28c22b
|
||||
react-native-safe-area-context: 0f7bf11598f9a61b7ceac8dc3f59ef98697e99e1
|
||||
react-native-safe-area-context: 2243039f43d10cb1ea30ec5ac57fc6d1448413f4
|
||||
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
|
||||
react-native-vector-icons: a45ecc326ec090450f152dfc7076ce1173331ce5
|
||||
react-native-vector-icons-fontawesome5: 271d813e27a86d30bb8cf1fc2f12dae74b74b69b
|
||||
react-native-vector-icons-ionicons: ad07e944a092a5cf71b8b569d8f5ce2bf674c415
|
||||
react-native-vector-icons-material-design-icons: 76cd460b3540b80527b4a80fb7f867f7deedb498
|
||||
react-native-vector-icons-material-icons: d67e485a05560416ff6b5977d5fa7e0eb6af6870
|
||||
react-native-version-info: f0b04e16111c4016749235ff6d9a757039189141
|
||||
react-native-webview: 0dceb35a9d050f5fa55f7fe2d8c4d1903651eb7d
|
||||
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
|
||||
@@ -2395,13 +2416,12 @@ SPEC CHECKSUMS:
|
||||
RNLocalize: 3c4d0abd777a546fa77bdb6caef85a87fb9ea349
|
||||
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
|
||||
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
|
||||
RNShare: 6496fc1ea6e8fce76b769513b6c2852f9c3ded82
|
||||
RNShare: 40ace3f87cd881869e8085aced9dc16b425c74aa
|
||||
RNSVG: 295a96bc43f2baa5958d64aeec9847a1d8ca7a3d
|
||||
RNVectorIcons: e431ef1e6bef75d6ad0e33a83d376e6207962a9d
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf
|
||||
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
||||
|
||||
PODFILE CHECKSUM: 8140bf5e9b1f33537d13122a2fecbacaefb2ee5b
|
||||
PODFILE CHECKSUM: 862189470c6e7bbee6a39c783bf65a36b631921c
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -112,14 +112,22 @@ jest.doMock('@expo/vector-icons/MaterialCommunityIcons', () => {
|
||||
throw new Error('Not supported in testing environments.');
|
||||
});
|
||||
|
||||
// Used by the renderer
|
||||
jest.doMock('react-native-vector-icons/Ionicons', () => {
|
||||
return {
|
||||
default: class extends require('react-native').View {
|
||||
const mockIconLibrary = (libraryName, exportName) => {
|
||||
jest.doMock(libraryName, () => {
|
||||
const MockIconComponent = class extends require('react-native').View {
|
||||
// Used by the renderer
|
||||
static getImageSourceSync = () => ({ uri: '' });
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
return {
|
||||
default: MockIconComponent,
|
||||
[exportName]: MockIconComponent,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
mockIconLibrary('@react-native-vector-icons/ionicons', 'Ionicons');
|
||||
mockIconLibrary('@react-native-vector-icons/material-design-icons', 'MaterialDesignIcons');
|
||||
mockIconLibrary('@react-native-vector-icons/fontawesome5', 'FontAwesome5');
|
||||
|
||||
// react-native-fs's CachesDirectoryPath export doesn't work in a testing environment.
|
||||
// Use a temporary folder instead.
|
||||
|
||||
@@ -28,12 +28,18 @@
|
||||
"@joplin/react-native-saf-x": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@js-draw/material-icons": "1.32.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-community/datetimepicker": "8.4.4",
|
||||
"@react-native-community/datetimepicker": "8.4.5",
|
||||
"@react-native-community/geolocation": "3.4.0",
|
||||
"@react-native-community/netinfo": "11.4.1",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-documents/picker": "10.1.5",
|
||||
"@react-native-documents/picker": "10.1.6",
|
||||
"@react-native-vector-icons/fontawesome5": "12.3.0",
|
||||
"@react-native-vector-icons/get-image": "12.3.0",
|
||||
"@react-native-vector-icons/ionicons": "12.3.0",
|
||||
"@react-native-vector-icons/material-design-icons": "12.4.0",
|
||||
"@react-native-vector-icons/material-icons": "12.4.0",
|
||||
"assert-browserify": "2.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"color": "3.2.1",
|
||||
@@ -45,6 +51,7 @@
|
||||
"expo-av": "15.1.7",
|
||||
"expo-camera": "16.1.11",
|
||||
"expo-local-authentication": "16.0.5",
|
||||
"js-draw": "1.32.0",
|
||||
"lodash": "4.17.21",
|
||||
"md5": "2.3.0",
|
||||
"path-browserify": "1.0.1",
|
||||
@@ -66,13 +73,12 @@
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-quick-crypto": "0.7.17",
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "5.5.2",
|
||||
"react-native-safe-area-context": "5.6.1",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "12.1.2",
|
||||
"react-native-share": "12.2.0",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-svg": "15.13.0",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-vector-icons": "10.3.0",
|
||||
"react-native-version-info": "1.1.1",
|
||||
"react-native-webview": "13.15.0",
|
||||
"react-native-zip-archive": "7.0.2",
|
||||
@@ -94,7 +100,6 @@
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/turndown": "~4.0.80",
|
||||
"@joplin/turndown-plugin-gfm": "~1.0.62",
|
||||
"@js-draw/material-icons": "1.30.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
|
||||
"@react-native-community/cli": "16.0.3",
|
||||
"@react-native-community/cli-platform-android": "16.0.3",
|
||||
@@ -109,20 +114,19 @@
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "19.0.14",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/serviceworker": "0.0.152",
|
||||
"@types/serviceworker": "0.0.153",
|
||||
"@types/tar-stream": "3.1.4",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"babel-plugin-react-native-web": "0.21.1",
|
||||
"esbuild": "0.25.9",
|
||||
"esbuild": "0.25.10",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "11.2.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jetifier": "2.0.0",
|
||||
"js-draw": "1.30.1",
|
||||
"jsdom": "26.1.0",
|
||||
"nodemon": "3.1.10",
|
||||
"punycode": "2.3.1",
|
||||
@@ -130,7 +134,7 @@
|
||||
"react-native-web": "0.21.1",
|
||||
"react-refresh": "0.17.0",
|
||||
"react-test-renderer": "19.0.0",
|
||||
"sharp": "0.34.3",
|
||||
"sharp": "0.34.4",
|
||||
"sqlite3": "5.1.6",
|
||||
"timers-browserify": "2.0.12",
|
||||
"ts-jest": "29.4.1",
|
||||
|
||||
@@ -178,6 +178,9 @@ const buildStartupTasks = (
|
||||
Setting.setConstant('pluginAssetDir', `${Setting.value('resourceDir')}/pluginAssets`);
|
||||
Setting.setConstant('pluginDir', `${getProfilesRootDir()}/plugins`);
|
||||
Setting.setConstant('pluginDataDir', getPluginDataDir(currentProfile, isSubProfile));
|
||||
Setting.setConstant('sync.9.apiKey', '');
|
||||
Setting.setConstant('sync.10.apiKey', '');
|
||||
Setting.setConstant('sync.11.apiKey', '');
|
||||
});
|
||||
addTask('buildStartupTasks/make resource directory', async () => {
|
||||
await shim.fsDriver().mkdir(Setting.value('resourceDir'));
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
import fontAwesomeFont from 'react-native-vector-icons/Fonts/FontAwesome.ttf';
|
||||
import fontAwesomeSolidFont from 'react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf';
|
||||
import fontAwesomeRegularFont from 'react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf';
|
||||
import fontAwesomeBrandsFont from 'react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf';
|
||||
import ioniconFont from 'react-native-vector-icons/Fonts/Ionicons.ttf';
|
||||
import materialCommunityIconsFont from 'react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf';
|
||||
import antDesignFont from 'react-native-vector-icons/Fonts/AntDesign.ttf';
|
||||
import fontAwesomeSolidFont from '@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Solid.ttf';
|
||||
import fontAwesomeRegularFont from '@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf';
|
||||
import fontAwesomeBrandsFont from '@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf';
|
||||
import ioniconFont from '@react-native-vector-icons/ionicons/fonts/Ionicons.ttf';
|
||||
import materialCommunityIconsFont from '@react-native-vector-icons/material-icons/fonts/MaterialIcons.ttf';
|
||||
import materialIconsFont from '@react-native-vector-icons/material-design-icons/fonts/MaterialDesignIcons.ttf';
|
||||
|
||||
// See https://www.npmjs.com/package/react-native-vector-icons
|
||||
const setUpRnVectorIcons = () => {
|
||||
const iconFontStyles = `
|
||||
@font-face {
|
||||
src: url(${fontAwesomeFont});
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
@font-face {
|
||||
src: url(${fontAwesomeSolidFont});
|
||||
font-family: FontAwesome5_Solid;
|
||||
font-family: FontAwesome5Free-Solid;
|
||||
}
|
||||
@font-face {
|
||||
src: url(${fontAwesomeRegularFont});
|
||||
font-family: FontAwesome5_Regular;
|
||||
font-family: FontAwesome5Free-Regular;
|
||||
}
|
||||
@font-face {
|
||||
src: url(${fontAwesomeBrandsFont});
|
||||
font-family: FontAwesome5_Brands;
|
||||
font-family: FontAwesome5Brands-Regular;
|
||||
}
|
||||
@font-face {
|
||||
src: url(${ioniconFont});
|
||||
@@ -34,8 +29,8 @@ const setUpRnVectorIcons = () => {
|
||||
font-family: MaterialCommunityIcons;
|
||||
}
|
||||
@font-face {
|
||||
src: url(${antDesignFont});
|
||||
font-family: AntDesign;
|
||||
src: url(${materialIconsFont});
|
||||
font-family: MaterialDesignIcons;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -79,8 +79,7 @@ const buildSharedConfig = (hotReload: boolean): webpack.Configuration => {
|
||||
'@react-native-documents/picker': emptyLibraryMock,
|
||||
'react-native-exit-app': emptyLibraryMock,
|
||||
'expo-camera': emptyLibraryMock,
|
||||
// Remove this after upgrading react-native-vector-icons.
|
||||
'@react-native-vector-icons/material-design-icons': throwOnLoadLibraryMock,
|
||||
'react-native-vector-icons/MaterialCommunityIcons': throwOnLoadLibraryMock,
|
||||
|
||||
// Workaround for applying serviceworker types to a single file.
|
||||
// See https://joshuatz.com/posts/2021/strongly-typed-service-workers/.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/utils": "~3.5",
|
||||
"fs-extra": "11.2.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"yargs": "17.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
EditorView, drawSelection, highlightSpecialChars, ViewUpdate, Command, rectangularSelection,
|
||||
dropCursor,
|
||||
} from '@codemirror/view';
|
||||
import { history, undoDepth, redoDepth, standardKeymap, insertTab } from '@codemirror/commands';
|
||||
import { history, undoDepth, redoDepth, standardKeymap, insertTab, simplifySelection } from '@codemirror/commands';
|
||||
|
||||
import { keymap, KeyBinding } from '@codemirror/view';
|
||||
import { searchKeymap } from '@codemirror/search';
|
||||
@@ -40,6 +40,7 @@ import ctrlKeyStateClassExtension from './extensions/modifierKeyCssExtension';
|
||||
import ctrlClickLinksExtension from './extensions/links/ctrlClickLinksExtension';
|
||||
import { RenderedContentContext } from './extensions/rendering/types';
|
||||
import ctrlClickCheckboxExtension from './extensions/ctrlClickCheckboxExtension';
|
||||
import editorSettingsExtension, { setEditorSettingsEffect } from './extensions/editorSettingsExtension';
|
||||
|
||||
// Newer versions of CodeMirror by default use Chrome's EditContext API.
|
||||
// While this might be stable enough for desktop use, it causes significant
|
||||
@@ -235,6 +236,11 @@ const createEditor = (
|
||||
}, true),
|
||||
|
||||
...standardKeymap, ...historyKeymap, ...searchKeymap,
|
||||
|
||||
// The escape -> simplifySelection mapping is present in "defaultKeymap",
|
||||
// which is disabled on desktop but enabled on mobile. Enable this mapping
|
||||
// globally for consistency:
|
||||
keyCommand('Escape', simplifySelection, true),
|
||||
]));
|
||||
|
||||
const editor = new EditorView({
|
||||
@@ -295,6 +301,7 @@ const createEditor = (
|
||||
biDirectionalTextExtension,
|
||||
overwriteModeExtension,
|
||||
ctrlKeyStateClassExtension,
|
||||
editorSettingsExtension(settings),
|
||||
|
||||
selectedNoteIdExtension,
|
||||
|
||||
@@ -340,9 +347,12 @@ const createEditor = (
|
||||
onSettingsChange: (newSettings: EditorSettings) => {
|
||||
settings = newSettings;
|
||||
editor.dispatch({
|
||||
effects: dynamicConfig.reconfigure(
|
||||
configFromSettings(newSettings, context),
|
||||
),
|
||||
effects: [
|
||||
dynamicConfig.reconfigure(
|
||||
configFromSettings(newSettings, context),
|
||||
),
|
||||
setEditorSettingsEffect.of(newSettings),
|
||||
],
|
||||
});
|
||||
},
|
||||
onUndoRedo: () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { Prec } from '@codemirror/state';
|
||||
import { editorSettingsFacet } from './editorSettingsExtension';
|
||||
|
||||
const hasMultipleCursors = (view: EditorView) => {
|
||||
return view.state.selection.ranges.length > 1;
|
||||
@@ -12,7 +13,9 @@ const ctrlClickActionExtension = (onCtrlClick: OnCtrlClick) => {
|
||||
Prec.high([
|
||||
EditorView.domEventHandlers({
|
||||
mousedown: (event: MouseEvent, view: EditorView) => {
|
||||
const hasModifier = event.ctrlKey || event.metaKey;
|
||||
const editorSettings = view.state.facet(editorSettingsFacet);
|
||||
const hasModifier = editorSettings.preferMacShortcuts ? event.metaKey : event.ctrlKey;
|
||||
|
||||
// The default CodeMirror action for ctrl-click is to add another cursor
|
||||
// to the document. If the user already has multiple cursors, assume that
|
||||
// the ctrl-click action is intended to add another.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Facet, StateEffect, StateField } from '@codemirror/state';
|
||||
import { EditorSettings } from '../../types';
|
||||
|
||||
export const setEditorSettingsEffect = StateEffect.define<EditorSettings>();
|
||||
|
||||
export const editorSettingsFacet = Facet.define<EditorSettings|null, EditorSettings|null>({
|
||||
combine: (possibleValues) => {
|
||||
return possibleValues.filter(value => !!value)[0] ?? null;
|
||||
},
|
||||
});
|
||||
|
||||
export default (initialSettings: EditorSettings) => [
|
||||
StateField.define<EditorSettings|null>({
|
||||
create: () => initialSettings,
|
||||
update: (oldValue, transaction) => {
|
||||
for (const e of transaction.effects) {
|
||||
if (e.is(setEditorSettingsEffect)) {
|
||||
return e.value;
|
||||
}
|
||||
}
|
||||
return oldValue;
|
||||
},
|
||||
provide: (field) => editorSettingsFacet.from(field),
|
||||
}),
|
||||
];
|
||||
@@ -1,6 +1,9 @@
|
||||
import { StateEffect, StateField, Transaction } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { editorSettingsFacet } from './editorSettingsExtension';
|
||||
|
||||
// On MacOS: Tracks the meta key
|
||||
// On other platforms: Tracks the ctrl key.
|
||||
const ctrlOrMetaChangedEffect = StateEffect.define<boolean>();
|
||||
|
||||
const ctrlOrMetaPressedField = StateField.define<boolean>({
|
||||
@@ -18,11 +21,13 @@ const ctrlOrMetaPressedField = StateField.define<boolean>({
|
||||
})),
|
||||
...(() => {
|
||||
const onEvent = (event: KeyboardEvent|MouseEvent, view: EditorView) => {
|
||||
const ctrlOrCmdPressed = event.ctrlKey || event.metaKey;
|
||||
if (ctrlOrCmdPressed !== view.state.field(ctrlOrMetaPressedField)) {
|
||||
const editorSettings = view.state.facet(editorSettingsFacet);
|
||||
const hasModifier = editorSettings.preferMacShortcuts ? event.metaKey : event.ctrlKey;
|
||||
|
||||
if (hasModifier !== view.state.field(ctrlOrMetaPressedField)) {
|
||||
view.dispatch({
|
||||
effects: [
|
||||
ctrlOrMetaChangedEffect.of(ctrlOrCmdPressed),
|
||||
ctrlOrMetaChangedEffect.of(hasModifier),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ const allowImageUrlsToBeFetched = async () => {
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
const createEditor = async (initialMarkdown: string, hasImage: boolean) => {
|
||||
const createEditor = async (initialMarkdown: string, expectedTags: string[] = ['Image']) => {
|
||||
const resolveImageSrc = jest.fn((src, counter) => Promise.resolve(`${src}?r=${counter}`));
|
||||
const editor = await createTestEditor(
|
||||
initialMarkdown,
|
||||
EditorSelection.cursor(0),
|
||||
hasImage ? ['Image'] : [],
|
||||
expectedTags,
|
||||
[renderBlockImages({ resolveImageSrc })],
|
||||
);
|
||||
await allowImageUrlsToBeFetched();
|
||||
@@ -40,7 +40,7 @@ describe('renderBlockImages', () => {
|
||||
{ spaceBefore: ' ', spaceAfter: ' ', alt: 'test' },
|
||||
{ spaceBefore: '', spaceAfter: '', alt: '!!!!' },
|
||||
])('should render images below their Markdown source (case %#)', async ({ spaceBefore, spaceAfter, alt }) => {
|
||||
const editor = await createEditor(`${spaceBefore}${spaceAfter}`, true);
|
||||
const editor = await createEditor(`${spaceBefore}${spaceAfter}`);
|
||||
|
||||
const images = findImages(editor);
|
||||
expect(images).toHaveLength(1);
|
||||
@@ -51,13 +51,13 @@ describe('renderBlockImages', () => {
|
||||
// For now, only Joplin resources are rendered. This simplifies the implementation and avoids
|
||||
// potentially-unwanted web requests when opening a note with only the editor open.
|
||||
test('should not render web images', async () => {
|
||||
const editor = await createEditor('\n\n', true);
|
||||
const editor = await createEditor('\n\n');
|
||||
const images = findImages(editor);
|
||||
expect(images).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should allow reloading specific images', async () => {
|
||||
const editor = await createEditor('\n', true);
|
||||
const editor = await createEditor('\n');
|
||||
|
||||
// Should have the expected original image URLs
|
||||
expect(getImageUrls(editor)).toMatchObject([
|
||||
@@ -98,7 +98,7 @@ describe('renderBlockImages', () => {
|
||||
const widthAttr = width ? ` width="${width}"` : '';
|
||||
const editor = await createEditor(
|
||||
`${spaceBefore}<img src=":/0123456789abcdef0123456789abcdef" alt="${alt}"${widthAttr} />${spaceAfter}`,
|
||||
false,
|
||||
['HTMLTag'],
|
||||
);
|
||||
|
||||
const images = findImages(editor);
|
||||
@@ -117,7 +117,7 @@ describe('renderBlockImages', () => {
|
||||
test('should render non-self-closing HTML img tags', async () => {
|
||||
const editor = await createEditor(
|
||||
'<img src=":/0123456789abcdef0123456789abcdef" alt="test" width="300">',
|
||||
false,
|
||||
['HTMLBlock'],
|
||||
);
|
||||
|
||||
const images = findImages(editor);
|
||||
@@ -128,7 +128,7 @@ describe('renderBlockImages', () => {
|
||||
test('should not render HTML img tags with web URLs', async () => {
|
||||
const editor = await createEditor(
|
||||
'<img src="https://example.com/test.png" alt="test" />',
|
||||
false,
|
||||
['HTMLTag'],
|
||||
);
|
||||
|
||||
const images = findImages(editor);
|
||||
@@ -138,7 +138,7 @@ describe('renderBlockImages', () => {
|
||||
test('should render both markdown and HTML images in same document', async () => {
|
||||
const editor = await createEditor(
|
||||
'\n\n<img src=":/b123456789abcdef0123456789abcde2" alt="html" width="400" />',
|
||||
true,
|
||||
['Image', 'HTMLTag'],
|
||||
);
|
||||
|
||||
const images = findImages(editor);
|
||||
@@ -151,7 +151,7 @@ describe('renderBlockImages', () => {
|
||||
const editor = await createEditor(
|
||||
// eslint-disable-next-line quotes
|
||||
"<img src=':/0123456789abcdef0123456789abcdef' alt='test' width='250' />",
|
||||
false,
|
||||
['HTMLTag'],
|
||||
);
|
||||
|
||||
const images = findImages(editor);
|
||||
|
||||
@@ -15,6 +15,7 @@ import jumpToHash from '../utils/jumpToHash';
|
||||
import focusEditor from './focusEditor';
|
||||
import canReplaceSelectionWith from '../utils/canReplaceSelectionWith';
|
||||
import showCreateEditablePrompt from '../plugins/joplinEditablePlugin/showCreateEditablePrompt';
|
||||
import getTextBetween from '../utils/getTextBetween';
|
||||
|
||||
type Dispatch = (tr: Transaction)=> void;
|
||||
type ExtendedCommand = (state: EditorState, dispatch: Dispatch, view?: EditorView, options?: string[])=> boolean;
|
||||
@@ -86,13 +87,20 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
|
||||
[EditorCommandType.ToggleItalicized]: toggleMark(schema.marks.emphasis),
|
||||
[EditorCommandType.ToggleCode]: toggleCode,
|
||||
[EditorCommandType.ToggleMath]: (state, dispatch, view) => {
|
||||
const selectedText = state.doc.textBetween(state.selection.from, state.selection.to);
|
||||
const block = selectedText.includes('\n');
|
||||
const nodeType = block ? schema.nodes.joplinEditableBlock : schema.nodes.joplinEditableInline;
|
||||
const inlineNodeType = schema.nodes.joplinEditableInline;
|
||||
const blockNodeType = schema.nodes.joplinEditableBlock;
|
||||
// If multiple paragraphs are selected, it usually isn't possible to replace them
|
||||
// to inline math. Fall back to block math:
|
||||
const block = !canReplaceSelectionWith(state.selection, inlineNodeType);
|
||||
const nodeType = block ? blockNodeType : inlineNodeType;
|
||||
|
||||
if (canReplaceSelectionWith(state.selection, nodeType)) {
|
||||
if (view) {
|
||||
return showCreateEditablePrompt(block ? '$$\n\t...\n$$' : '$...$', !block)(state, dispatch, view);
|
||||
const selectedText = getTextBetween(state.doc, state.selection.from, state.selection.to);
|
||||
const content = selectedText || '...';
|
||||
return showCreateEditablePrompt(
|
||||
block ? `$$\n\t${content}\n$$` : `$${content}$`, !block,
|
||||
)(state, dispatch, view);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -135,7 +143,10 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
|
||||
return true;
|
||||
},
|
||||
[EditorCommandType.InsertCodeBlock]: (state, dispatch, view) => {
|
||||
return showCreateEditablePrompt('```\n\n```', false)(state, dispatch, view);
|
||||
const selectedText = getTextBetween(state.doc, state.selection.from, state.selection.to);
|
||||
return showCreateEditablePrompt(
|
||||
`\`\`\`\n${selectedText}\n\`\`\``, false,
|
||||
)(state, dispatch, view);
|
||||
},
|
||||
[EditorCommandType.ToggleSearch]: (state, dispatch, view) => {
|
||||
const command = setSearchVisible(!getSearchVisible(state));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { addColumnAfter, addRowAfter, deleteColumn, deleteRow, tableEditing } from 'prosemirror-tables';
|
||||
import { addColumnAfter, addRowAfter, deleteColumn, deleteRow, deleteTable, tableEditing } from 'prosemirror-tables';
|
||||
import createFloatingButtonPlugin, { ToolbarType } from './utils/createFloatingButtonPlugin';
|
||||
import addColumnRightIcon from '../vendor/icons/addColumnRight';
|
||||
import addRowBelowIcon from '../vendor/icons/addRowBelow';
|
||||
@@ -11,6 +11,13 @@ const tableCommand = (command: Command): Command => (state, dispatch, view) => {
|
||||
return command(state, dispatch, view) && focusEditor(state, dispatch, view);
|
||||
};
|
||||
|
||||
// By default, commands like deleteRow or deleteColumn don't delete the last
|
||||
// row/column in the table. This command removes the table when there are no more
|
||||
// rows/columns to delete:
|
||||
const runCommandOrDeleteTable = (command: Command): Command => (state, dispatch, view) => {
|
||||
return command(state, dispatch, view) || deleteTable(state, dispatch);
|
||||
};
|
||||
|
||||
const tablePlugin = [
|
||||
tableEditing({ allowTableNodeSelection: true }),
|
||||
createFloatingButtonPlugin('table', [
|
||||
@@ -27,12 +34,12 @@ const tablePlugin = [
|
||||
{
|
||||
icon: removeRowIcon,
|
||||
label: (_) => _('Delete row'),
|
||||
command: () => tableCommand(deleteRow),
|
||||
command: () => tableCommand(runCommandOrDeleteTable(deleteRow)),
|
||||
},
|
||||
{
|
||||
icon: removeColumnIcon,
|
||||
label: (_) => _('Delete column'),
|
||||
command: () => tableCommand(deleteColumn),
|
||||
command: () => tableCommand(runCommandOrDeleteTable(deleteColumn)),
|
||||
},
|
||||
], ToolbarType.FloatAboveBelow),
|
||||
];
|
||||
|
||||
@@ -161,6 +161,7 @@ const nodes = addDefaultToplevelAttributes({
|
||||
inline: true,
|
||||
group: 'inlineBreak',
|
||||
selectable: false,
|
||||
leafText: () => '\n',
|
||||
parseDOM: [{ tag: 'br' }],
|
||||
toDOM: () => domOutputSpecs.br,
|
||||
},
|
||||
|
||||
8
packages/editor/ProseMirror/utils/getTextBetween.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Node } from 'prosemirror-model';
|
||||
|
||||
const getTextBetween = (doc: Node, from: number, to: number) => {
|
||||
const blockSeparator = '\n\n';
|
||||
return doc.textBetween(from, to, blockSeparator);
|
||||
};
|
||||
|
||||
export default getTextBetween;
|
||||
@@ -31,4 +31,30 @@ describe('postprocessEditorOutput', () => {
|
||||
`),
|
||||
);
|
||||
});
|
||||
|
||||
// Removing extra space around checklist item content prevents extra space from being
|
||||
// added when converting from HTML to Markdown
|
||||
test('should remove wrapper paragraphs from around checklist items', () => {
|
||||
const doc = new DOMParser().parseFromString(`
|
||||
<body>
|
||||
<ul>
|
||||
<li><input><div><p>Should remove single wrapper paragraphs to avoid extra newlines when saving as Markdown.</p></div></li>
|
||||
<li><input><div><p>Should not remove paragraphs...</p><p>...when there are multiple.</p></div></li>
|
||||
</ul>
|
||||
</body>
|
||||
`, 'text/html');
|
||||
|
||||
const output = postprocessEditorOutput(doc.body);
|
||||
|
||||
expect(
|
||||
normalizeHtmlString(output.querySelector('ul').outerHTML),
|
||||
).toBe(
|
||||
normalizeHtmlString(`
|
||||
<ul>
|
||||
<li><input><span>Should remove single wrapper paragraphs to avoid extra newlines when saving as Markdown.</span></li>
|
||||
<li><input><div><p>Should not remove paragraphs...</p><p>...when there are multiple.</p></div></li>
|
||||
</ul>
|
||||
`),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ const removeListItemWrapperParagraphs = (container: HTMLElement) => {
|
||||
for (const item of listItems) {
|
||||
trimEmptyParagraphs(item);
|
||||
|
||||
// Replace <li><p>...text...</p></li> with <li>...text...</li>
|
||||
if (item.children.length === 1) {
|
||||
const firstChild = item.children[0];
|
||||
if (firstChild.tagName === 'P') {
|
||||
@@ -22,6 +23,30 @@ const removeListItemWrapperParagraphs = (container: HTMLElement) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Avoids extra newlines from being included in the output Markdown
|
||||
const removeChecklistItemWrapperParagraphs = (container: HTMLElement) => {
|
||||
const listItems = container.querySelectorAll<HTMLLIElement>('li');
|
||||
for (const item of listItems) {
|
||||
// Is it a checklist item?
|
||||
if (item.children.length !== 2) continue;
|
||||
const input = item.children[0];
|
||||
const content = item.children[1];
|
||||
if (input.tagName !== 'INPUT' || content.tagName !== 'DIV') continue;
|
||||
|
||||
trimEmptyParagraphs(content);
|
||||
|
||||
// Replace <li><input/><div><p>...text...</p></div></li> with <li><input/><span>...text...</span></li>
|
||||
if (content.children.length === 1) {
|
||||
const firstChild = content.children[0];
|
||||
if (firstChild.tagName === 'P') {
|
||||
const newContent = document.createElement('span');
|
||||
newContent.replaceChildren(...firstChild.childNodes);
|
||||
content.replaceWith(newContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const restoreOriginalLinks = (container: HTMLElement) => {
|
||||
// Restore HREFs
|
||||
const links = container.querySelectorAll<HTMLAnchorElement>('a[href="#"][data-original-href]');
|
||||
@@ -63,6 +88,7 @@ const postprocessEditorOutput = (node: Node|DocumentFragment) => {
|
||||
fixResourceUrls(html);
|
||||
restoreOriginalLinks(html);
|
||||
removeListItemWrapperParagraphs(html);
|
||||
removeChecklistItemWrapperParagraphs(html);
|
||||
removeTableItemExtraPadding(html);
|
||||
|
||||
return html;
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lezer/markdown": "1.3.2",
|
||||
"@replit/codemirror-vim": "6.2.1",
|
||||
"dompurify": "3.2.6",
|
||||
"dompurify": "3.2.7",
|
||||
"orderedmap": "2.1.1",
|
||||
"prosemirror-commands": "1.7.1",
|
||||
"prosemirror-dropcursor": "1.8.2",
|
||||
|
||||