You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-02 20:46:21 +02:00
Compare commits
167 Commits
ios-v12.13
...
android-v2
Author | SHA1 | Date | |
---|---|---|---|
|
39c8fc812d | ||
|
0638d711d7 | ||
|
9bad668cc5 | ||
|
c18c31ab7f | ||
|
7c24a2f4be | ||
|
56438ea644 | ||
|
7f9bc1e15c | ||
|
b1c8cb5632 | ||
|
f0a1b41794 | ||
|
02982464a6 | ||
|
62e317db05 | ||
|
e0795748a9 | ||
|
67070ed3d5 | ||
|
fec8c6131c | ||
|
24ed5bda63 | ||
|
dbb354ad10 | ||
|
92dccbe98d | ||
|
9b775d77f6 | ||
|
4fd6937d05 | ||
|
7230f0e698 | ||
|
ada82538ee | ||
|
e7dd981db6 | ||
|
767bf9f002 | ||
|
f698068587 | ||
|
d0955b4ca2 | ||
|
18e86a7ba3 | ||
|
f9a1ab4d40 | ||
|
062d0898a0 | ||
|
3b51b4fd72 | ||
|
1a78ff4398 | ||
|
544af8d118 | ||
|
2616c377a9 | ||
|
4a63331306 | ||
|
48621443ec | ||
|
79fd66b94c | ||
|
6a6c8c1d83 | ||
|
cf19dacbaf | ||
|
50925abc40 | ||
|
c80cbaa32f | ||
|
f7cb1aef4b | ||
|
96d5d1dfab | ||
|
98d608fec5 | ||
|
1af46b0246 | ||
|
1e530b74d4 | ||
|
e61c4acce5 | ||
|
184499711d | ||
|
2c0181d097 | ||
|
06ea12adb3 | ||
|
9923e5c821 | ||
|
9a06e59cfe | ||
|
80a2cd91f4 | ||
|
df9ed3e487 | ||
|
368d0130f6 | ||
|
824e1b44dd | ||
|
ccf1c8ee31 | ||
|
d5f6d83f6d | ||
|
5d422f85c8 | ||
|
78aeb46d56 | ||
|
091bf45149 | ||
|
8d9d24740b | ||
|
21e5f88cb2 | ||
|
5d4259d064 | ||
|
9ac03ec33a | ||
|
e760276341 | ||
|
206f35ffe5 | ||
|
ddf716479d | ||
|
ec7f94df25 | ||
|
bcbba0973f | ||
|
bd1ddb8522 | ||
|
fb47398554 | ||
|
10356f4009 | ||
|
ba83fca47a | ||
|
b01295f0fd | ||
|
b928e614cc | ||
|
973b9c354c | ||
|
c12444d6e8 | ||
|
1401d28f82 | ||
|
335269f92d | ||
|
6211606a22 | ||
|
5f7d438ac1 | ||
|
ee2df96cfb | ||
|
692e925997 | ||
|
39803f53a0 | ||
|
b3591808b7 | ||
|
2427677fd5 | ||
|
9a051effcd | ||
|
e6e9f92e01 | ||
|
ca6762c891 | ||
|
76d07beb27 | ||
|
f3daa7f0e4 | ||
|
041ad22443 | ||
|
6cd0938ee4 | ||
|
c3dc30ee5d | ||
|
37c925dcf2 | ||
|
05bd51f85c | ||
|
cfbc37df8d | ||
|
36635c452c | ||
|
d08a16c381 | ||
|
1cee10ce12 | ||
|
b06974e104 | ||
|
bfafe7a70c | ||
|
05a29b4509 | ||
|
54f7a83789 | ||
|
1d04ec6b64 | ||
|
99d93f0a85 | ||
|
b78101ef90 | ||
|
6261d30574 | ||
|
672d028d29 | ||
|
0340c7f65c | ||
|
632802e58b | ||
|
c3510bf26b | ||
|
e22aa4f6e9 | ||
|
76a8ae3a83 | ||
|
1c104d2e83 | ||
|
7c38c2c8f2 | ||
|
188d9ac159 | ||
|
91751d5fa3 | ||
|
5124cbfd9b | ||
|
cb21a61d7c | ||
|
f50019d098 | ||
|
ab9a1776c8 | ||
|
a357665c77 | ||
|
be097afd83 | ||
|
b417299616 | ||
|
582b963570 | ||
|
1405def25d | ||
|
e9c598cf46 | ||
|
02361e37f0 | ||
|
041414b11e | ||
|
b4ca00ebf5 | ||
|
9d96866531 | ||
|
6593025051 | ||
|
a38fe11bbe | ||
|
b030ca914d | ||
|
88b44a0f74 | ||
|
7b2cf0e483 | ||
|
5d3e920370 | ||
|
7cec62fc71 | ||
|
698d16e970 | ||
|
d88af474d2 | ||
|
7488129517 | ||
|
9772389fd9 | ||
|
c2a1ea8cba | ||
|
9afc94fb3b | ||
|
390b28c3f5 | ||
|
8be22ed910 | ||
|
b097ab29ee | ||
|
d9bf0b7d82 | ||
|
0f5533af55 | ||
|
1c47db70a0 | ||
|
9723ab0ba6 | ||
|
2b9d519f4b | ||
|
4478ce118a | ||
|
7b56311729 | ||
|
09b52237f2 | ||
|
c7c86c2b52 | ||
|
dce8bced15 | ||
|
a9719307af | ||
|
0c8b475736 | ||
|
dbd3db873f | ||
|
073781da92 | ||
|
7d87d0b394 | ||
|
0cb2a3a385 | ||
|
c1e970a703 | ||
|
17831bf87a | ||
|
a93c558479 | ||
|
13b09aa9a4 |
@@ -260,6 +260,7 @@ packages/app-desktop/gui/NoteEditor/commands/index.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js
|
||||
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.test.js
|
||||
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js
|
||||
packages/app-desktop/gui/NoteEditor/styles/index.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js
|
||||
@@ -382,6 +383,8 @@ packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
@@ -426,6 +429,7 @@ packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
|
||||
@@ -466,17 +470,29 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
@@ -494,7 +510,10 @@ packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs.js
|
||||
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
|
||||
packages/app-mobile/tools/buildInjectedJs/constants.js
|
||||
packages/app-mobile/tools/buildInjectedJs/copyJs.js
|
||||
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
@@ -599,6 +618,7 @@ packages/lib/commands/index.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/config/config-shared.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
|
||||
packages/lib/components/shared/note-screen-shared.js
|
||||
@@ -825,6 +845,7 @@ packages/lib/services/rest/routes/events.test.js
|
||||
packages/lib/services/rest/routes/events.js
|
||||
packages/lib/services/rest/routes/folders.js
|
||||
packages/lib/services/rest/routes/master_keys.js
|
||||
packages/lib/services/rest/routes/notes.test.js
|
||||
packages/lib/services/rest/routes/notes.js
|
||||
packages/lib/services/rest/routes/ping.js
|
||||
packages/lib/services/rest/routes/resources.js
|
||||
|
@@ -157,6 +157,8 @@ module.exports = {
|
||||
// In user-facing text, it should be "notebook".
|
||||
'id-denylist': ['error', 'err', 'notebook', 'notebooks'],
|
||||
'prefer-arrow-callback': ['error'],
|
||||
|
||||
'no-constant-binary-expression': ['error'],
|
||||
},
|
||||
'plugins': [
|
||||
'react',
|
||||
|
62
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal file
62
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Bug Report
|
||||
description: Report a reproducible bug or regression in Joplin.
|
||||
labels: ['bug']
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: "Operating system"
|
||||
multiple: false
|
||||
options:
|
||||
- "Windows"
|
||||
- "macOS"
|
||||
- "Linux"
|
||||
- "Android"
|
||||
- "iOS"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: "Joplin version"
|
||||
placeholder: "For example 2.3.6"
|
||||
description:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: desktop-about-content
|
||||
attributes:
|
||||
label: "Desktop version info"
|
||||
description: "If this issue is about the **desktop app**, please open the \"About\" dialog under the \"Help\" or \"Joplin\" menu and copy its content here."
|
||||
placeholder: "Joplin 2.13.5 (dev, darwin)\n\nClient ID: ..."
|
||||
|
||||
- type: textarea
|
||||
id: current
|
||||
attributes:
|
||||
label: Current behaviour
|
||||
description: What did Joplin do? Include screenshots and video recordings for UI problems if needed. If you are reporting a clipper bug, please include an example URL that shows the issue.
|
||||
placeholder: |
|
||||
1. This
|
||||
2. Then that
|
||||
3. Then this
|
||||
4. Etc.
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
description: What did you expect Joplin to do?
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: "If relevant, please provide a log file as described here: https://joplinapp.org/help/apps/debugging"
|
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,52 +0,0 @@
|
||||
---
|
||||
name: "\U0001F41B Bug Report"
|
||||
about: Report a reproducible bug or regression in Joplin.
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Please provide a clear and concise description of what the bug is. (In the section Steps To Reproduce.)
|
||||
Include screenshots for UI problems if needed.
|
||||
DO NOT create screenshots of text !!! Copy and paste the text into a code block.
|
||||
Please test using the latest Joplin release to make sure your issue has not already been fixed.
|
||||
-->
|
||||
|
||||
<!--
|
||||
IMPORTANT: If you are reporting a clipper bug, please include an example URL that shows the issue.
|
||||
Without the URL the issue is likely to be closed.
|
||||
-->
|
||||
|
||||
## Environment
|
||||
|
||||
Joplin version:
|
||||
Platform:
|
||||
OS specifics:
|
||||
|
||||
<!--
|
||||
Platform can be one of: macOS, Linux, Windows, Android, iOS, terminal (or a combination)
|
||||
OS specifics: e.g. OS version, Linux distribution, Android/iOS version...
|
||||
-->
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
<!--
|
||||
Issues without reproduction steps are likely to stall.
|
||||
-->
|
||||
|
||||
## Describe what you expected to happen
|
||||
|
||||
|
||||
|
||||
## Logfile
|
||||
|
||||
<!--
|
||||
Please attach a debug log. Issues without a debug log are likely to stall.
|
||||
For information on how to collect a log file: https://joplinapp.org/help/apps/debugging/
|
||||
-->
|
9
.github/ISSUE_TEMPLATE/config.yml
vendored
9
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "\U0001F914 Feature requests and support"
|
||||
url: https://discourse.joplinapp.org/
|
||||
about: I have a question or feature request …
|
||||
- name: Feature Requests
|
||||
url: https://discourse.joplinapp.org/c/features/
|
||||
about: Discuss ideas for new features or changes
|
||||
- name: Support
|
||||
url: https://discourse.joplinapp.org/c/support/
|
||||
about: Please ask for help here
|
4
.github/scripts/run_ci.sh
vendored
4
.github/scripts/run_ci.sh
vendored
@@ -75,6 +75,10 @@ if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
|
||||
if [ "$IS_LINUX" == "1" ]; then
|
||||
echo "Running Joplin Server tests using PostgreSQL..."
|
||||
sudo docker-compose --file docker-compose.db-dev.yml up -d
|
||||
cmdResult=$?
|
||||
if [ $cmdResult -ne 0 ]; then
|
||||
exit $cmdResult
|
||||
fi
|
||||
export JOPLIN_TESTS_SERVER_DB=pg
|
||||
else
|
||||
echo "Running Joplin Server tests using SQLite..."
|
||||
|
5
.github/workflows/build-macos-m1.yml
vendored
5
.github/workflows/build-macos-m1.yml
vendored
@@ -33,6 +33,11 @@ jobs:
|
||||
# https://yarnpkg.com/getting-started/install
|
||||
corepack enable
|
||||
|
||||
# See github-action-main.yml for explanation
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Build macOS M1 app
|
||||
env:
|
||||
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
|
||||
|
9
.github/workflows/github-actions-main.yml
vendored
9
.github/workflows/github-actions-main.yml
vendored
@@ -98,6 +98,15 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# macos-latest ships with Python 3.12 by default, but this removes a
|
||||
# utility that's used by electron-builder (distutils) so we need to pin
|
||||
# Python to an earlier version.
|
||||
# Fixes error `ModuleNotFoundError: No module named 'distutils'`
|
||||
# Ref: https://github.com/nodejs/node-gyp/issues/2869
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Run tests, build and publish Linux and macOS apps
|
||||
if: runner.os == 'Linux' || runner.os == 'macOs'
|
||||
env:
|
||||
|
27
.gitignore
vendored
27
.gitignore
vendored
@@ -242,6 +242,7 @@ packages/app-desktop/gui/NoteEditor/commands/index.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js
|
||||
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.test.js
|
||||
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js
|
||||
packages/app-desktop/gui/NoteEditor/styles/index.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js
|
||||
@@ -364,6 +365,8 @@ packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
@@ -408,6 +411,7 @@ packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
|
||||
@@ -448,17 +452,29 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/types.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
@@ -476,7 +492,10 @@ packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs.js
|
||||
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
|
||||
packages/app-mobile/tools/buildInjectedJs/constants.js
|
||||
packages/app-mobile/tools/buildInjectedJs/copyJs.js
|
||||
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
@@ -581,6 +600,7 @@ packages/lib/commands/index.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/config/config-shared.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
|
||||
packages/lib/components/shared/note-screen-shared.js
|
||||
@@ -807,6 +827,7 @@ packages/lib/services/rest/routes/events.test.js
|
||||
packages/lib/services/rest/routes/events.js
|
||||
packages/lib/services/rest/routes/folders.js
|
||||
packages/lib/services/rest/routes/master_keys.js
|
||||
packages/lib/services/rest/routes/notes.test.js
|
||||
packages/lib/services/rest/routes/notes.js
|
||||
packages/lib/services/rest/routes/ping.js
|
||||
packages/lib/services/rest/routes/resources.js
|
||||
|
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@ plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.6.3.cjs
|
||||
yarnPath: .yarn/releases/yarn-3.6.4.cjs
|
||||
|
||||
logFilters:
|
||||
|
||||
|
BIN
Assets/BadgeMacOSM1.psd
Normal file
BIN
Assets/BadgeMacOSM1.psd
Normal file
Binary file not shown.
BIN
Assets/WebsiteAssets/images/BadgeMacOSM1.png
Normal file
BIN
Assets/WebsiteAssets/images/BadgeMacOSM1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@@ -1,6 +1,31 @@
|
||||
function getOs() {
|
||||
async function getOs() {
|
||||
|
||||
// The macOS release is available for Intel and Apple silicon processors,
|
||||
// and the only way to get that info is through this new
|
||||
// `getHighEntropyValues` function (which is not available on all browsers).
|
||||
// So here we either return "macOs" for Intel or "macOsM1" for Apple
|
||||
// Silicon. If we don't know which it is, we return "macOsUndefined".
|
||||
// https://stackoverflow.com/a/75177111/561309
|
||||
|
||||
if (navigator.appVersion.indexOf("Mac")!=-1) {
|
||||
let platformInfo = null;
|
||||
try {
|
||||
platformInfo = await navigator.userAgentData.getHighEntropyValues(['architecture'])
|
||||
} catch (error) {
|
||||
console.warn('Failed getting Mac architecture:', error);
|
||||
return 'macOsUndefined';
|
||||
}
|
||||
|
||||
console.info('Got platform info:', platformInfo);
|
||||
|
||||
if (platformInfo.architecture === 'arm') {
|
||||
return "macOsM1";
|
||||
} else {
|
||||
return "macOs";
|
||||
}
|
||||
}
|
||||
|
||||
if (navigator.appVersion.indexOf("Win")!=-1) return "windows";
|
||||
if (navigator.appVersion.indexOf("Mac")!=-1) return "macOs";
|
||||
if (navigator.appVersion.indexOf("X11")!=-1) return "linux";
|
||||
if (navigator.appVersion.indexOf("Linux")!=-1) return "linux";
|
||||
return null;
|
||||
@@ -45,7 +70,7 @@ function setupMobileMenu() {
|
||||
});
|
||||
}
|
||||
|
||||
function setupDownloadPage() {
|
||||
async function setupDownloadPage() {
|
||||
if (!$('.page-download').length) return;
|
||||
|
||||
const downloadLinks = {};
|
||||
@@ -55,6 +80,7 @@ function setupDownloadPage() {
|
||||
|
||||
if (href.indexOf('-Setup') > 0) downloadLinks['windows'] = href;
|
||||
if (href.indexOf('.dmg') > 0) downloadLinks['macOs'] = href;
|
||||
if (href.endsWith('arm64.DMG')) downloadLinks['macOsM1'] = href;
|
||||
if (href.indexOf('.AppImage') > 0) downloadLinks['linux'] = href;
|
||||
});
|
||||
|
||||
@@ -70,8 +96,17 @@ function setupDownloadPage() {
|
||||
if (mobileOs) {
|
||||
$('.page-download .intro').hide();
|
||||
} else {
|
||||
const os = getOs();
|
||||
if (!os || !downloadLinks[os]) {
|
||||
const os = await getOs();
|
||||
|
||||
if (os === 'macOsUndefined') {
|
||||
// If we don't know which macOS version it is, we let the user choose.
|
||||
$('.main-content .intro').html('<p class="macos-m1-info">The macOS release is available for Intel processors or for Apple Silicon (M1) processors. Please select your version:</p>');
|
||||
const macOsLink = $('.download-link-macOs');
|
||||
const macOsM1Link = $('.download-link-macOsM1');
|
||||
$('.macos-m1-info').after('<p style="font-style: italic; font-size: .8em;">To find out what processor you have, click on the <b>Apple logo</b> in the macOS menu bar, choose <b>About This Mac</b> from the dropdown menu. If you have an Apple silicon it should say"Apple M1" under "Chip". Otherwise you have an Intel processor.</p>');
|
||||
$('.macos-m1-info').after(macOsM1Link);
|
||||
$('.macos-m1-info').after(macOsLink);
|
||||
} else if (!os || !downloadLinks[os]) {
|
||||
// If we don't know, display the section to manually download the app
|
||||
$('.page-download .get-it-desktop').show();
|
||||
} else if (os === 'linux') {
|
||||
@@ -89,5 +124,5 @@ function setupDownloadPage() {
|
||||
|
||||
$(function () {
|
||||
setupMobileMenu();
|
||||
setupDownloadPage();
|
||||
void setupDownloadPage();
|
||||
});
|
||||
|
@@ -71,7 +71,7 @@ EXPOSE ${APP_PORT}
|
||||
# https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals
|
||||
WORKDIR /home/$user/packages/server
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["node", "dist/app.js"]
|
||||
CMD ["yarn", "start-prod"]
|
||||
|
||||
# Build-time metadata
|
||||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
|
@@ -124,7 +124,7 @@ else
|
||||
fi
|
||||
if [[ $LIBFUSE == "" ]] ; then
|
||||
print "${COLOR_RED}Error: Can't get libfuse2 on system, please install libfuse2${COLOR_RESET}"
|
||||
print "See https://joplinapp.org/faq/#desktop-application-will-not-launch-on-linux and https://github.com/AppImage/AppImageKit/wiki/FUSE for more information"
|
||||
print "See https://joplinapp.org/help/faq/#desktop-application-will-not-launch-on-linux and https://github.com/AppImage/AppImageKit/wiki/FUSE for more information"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -205,9 +205,16 @@ if command -v lsb_release &> /dev/null; then
|
||||
# Check for "The SUID sandbox helper binary was found, but is not configured correctly" problem.
|
||||
# It is present in Debian 1X. A (temporary) patch will be applied at .desktop file
|
||||
# Linux Mint 4 Debbie is based on Debian 10 and requires the same param handling.
|
||||
if [[ $DISTVER =~ Debian1. ]] || [ "$DISTVER" = "Linuxmint4" ] && [ "$DISTCODENAME" = "debbie" ] || [ "$DISTVER" = "CentOS" ] && [[ "$DISTMAJOR" =~ 6|7 ]]
|
||||
#
|
||||
# This also works around Ubuntu 23.10+'s restrictions on unprivileged user namespaces. Electron
|
||||
# uses these to sandbox processes. Unfortunately, it doesn't look like we can get around this
|
||||
# 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/.
|
||||
if [[ $DISTVER = "Ubuntu23.10" || $DISTVER =~ Debian1. || ( "$DISTVER" = "Linuxmint4" && "$DISTCODENAME" = "debbie" ) || ( "$DISTVER" = "CentOS" && "$DISTMAJOR" =~ 6|7 ) ]]
|
||||
then
|
||||
SANDBOXPARAM="--no-sandbox"
|
||||
print "${COLOR_YELLOW}WARNING${COLOR_RESET} Electron sandboxing disabled."
|
||||
print " See https://discourse.joplinapp.org/t/32160/5 for details."
|
||||
fi
|
||||
fi
|
||||
|
||||
|
@@ -42,8 +42,8 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) | <img width="50" src="https://avatars2.githubusercontent.com/u/2793530?s=96&v=4"/></br>[CyberXZT](https://github.com/CyberXZT) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/14873877?s=96&v=4"/></br>[dchecks](https://github.com/dchecks) | <img width="50" src="https://avatars2.githubusercontent.com/u/56287?s=96&v=4"/></br>[fats](https://github.com/fats) | <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | | |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/4560672?s=96&v=4"/></br>[mu88](https://github.com/mu88) | <img width="50" src="https://avatars2.githubusercontent.com/u/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | |
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
|
||||
# Community
|
||||
|
@@ -6,7 +6,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
image: postgres:16
|
||||
command: postgres -c work_mem=100000
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
@@ -18,7 +18,7 @@ services:
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_HOST=localhost
|
||||
db:
|
||||
image: postgres:15
|
||||
image: postgres:16
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
|
@@ -19,7 +19,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
|
@@ -367,6 +367,12 @@
|
||||
"type": "shell",
|
||||
"command": "cd ${workspaceFolder}/packages/server && yarn tsc",
|
||||
"group": "build",
|
||||
},
|
||||
{
|
||||
"label": "transpile-lib",
|
||||
"type": "shell",
|
||||
"command": "cd ${workspaceFolder}/packages/lib && yarn tsc",
|
||||
"group": "build",
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -395,6 +401,19 @@
|
||||
"APP_BASE_URL": "http://joplincloud.local:22300",
|
||||
"API_BASE_URL": "http://api.joplincloud.local:22300",
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "lib: debug test file",
|
||||
"preLaunchTask": "transpile-lib",
|
||||
"program": "${workspaceFolder}/packages/lib/node_modules/.bin/jest",
|
||||
"args": [
|
||||
"${fileBasenameNoExtension}",
|
||||
"--config",
|
||||
"packages/lib/jest.config.js",
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
22
package.json
22
package.json
@@ -32,7 +32,7 @@
|
||||
"cspell": "cspell",
|
||||
"dependencyTree": "madge",
|
||||
"generateDatabaseTypes": "node packages/tools/generate-database-types",
|
||||
"linkChecker": "linkchecker https://joplinapp.org",
|
||||
"linkChecker": "linkchecker https://joplinapp.org/ && linkchecker --check-extern https://joplinapp.org/api/references/plugin_api/classes/joplin.html",
|
||||
"linter-ci": "eslint --resolve-plugins-relative-to . --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter-interactive": "eslint-interactive --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter-precommit": "eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
@@ -72,13 +72,13 @@
|
||||
"@crowdin/cli": "3",
|
||||
"@joplin/utils": "~2.12",
|
||||
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.0.0",
|
||||
"@typescript-eslint/parser": "6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.2",
|
||||
"@typescript-eslint/parser": "6.7.2",
|
||||
"cspell": "5.21.2",
|
||||
"eslint": "8.47.0",
|
||||
"eslint": "8.49.0",
|
||||
"eslint-interactive": "10.8.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint-plugin-jest": "27.2.3",
|
||||
"eslint-plugin-jest": "27.4.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"execa": "5.1.1",
|
||||
@@ -87,23 +87,23 @@
|
||||
"gulp": "4.0.2",
|
||||
"husky": "3.1.0",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "13.3.0",
|
||||
"lint-staged": "14.0.1",
|
||||
"madge": "6.1.0",
|
||||
"npm-package-json-lint": "7.0.0",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"eslint-plugin-github": "4.9.2",
|
||||
"@types/fs-extra": "11.0.3",
|
||||
"eslint-plugin-github": "4.10.0",
|
||||
"http-server": "14.1.1",
|
||||
"node-gyp": "9.4.0",
|
||||
"nodemon": "3.0.1"
|
||||
},
|
||||
"packageManager": "yarn@3.6.3",
|
||||
"packageManager": "yarn@3.6.4",
|
||||
"resolutions": {
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
|
||||
"eslint": "patch:eslint@8.47.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"eslint": "patch:eslint@8.49.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
|
||||
"react-native@0.71.10": "patch:react-native@npm%3A0.71.10#./.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch"
|
||||
}
|
||||
|
@@ -57,6 +57,10 @@ class Command extends BaseCommand {
|
||||
|
||||
const lines = [];
|
||||
|
||||
lines.push('---');
|
||||
lines.push('sidebar_position: 2');
|
||||
lines.push('---');
|
||||
lines.push('');
|
||||
lines.push('# Joplin Data API');
|
||||
lines.push('');
|
||||
lines.push('This API is available when the clipper server is running. It provides access to the notes, notebooks, tags and other Joplin object via a REST API. Plugins can also access this API even when the clipper server is not running.');
|
||||
|
@@ -52,7 +52,7 @@ export default class PluginRunner extends BasePluginRunner {
|
||||
this.activeSandboxCalls_[callId] = true;
|
||||
const promise = executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler);
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
promise.finally(() => {
|
||||
void promise.finally(() => {
|
||||
delete this.activeSandboxCalls_[callId];
|
||||
});
|
||||
return promise;
|
||||
|
@@ -35,7 +35,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "2.13.0",
|
||||
"version": "2.13.2",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -71,13 +71,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~2.13",
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.19",
|
||||
"@types/fs-extra": "11.0.3",
|
||||
"@types/jest": "29.5.5",
|
||||
"@types/node": "18.18.7",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.6.4",
|
||||
"jest": "29.7.0",
|
||||
"temp": "0.9.4",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
|
@@ -36,6 +36,10 @@ describe('HtmlToMd', () => {
|
||||
htmlToMdOptions.preserveImageTagsWithSize = true;
|
||||
}
|
||||
|
||||
if (htmlFilename.indexOf('preserve_nested_tables') === 0) {
|
||||
htmlToMdOptions.preserveNestedTables = true;
|
||||
}
|
||||
|
||||
const html = await readFile(htmlPath, 'utf8');
|
||||
let expectedMd = await readFile(mdPath, 'utf8');
|
||||
|
||||
@@ -83,8 +87,8 @@ describe('HtmlToMd', () => {
|
||||
|
||||
it('should allow disabling escape', async () => {
|
||||
const htmlToMd = new HtmlToMd();
|
||||
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: true })).toBe('https://test.com/1_2_3.pdf');
|
||||
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: false })).toBe('https://test.com/1\\_2\\_3.pdf');
|
||||
expect(htmlToMd.parse('> 1 _2_ 3.pdf', { disableEscapeContent: true })).toBe('> 1 _2_ 3.pdf');
|
||||
expect(htmlToMd.parse('> 1 _2_ 3.pdf', { disableEscapeContent: false })).toBe('\\> 1 \\_2_ 3.pdf');
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -15,7 +15,4 @@ however.
|
||||
Because it isn't
|
||||
|
||||
necessary.
|
||||
|
||||
|
||||
|
||||
...
|
||||
<br/><br/><br/>...
|
@@ -0,0 +1,19 @@
|
||||
<body>
|
||||
<table border="5px" bordercolor="#8707B0">
|
||||
<tr>
|
||||
<td>Left side of the main table</td>
|
||||
<td>
|
||||
<table border="5px" bordercolor="#F35557">
|
||||
<h4 align="center">Nested Table</h4>
|
||||
<tr>
|
||||
<td>nested table C1</td>
|
||||
<td>nested table C2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>nested table</td>
|
||||
<td>nested table</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
@@ -0,0 +1 @@
|
||||
<table border="5px" bordercolor="#8707B0"><tbody><tr><td>Left side of the main table</td><td><h4 align="center">Nested Table</h4><table border="5px" bordercolor="#F35557"><tbody><tr><td>nested table C1</td><td>nested table C2</td></tr><tr><td>nested table</td><td>nested table</td></tr></tbody></table></td></tr></tbody></table>
|
4
packages/app-cli/tests/html_to_md/repeated_brs.html
Normal file
4
packages/app-cli/tests/html_to_md/repeated_brs.html
Normal file
@@ -0,0 +1,4 @@
|
||||
A<br/><br/><br/>test.<br/>
|
||||
|
||||
A single <br/><br/>can use two spaces at the end of the line,
|
||||
but<br/><br/>the markdown renderer discards these if the line is otherwise empty.
|
5
packages/app-cli/tests/html_to_md/repeated_brs.md
Normal file
5
packages/app-cli/tests/html_to_md/repeated_brs.md
Normal file
@@ -0,0 +1,5 @@
|
||||
A
|
||||
<br/><br/>test.
|
||||
A single <br/>
|
||||
can use two spaces at the end of the line, but
|
||||
<br/>the markdown renderer discards these if the line is otherwise empty.
|
14
packages/app-cli/tests/html_to_md/underscores_in_words.html
Normal file
14
packages/app-cli/tests/html_to_md/underscores_in_words.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<p>
|
||||
Some URLs in the Rich_Text_Editor contain <code>_</code> characters, but haven't been converted
|
||||
to links yet. For example, https://www.example.com/a_test_of_links.
|
||||
</p>
|
||||
<p>We should preserve the underscores _without escaping them_ to prevent the links from breaking.</p>
|
||||
<p>
|
||||
This should also correctly handle unicode characters. For example, punctuation❯_requires escapes_,
|
||||
but 𝔏𝔈𝔗𝔗𝔈𝕽_𝔠𝔥𝔞𝔯𝔞𝔠𝔱𝔢𝔯𝔰_and_897_numbers_𝒟on_'t.
|
||||
</p>
|
||||
<p>
|
||||
_Note_ that what [_causes_] a `_` to create italics_ seems to depend only on the character before
|
||||
and an escape at the _beginning_ seems to be sufficient.
|
||||
</p>
|
||||
<p>_s also don't need escapes if _ followed _ by a _space.</p>
|
@@ -0,0 +1,9 @@
|
||||
Some URLs in the Rich_Text_Editor contain `_` characters, but haven't been converted to links yet. For example, https://www.example.com/a_test_of_links.
|
||||
|
||||
We should preserve the underscores \_without escaping them_ to prevent the links from breaking.
|
||||
|
||||
This should also correctly handle unicode characters. For example, punctuation❯\_requires escapes_, but 𝔏𝔈𝔗𝔗𝔈𝕽_𝔠𝔥𝔞𝔯𝔞𝔠𝔱𝔢𝔯𝔰_and_897_numbers_𝒟on_'t.
|
||||
|
||||
\_Note_ that what \[\_causes_\] a \`\_\` to create italics_ seems to depend only on the character before and an escape at the \_beginning_ seems to be sufficient.
|
||||
|
||||
\_s also don't need escapes if _ followed _ by a \_space.
|
@@ -206,7 +206,7 @@ describe('services_PluginService', () => {
|
||||
|
||||
const mdToHtml = new MdToHtml();
|
||||
const module = require(contentScript.path).default;
|
||||
mdToHtml.loadExtraRendererRule(contentScript.id, tempDir, module({}));
|
||||
mdToHtml.loadExtraRendererRule(contentScript.id, tempDir, module({}), '');
|
||||
|
||||
const result = await mdToHtml.render([
|
||||
'```justtesting',
|
||||
|
@@ -74,7 +74,7 @@ To get such an external script file to compile, you need to add it to the `extra
|
||||
## More information
|
||||
|
||||
- [Joplin Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html)
|
||||
- [Joplin Data API](https://joplinapp.org/api/references/rest_api/)
|
||||
- [Joplin Data API](https://joplinapp.org/help/api/references/rest_api)
|
||||
- [Joplin Plugin Manifest](https://joplinapp.org/api/references/plugin_manifest/)
|
||||
- Ask for help on the [forum](https://discourse.joplinapp.org/) or our [Discord channel](https://discord.gg/VSj7AFHvpq)
|
||||
|
||||
|
@@ -26,7 +26,7 @@ const { FoldersScreenUtils } = require('@joplin/lib/folders-screen-utils.js');
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
const packageInfo = require('./packageInfo.js');
|
||||
const packageInfo: PackageInfo = require('./packageInfo.js');
|
||||
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
|
||||
import ClipperServer from '@joplin/lib/ClipperServer';
|
||||
const { webFrame } = require('electron');
|
||||
@@ -68,6 +68,7 @@ import path = require('path');
|
||||
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
|
||||
import { parseNotesParent } from '@joplin/lib/reducer';
|
||||
import { PackageInfo } from '@joplin/lib/versionInfo';
|
||||
|
||||
const pluginClasses = [
|
||||
require('./plugins/GotoAnything').default,
|
||||
|
@@ -5,8 +5,9 @@ import bridge from './services/bridge';
|
||||
import KvStore from '@joplin/lib/services/KvStore';
|
||||
import * as ArrayUtils from '@joplin/lib/ArrayUtils';
|
||||
import { CheckForUpdateOptions, extractVersionInfo, GitHubRelease } from './utils/checkForUpdatesUtils';
|
||||
const packageInfo = require('./packageInfo.js');
|
||||
import { PackageInfo } from '@joplin/lib/versionInfo';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
const packageInfo: PackageInfo = require('./packageInfo.js');
|
||||
|
||||
const logger = Logger.create('checkForUpdates');
|
||||
|
||||
|
@@ -12,7 +12,7 @@ const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const pathUtils = require('@joplin/lib/path-utils');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
|
||||
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
|
||||
import ClipperConfigScreen from '../ClipperConfigScreen';
|
||||
import restart from '../../services/restart';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
@@ -35,9 +35,10 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
public constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
shared.init(this, reg);
|
||||
shared.init(reg);
|
||||
|
||||
this.state = {
|
||||
...shared.defaultScreenState,
|
||||
selectedSectionName: 'general',
|
||||
screenName: '',
|
||||
changedSettingKeys: [],
|
||||
@@ -98,7 +99,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
}
|
||||
|
||||
public sectionByName(name: string) {
|
||||
const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings });
|
||||
const sections = shared.settingsSections({ device: AppType.Desktop, settings: this.state.settings });
|
||||
for (const section of sections) {
|
||||
if (section.name === name) return section;
|
||||
}
|
||||
@@ -699,7 +700,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
const hasChanges = this.hasChanges();
|
||||
|
||||
const settingComps = shared.settingsToComponents2(this, 'desktop', settings, this.state.selectedSectionName);
|
||||
const settingComps = shared.settingsToComponents2(this, AppType.Desktop, settings, this.state.selectedSectionName);
|
||||
|
||||
// screenComp is a custom config screen, such as the encryption config screen or keymap config screen.
|
||||
// These screens handle their own loading/saving of settings and have bespoke rendering.
|
||||
@@ -708,7 +709,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
if (screenComp) containerStyle.display = 'none';
|
||||
|
||||
const sections = shared.settingsSections({ device: 'desktop', settings });
|
||||
const sections = shared.settingsSections({ device: AppType.Desktop, settings });
|
||||
|
||||
const needRestartComp: any = this.state.needRestart ? (
|
||||
<div style={{ ...theme.textStyle, padding: 10, paddingLeft: 24, backgroundColor: theme.warningBackgroundColor, color: theme.color }}>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import { AppType, SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
@@ -92,8 +92,18 @@ export default function Sidebar(props: Props) {
|
||||
function renderButton(section: any) {
|
||||
const selected = props.selection === section.name;
|
||||
return (
|
||||
<StyledListItem key={section.name} isSubSection={Setting.isSubSection(section.name)} selected={selected} onClick={() => { props.onSelectionChange({ section: section }); }}>
|
||||
<StyledListItemIcon className={Setting.sectionNameToIcon(section.name)} />
|
||||
<StyledListItem
|
||||
key={section.name}
|
||||
href='#'
|
||||
role='tab'
|
||||
aria-selected={selected}
|
||||
isSubSection={Setting.isSubSection(section.name)}
|
||||
selected={selected}
|
||||
onClick={() => { props.onSelectionChange({ section: section }); }}
|
||||
>
|
||||
<StyledListItemIcon
|
||||
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
|
||||
/>
|
||||
<StyledListItemLabel>
|
||||
{Setting.sectionNameToLabel(section.name)}
|
||||
</StyledListItemLabel>
|
||||
@@ -121,7 +131,7 @@ export default function Sidebar(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<StyledRoot role='tablist'>
|
||||
{buttons}
|
||||
</StyledRoot>
|
||||
);
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import versionInfo from '@joplin/lib/versionInfo';
|
||||
import versionInfo, { PackageInfo } from '@joplin/lib/versionInfo';
|
||||
import PluginService, { Plugins } from '@joplin/lib/services/plugins/PluginService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import restart from '../services/restart';
|
||||
const packageInfo = require('../packageInfo.js');
|
||||
const packageInfo: PackageInfo = require('../packageInfo.js');
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
interface ErrorInfo {
|
||||
|
@@ -8,7 +8,7 @@ import KeymapService from '@joplin/lib/services/KeymapService';
|
||||
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import versionInfo from '@joplin/lib/versionInfo';
|
||||
import versionInfo, { PackageInfo } from '@joplin/lib/versionInfo';
|
||||
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
|
||||
import { ImportModule } from '@joplin/lib/services/interop/Module';
|
||||
import InteropServiceHelper from '../InteropServiceHelper';
|
||||
@@ -25,7 +25,7 @@ import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
|
||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
const packageInfo = require('../packageInfo.js');
|
||||
const packageInfo: PackageInfo = require('../packageInfo.js');
|
||||
const { clipboard } = require('electron');
|
||||
const Menu = bridge().Menu;
|
||||
|
||||
|
@@ -203,7 +203,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
let commandProcessed = true;
|
||||
|
||||
if (cmd.name === 'insertText') {
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, cmd.value, { bodyOnly: true });
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, cmd.value, markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
} else if (cmd.name === 'editor.focus') {
|
||||
editor.focus();
|
||||
@@ -559,11 +559,21 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
|
||||
const toolbarPluginButtons = pluginCommandNames.length ? ` | ${pluginCommandNames.join(' ')}` : '';
|
||||
|
||||
// The toolbar is going to wrap based on groups of buttons
|
||||
// (delimited by |). It means that if we leave large groups of
|
||||
// buttons towards the end of the toolbar it's going to needlessly
|
||||
// hide many buttons even when there is space. So this is why below,
|
||||
// we create small groups of just one button towards the end.
|
||||
|
||||
const toolbar = [
|
||||
'bold', 'italic', 'joplinHighlight', 'joplinStrikethrough', 'formattingExtras', '|',
|
||||
'link', 'joplinInlineCode', 'joplinCodeBlock', 'joplinAttach', '|',
|
||||
'bullist', 'numlist', 'joplinChecklist', '|',
|
||||
'h1', 'h2', 'h3', 'hr', 'blockquote', 'table', `joplinInsertDateTime${toolbarPluginButtons}`,
|
||||
'h1', 'h2', 'h3', '|',
|
||||
'hr', '|',
|
||||
'blockquote', '|',
|
||||
'table', '|',
|
||||
`joplinInsertDateTime${toolbarPluginButtons}`,
|
||||
];
|
||||
|
||||
const editors = await (window as any).tinymce.init({
|
||||
|
@@ -48,6 +48,7 @@ import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
import PlainEditor from './NoteBody/PlainEditor/PlainEditor';
|
||||
import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror';
|
||||
import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror';
|
||||
import { namespacedKey } from '@joplin/lib/services/plugins/api/JoplinSettings';
|
||||
|
||||
const commands = [
|
||||
require('./commands/showRevisions'),
|
||||
@@ -159,10 +160,15 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
return formNote.saveActionQueue.waitForAllDone();
|
||||
}
|
||||
|
||||
const settingValue = useCallback((pluginId: string, key: string) => {
|
||||
return Setting.value(namespacedKey(pluginId, key));
|
||||
}, []);
|
||||
|
||||
const markupToHtml = useMarkupToHtml({
|
||||
themeId: props.themeId,
|
||||
customCss: props.customCss,
|
||||
plugins: props.plugins,
|
||||
settingValue,
|
||||
});
|
||||
|
||||
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => {
|
||||
|
@@ -0,0 +1,64 @@
|
||||
import WhenClause from '@joplin/lib/services/WhenClause';
|
||||
import { enabledCondition } from './editorCommandDeclarations';
|
||||
|
||||
const baseContext: Record<string, any> = {
|
||||
modalDialogVisible: false,
|
||||
gotoAnythingVisible: false,
|
||||
markdownEditorPaneVisible: true,
|
||||
oneNoteSelected: true,
|
||||
noteIsMarkdown: true,
|
||||
noteIsReadOnly: false,
|
||||
richTextEditorVisible: false,
|
||||
};
|
||||
|
||||
describe('editorCommandDeclarations', () => {
|
||||
|
||||
test.each([
|
||||
[
|
||||
{},
|
||||
true,
|
||||
],
|
||||
[
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
},
|
||||
false,
|
||||
],
|
||||
[
|
||||
{
|
||||
noteIsReadOnly: true,
|
||||
},
|
||||
false,
|
||||
],
|
||||
[
|
||||
// In the Markdown editor, but only the viewer is visible
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: false,
|
||||
},
|
||||
false,
|
||||
],
|
||||
[
|
||||
// In the Markdown editor, and the viewer is visible
|
||||
{
|
||||
markdownEditorPaneVisible: true,
|
||||
richTextEditorVisible: false,
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
// In the RT editor
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: true,
|
||||
},
|
||||
true,
|
||||
],
|
||||
])('should create the enabledCondition', (context: Record<string, any>, expected: boolean) => {
|
||||
const condition = enabledCondition('textBold');
|
||||
const wc = new WhenClause(condition);
|
||||
const actual = wc.evaluate({ ...baseContext, ...context });
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
});
|
@@ -9,7 +9,17 @@ const workWithHtmlNotes = [
|
||||
export const enabledCondition = (commandName: string) => {
|
||||
const markdownEditorOnly = !Object.keys(joplinCommandToTinyMceCommands).includes(commandName);
|
||||
const noteMustBeMarkdown = !workWithHtmlNotes.includes(commandName);
|
||||
return `(!modalDialogVisible || gotoAnythingVisible) ${markdownEditorOnly ? '&& markdownEditorPaneVisible' : ''} && oneNoteSelected ${noteMustBeMarkdown ? '&& noteIsMarkdown' : ''} && !noteIsReadOnly`;
|
||||
|
||||
const output = [
|
||||
'!modalDialogVisible',
|
||||
'!gotoAnythingVisible',
|
||||
markdownEditorOnly ? 'markdownEditorPaneVisible' : '(markdownEditorPaneVisible || richTextEditorVisible)',
|
||||
'oneNoteSelected',
|
||||
noteMustBeMarkdown ? 'noteIsMarkdown' : '',
|
||||
'!noteIsReadOnly',
|
||||
];
|
||||
|
||||
return output.filter(c => !!c).join(' && ');
|
||||
};
|
||||
|
||||
const declarations: CommandDeclaration[] = [
|
||||
|
@@ -9,7 +9,10 @@ export async function htmlToMarkdown(markupLanguage: number, html: string, origi
|
||||
|
||||
if (markupLanguage === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN) {
|
||||
const htmlToMd = new HtmlToMd();
|
||||
newBody = htmlToMd.parse(html, { preserveImageTagsWithSize: true });
|
||||
newBody = htmlToMd.parse(html, {
|
||||
preserveImageTagsWithSize: true,
|
||||
preserveNestedTables: true,
|
||||
});
|
||||
newBody = await Note.replaceResourceExternalToInternalLinks(newBody, { useAbsolutePaths: true });
|
||||
} else {
|
||||
newBody = await Note.replaceResourceExternalToInternalLinks(html, { useAbsolutePaths: true });
|
||||
|
@@ -12,6 +12,7 @@ interface HookDependencies {
|
||||
themeId: number;
|
||||
customCss: string;
|
||||
plugins: PluginStates;
|
||||
settingValue: (pluginId: string, key: string)=> any;
|
||||
}
|
||||
|
||||
export interface MarkupToHtmlOptions {
|
||||
@@ -59,12 +60,16 @@ export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
|
||||
delete options.replaceResourceInternalToExternalLinks;
|
||||
|
||||
const result = await markupToHtml.render(markupLanguage, md, theme, { codeTheme: theme.codeThemeCss,
|
||||
const result = await markupToHtml.render(markupLanguage, md, theme, {
|
||||
codeTheme: theme.codeThemeCss,
|
||||
resources: resources,
|
||||
postMessageSyntax: 'ipcProxySendToHost',
|
||||
splitted: true,
|
||||
externalAssetsOnly: true,
|
||||
codeHighlightCacheKey: 'useMarkupToHtml', ...options });
|
||||
codeHighlightCacheKey: 'useMarkupToHtml',
|
||||
settingValue: deps.settingValue,
|
||||
...options,
|
||||
});
|
||||
|
||||
return result;
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
|
@@ -145,6 +145,7 @@ const NoteListItem = (props: NoteItemProps, ref: LegacyRef<HTMLDivElement>) => {
|
||||
tabIndex={0}
|
||||
className={className}
|
||||
data-id={noteId}
|
||||
style={{ height: props.itemSize.height }}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDragStart={props.onDragStart}
|
||||
onDragOver={props.onDragOver}
|
||||
|
@@ -2,18 +2,18 @@ import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import ButtonBar from '../ConfigScreen/ButtonBar';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
import ReportService from '@joplin/lib/services/ReportService';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import ReportService, { ReportItem, ReportSection, RetryAllHandler } from '@joplin/lib/services/ReportService';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import bridge from '../../services/bridge';
|
||||
const fs = require('fs-extra');
|
||||
import styled from 'styled-components';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { writeFileSync } from 'fs';
|
||||
|
||||
interface Props {
|
||||
themeId: string;
|
||||
themeId: number;
|
||||
style: any;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
@@ -34,12 +34,12 @@ async function exportDebugReportClick() {
|
||||
if (!filePath) return;
|
||||
|
||||
const service = new ReportService();
|
||||
const csv = await service.basicItemList({ format: 'csv' });
|
||||
await fs.writeFileSync(filePath, csv);
|
||||
const csv = (await service.basicItemList({ format: 'csv' })) as string;
|
||||
await writeFileSync(filePath, csv);
|
||||
}
|
||||
|
||||
function StatusScreen(props: Props) {
|
||||
const [report, setReport] = useState<any[]>([]);
|
||||
const [report, setReport] = useState<ReportSection[]>([]);
|
||||
|
||||
async function resfreshScreen() {
|
||||
const service = new ReportService();
|
||||
@@ -65,7 +65,7 @@ function StatusScreen(props: Props) {
|
||||
const containerStyle = { ...theme.containerStyle, padding: containerPadding,
|
||||
flex: 1 };
|
||||
|
||||
function renderSectionTitleHtml(key: string, title: string) {
|
||||
function renderSectionTitle(key: string, title: string) {
|
||||
return (
|
||||
<h2 key={`section_${key}`} style={theme.h2Style}>
|
||||
{title}
|
||||
@@ -73,7 +73,7 @@ function StatusScreen(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderSectionRetryAllHtml(key: string, retryAllHandler: any) {
|
||||
function renderSectionRetryAll(key: string, retryAllHandler: RetryAllHandler) {
|
||||
return (
|
||||
<a key={`retry_all_${key}`} href="#" onClick={retryAllHandler} style={retryAllStyle}>
|
||||
{_('Retry All')}
|
||||
@@ -81,13 +81,26 @@ function StatusScreen(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const renderSectionHtml = (key: string, section: any) => {
|
||||
const itemsHtml = [];
|
||||
const renderRetryAll = (section: ReportSection) => {
|
||||
const items: React.JSX.Element[] = [];
|
||||
if (section.canRetryAll) {
|
||||
items.push(renderSectionRetryAll(section.title, async () => {
|
||||
await section.retryAllHandler();
|
||||
void resfreshScreen();
|
||||
}));
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
itemsHtml.push(renderSectionTitleHtml(section.title, section.title));
|
||||
const renderSection = (key: string, section: ReportSection) => {
|
||||
let items = [];
|
||||
|
||||
items.push(renderSectionTitle(section.title, section.title));
|
||||
|
||||
items = items.concat(renderRetryAll(section));
|
||||
|
||||
let currentListKey = '';
|
||||
let listItems: any[] = [];
|
||||
let listItems: React.JSX.Element[] = [];
|
||||
for (const n in section.body) {
|
||||
if (!section.body.hasOwnProperty(n)) continue;
|
||||
const item = section.body[n];
|
||||
@@ -115,12 +128,12 @@ function StatusScreen(props: Props) {
|
||||
}
|
||||
|
||||
if (itemType === 'openList') {
|
||||
currentListKey = item.key;
|
||||
currentListKey = (item as ReportItem).key;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemType === 'closeList') {
|
||||
itemsHtml.push(<ul key={currentListKey}>{listItems}</ul>);
|
||||
items.push(<ul key={currentListKey}>{listItems}</ul>);
|
||||
currentListKey = '';
|
||||
listItems = [];
|
||||
continue;
|
||||
@@ -136,7 +149,7 @@ function StatusScreen(props: Props) {
|
||||
</li>,
|
||||
);
|
||||
} else {
|
||||
itemsHtml.push(
|
||||
items.push(
|
||||
<div style={theme.textStyle} key={`item_${n}`}>
|
||||
<span>{text}</span>
|
||||
{retryLink}
|
||||
@@ -145,26 +158,21 @@ function StatusScreen(props: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
if (section.canRetryAll) {
|
||||
itemsHtml.push(renderSectionRetryAllHtml(section.title, async () => {
|
||||
await section.retryAllHandler();
|
||||
void resfreshScreen();
|
||||
}));
|
||||
}
|
||||
items = items.concat(renderRetryAll(section));
|
||||
|
||||
return <div key={key}>{itemsHtml}</div>;
|
||||
return <div key={key}>{items}</div>;
|
||||
};
|
||||
|
||||
function renderBodyHtml(report: any) {
|
||||
const sectionsHtml = [];
|
||||
function renderBody(report: ReportSection[]) {
|
||||
const sections = [];
|
||||
|
||||
for (let i = 0; i < report.length; i++) {
|
||||
const section = report[i];
|
||||
if (!section.body.length) continue;
|
||||
sectionsHtml.push(renderSectionHtml(`${i}`, section));
|
||||
sections.push(renderSection(`${i}`, section));
|
||||
}
|
||||
|
||||
return <div>{sectionsHtml}</div>;
|
||||
return <div>{sections}</div>;
|
||||
}
|
||||
|
||||
function renderTools() {
|
||||
@@ -180,7 +188,7 @@ function StatusScreen(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const body = renderBodyHtml(report);
|
||||
const body = renderBody(report);
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
@@ -195,7 +203,7 @@ function StatusScreen(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any) => {
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
settings: state.settings,
|
||||
|
@@ -5,6 +5,8 @@ import SettingsScreen from './models/SettingsScreen';
|
||||
import { _electron as electron } from '@playwright/test';
|
||||
import { writeFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import createStartupArgs from './util/createStartupArgs';
|
||||
import firstNonDevToolsWindow from './util/firstNonDevToolsWindow';
|
||||
|
||||
|
||||
test.describe('main', () => {
|
||||
@@ -130,11 +132,9 @@ test.describe('main', () => {
|
||||
|
||||
// We need to write to the force-safe-mode file before opening the Electron app.
|
||||
// Open the app ourselves:
|
||||
const startupArgs = [
|
||||
'main.js', '--env', 'dev', '--profile', profileDirectory,
|
||||
];
|
||||
const startupArgs = createStartupArgs(profileDirectory);
|
||||
const electronApp = await electron.launch({ args: startupArgs });
|
||||
const mainWindow = await electronApp.firstWindow();
|
||||
const mainWindow = await firstNonDevToolsWindow(electronApp);
|
||||
|
||||
const safeModeDisableLink = mainWindow.getByText('Disable safe mode and restart');
|
||||
await safeModeDisableLink.waitFor();
|
||||
|
@@ -0,0 +1,9 @@
|
||||
|
||||
const createStartupArgs = (profileDirectory: string) => {
|
||||
// We need to run with --env dev to disable the single instance check.
|
||||
return [
|
||||
'main.js', '--env', 'dev', '--profile', profileDirectory,
|
||||
];
|
||||
};
|
||||
|
||||
export default createStartupArgs;
|
@@ -0,0 +1,43 @@
|
||||
import { ElectronApplication, Page } from '@playwright/test';
|
||||
|
||||
const isDevTools = async (page: Page) => {
|
||||
// It seems that the developer tools window can have titles in different
|
||||
// formats (e.g. DevTools, Developer Tools).
|
||||
return (await page.title()).match(/Dev(eloper)?\s*Tools/i);
|
||||
};
|
||||
|
||||
const firstNonDevToolsWindow = async (electronApp: ElectronApplication) => {
|
||||
// Wait for the window event as soon as possible -- it's possible that
|
||||
// the window we want will be shown while doing other async checks.
|
||||
const nextNonDevToolsPage = electronApp.waitForEvent('window', {
|
||||
predicate: async page => {
|
||||
return !(await isDevTools(page));
|
||||
},
|
||||
});
|
||||
|
||||
// First use firstWindow -- it's possible that the first window
|
||||
// has already been shown.
|
||||
let mainWindow = await electronApp.firstWindow();
|
||||
|
||||
if (await isDevTools(mainWindow)) {
|
||||
for (const window of electronApp.windows()) {
|
||||
if (!(await isDevTools(window))) {
|
||||
mainWindow = window;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (await isDevTools(mainWindow)) {
|
||||
mainWindow = await nextNonDevToolsPage;
|
||||
}
|
||||
}
|
||||
|
||||
// waitForEvent will throw if no additional windows are created.
|
||||
// Ignore.
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
nextNonDevToolsPage.catch(_error => {});
|
||||
|
||||
return mainWindow;
|
||||
};
|
||||
|
||||
export default firstNonDevToolsWindow;
|
@@ -2,6 +2,8 @@ import { resolve, join, dirname } from 'path';
|
||||
import { remove, mkdirp } from 'fs-extra';
|
||||
import { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import createStartupArgs from './createStartupArgs';
|
||||
import firstNonDevToolsWindow from './firstNonDevToolsWindow';
|
||||
|
||||
|
||||
|
||||
@@ -32,9 +34,7 @@ export const test = base.extend<JoplinFixtures>({
|
||||
},
|
||||
|
||||
electronApp: async ({ profileDirectory }, use) => {
|
||||
const startupArgs = [
|
||||
'main.js', '--env', 'dev', '--profile', profileDirectory,
|
||||
];
|
||||
const startupArgs = createStartupArgs(profileDirectory);
|
||||
const electronApp = await electron.launch({ args: startupArgs });
|
||||
|
||||
await use(electronApp);
|
||||
@@ -44,8 +44,8 @@ export const test = base.extend<JoplinFixtures>({
|
||||
},
|
||||
|
||||
mainWindow: async ({ electronApp }, use) => {
|
||||
const window = await electronApp.firstWindow();
|
||||
await use(window);
|
||||
const mainWindow = await firstNonDevToolsWindow(electronApp);
|
||||
await use(mainWindow);
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.13.4",
|
||||
"version": "2.13.7",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -119,21 +119,21 @@
|
||||
"@joplin/tools": "~2.13",
|
||||
"@playwright/test": "1.38.1",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.19",
|
||||
"@types/react": "18.2.31",
|
||||
"@types/react-redux": "7.1.27",
|
||||
"@types/styled-components": "5.1.28",
|
||||
"electron": "25.9.0",
|
||||
"electron-builder": "24.4.0",
|
||||
"@types/jest": "29.5.5",
|
||||
"@types/node": "18.18.7",
|
||||
"@types/react": "18.2.33",
|
||||
"@types/react-redux": "7.1.28",
|
||||
"@types/styled-components": "5.1.29",
|
||||
"electron": "26.5.0",
|
||||
"electron-builder": "24.6.4",
|
||||
"glob": "10.3.10",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.6.4",
|
||||
"jest-environment-jsdom": "29.6.4",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"js-sha512": "0.8.0",
|
||||
"nan": "2.18.0",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"7zip-bin-linux": "^1.0.1",
|
||||
@@ -142,14 +142,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/notarize": "2.1.0",
|
||||
"@electron/remote": "2.0.11",
|
||||
"@electron/remote": "2.0.12",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@joeattardi/emoji-button": "4.6.4",
|
||||
"@joplin/editor": "~2.13",
|
||||
"@joplin/lib": "~2.13",
|
||||
"@joplin/renderer": "~2.13",
|
||||
"@joplin/utils": "~2.13",
|
||||
"@types/mustache": "4.2.3",
|
||||
"@types/mustache": "4.2.4",
|
||||
"async-mutex": "0.4.0",
|
||||
"codemirror": "5.65.9",
|
||||
"color": "3.2.1",
|
||||
|
@@ -32,7 +32,7 @@ export default function(frameWindow: any, isReady: boolean, postMessage: Functio
|
||||
frameWindow.addEventListener('message', onMessage);
|
||||
|
||||
return () => {
|
||||
frameWindow.removeEventListener('message', onMessage);
|
||||
if (frameWindow.removeEventListener) frameWindow.removeEventListener('message', onMessage);
|
||||
};
|
||||
}, [frameWindow, htmlHash]);
|
||||
|
||||
|
@@ -5,8 +5,8 @@
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
TEMP_PATH=~/src/plugin-tests
|
||||
NEED_COMPILING=0
|
||||
PLUGIN_PATH=~/src/joplin/packages/app-cli/tests/support/plugins/simple
|
||||
NEED_COMPILING=1
|
||||
PLUGIN_PATH=~/src/plugin-abc
|
||||
|
||||
if [[ $NEED_COMPILING == 1 ]]; then
|
||||
mkdir -p "$TEMP_PATH"
|
||||
|
@@ -110,8 +110,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097725
|
||||
versionName "2.13.5"
|
||||
versionCode 2097730
|
||||
versionName "2.13.10"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
const React = require('react');
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import { FAB, Portal } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { Dispatch } from 'redux';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
|
||||
// eslint-disable-next-line no-undef -- Don't know why it says React is undefined when it's defined above
|
||||
type FABGroupProps = React.ComponentProps<typeof FAB.Group>;
|
||||
|
||||
type OnButtonPress = ()=> void;
|
||||
interface ButtonSpec {
|
||||
@@ -19,6 +21,7 @@ interface ActionButtonProps {
|
||||
|
||||
// If not given, an "add" button will be used.
|
||||
mainButton?: ButtonSpec;
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
const defaultOnPress = () => {};
|
||||
@@ -36,10 +39,12 @@ const useIcon = (iconName: string) => {
|
||||
|
||||
const ActionButton = (props: ActionButtonProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const onMenuToggled = useCallback(
|
||||
(state: { open: boolean }) => setOpen(state.open)
|
||||
, [setOpen]);
|
||||
|
||||
const onMenuToggled: FABGroupProps['onStateChange'] = useCallback(state => {
|
||||
props.dispatch({
|
||||
type: 'SIDE_MENU_CLOSE',
|
||||
});
|
||||
setOpen(state.open);
|
||||
}, [setOpen, props.dispatch]);
|
||||
|
||||
const actions = useMemo(() => (props.buttons ?? []).map(button => {
|
||||
return {
|
||||
|
@@ -178,6 +178,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
onRequestClose={() => {
|
||||
closeList();
|
||||
}}
|
||||
supportedOrientations={['landscape', 'portrait']}
|
||||
>
|
||||
<TouchableWithoutFeedback
|
||||
accessibilityElementsHidden={true}
|
||||
|
@@ -48,6 +48,8 @@ interface Props {
|
||||
// See react-native-webview's prop with the same name.
|
||||
mixedContentMode?: 'never' | 'always';
|
||||
|
||||
allowFileAccessFromJs?: boolean;
|
||||
|
||||
// Initial javascript. Must evaluate to true.
|
||||
injectedJavaScript: string;
|
||||
|
||||
@@ -143,6 +145,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
|
||||
mixedContentMode={props.mixedContentMode}
|
||||
allowFileAccess={true}
|
||||
allowFileAccessFromFileURLs={props.allowFileAccessFromJs}
|
||||
injectedJavaScript={props.injectedJavaScript}
|
||||
onMessage={props.onMessage}
|
||||
onError={props.onError}
|
||||
|
44
packages/app-mobile/components/Icon.tsx
Normal file
44
packages/app-mobile/components/Icon.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { TextStyle } from 'react-native';
|
||||
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
style: TextStyle;
|
||||
|
||||
// If `null` is given, the content must be labeled elsewhere.
|
||||
accessibilityLabel: string|null;
|
||||
}
|
||||
|
||||
const Icon: React.FC<Props> = props => {
|
||||
// Matches:
|
||||
// 1. A prefix of word characters (\w+)
|
||||
// 2. A suffix of non-spaces (\S+)
|
||||
// An "fa-" at the beginning of the suffix is ignored.
|
||||
const nameMatch = props.name.match(/^(\w+)\s+(?:fa-)?(\S+)$/);
|
||||
|
||||
const namePrefix = nameMatch ? nameMatch[1] : '';
|
||||
const nameSuffix = nameMatch ? nameMatch[2] : props.name;
|
||||
|
||||
// If there's no label, make sure that the screen reader doesn't try
|
||||
// to read the characters from the icon font (they don't make sense
|
||||
// without the icon font applied).
|
||||
const accessibilityHidden = props.accessibilityLabel === null;
|
||||
|
||||
return (
|
||||
<FontAwesomeIcon
|
||||
brand={namePrefix.startsWith('fab')}
|
||||
solid={namePrefix.startsWith('fas')}
|
||||
accessibilityLabel={props.accessibilityLabel}
|
||||
aria-hidden={accessibilityHidden}
|
||||
importantForAccessibility={
|
||||
accessibilityHidden ? 'no-hide-descendants' : 'yes'
|
||||
}
|
||||
name={nameSuffix}
|
||||
style={props.style}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
@@ -1,14 +1,14 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
|
||||
import useSource from './hooks/useSource';
|
||||
import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
|
||||
import useOnMessage, { HandleMessageCallback, HandleScrollCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
|
||||
import useOnResourceLongPress from './hooks/useOnResourceLongPress';
|
||||
|
||||
const React = require('react');
|
||||
import { View } from 'react-native';
|
||||
import BackButtonDialogBox from '../BackButtonDialogBox';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ExtendedWebView from '../ExtendedWebView';
|
||||
import ExtendedWebView, { WebViewControl } from '../ExtendedWebView';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -18,11 +18,13 @@ interface Props {
|
||||
highlightedKeywords: string[];
|
||||
noteResources: any;
|
||||
paddingBottom: number;
|
||||
initialScroll: number|null;
|
||||
noteHash: string;
|
||||
onJoplinLinkClick: HandleMessageCallback;
|
||||
onCheckboxChange?: HandleMessageCallback;
|
||||
onRequestEditResource?: HandleMessageCallback;
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
onScroll: HandleScrollCallback;
|
||||
onLoadEnd?: ()=> void;
|
||||
}
|
||||
|
||||
@@ -32,6 +34,7 @@ const webViewStyle = {
|
||||
|
||||
export default function NoteBodyViewer(props: Props) {
|
||||
const dialogBoxRef = useRef(null);
|
||||
const webviewRef = useRef<WebViewControl>(null);
|
||||
|
||||
const { html, injectedJs } = useSource(
|
||||
props.noteBody,
|
||||
@@ -41,6 +44,7 @@ export default function NoteBodyViewer(props: Props) {
|
||||
props.noteResources,
|
||||
props.paddingBottom,
|
||||
props.noteHash,
|
||||
props.initialScroll,
|
||||
);
|
||||
|
||||
const onResourceLongPress = useOnResourceLongPress(
|
||||
@@ -59,6 +63,7 @@ export default function NoteBodyViewer(props: Props) {
|
||||
onJoplinLinkClick: props.onJoplinLinkClick,
|
||||
onRequestEditResource: props.onRequestEditResource,
|
||||
onResourceLongPress,
|
||||
onMainContainerScroll: props.onScroll,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -96,6 +101,7 @@ export default function NoteBodyViewer(props: Props) {
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<ExtendedWebView
|
||||
ref={webviewRef}
|
||||
webviewInstanceId='NoteBodyViewer'
|
||||
themeId={props.themeId}
|
||||
style={webViewStyle}
|
||||
|
@@ -3,6 +3,7 @@ import shared from '@joplin/lib/components/shared/note-screen-shared';
|
||||
|
||||
export type HandleMessageCallback = (message: string)=> void;
|
||||
export type OnMarkForDownloadCallback = (resource: { resourceId: string })=> void;
|
||||
export type HandleScrollCallback = (scrollTop: number)=> void;
|
||||
|
||||
interface MessageCallbacks {
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
@@ -10,6 +11,7 @@ interface MessageCallbacks {
|
||||
onResourceLongPress: HandleMessageCallback;
|
||||
onRequestEditResource?: HandleMessageCallback;
|
||||
onCheckboxChange: HandleMessageCallback;
|
||||
onMainContainerScroll: HandleScrollCallback;
|
||||
}
|
||||
|
||||
export default function useOnMessage(
|
||||
@@ -24,6 +26,7 @@ export default function useOnMessage(
|
||||
// Thus, useCallback should depend on each callback individually.
|
||||
const {
|
||||
onMarkForDownload, onResourceLongPress, onCheckboxChange, onRequestEditResource, onJoplinLinkClick,
|
||||
onMainContainerScroll,
|
||||
} = callbacks;
|
||||
|
||||
return useCallback((event: any) => {
|
||||
@@ -35,10 +38,23 @@ export default function useOnMessage(
|
||||
// https://github.com/laurent22/joplin/issues/4494
|
||||
const msg = event.nativeEvent.data;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Got IPC message: ', msg);
|
||||
const isScrollMessage = msg.startsWith('onscroll:');
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
// Scroll messages are very frequent so we avoid logging them.
|
||||
if (!isScrollMessage) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Got IPC message: ', msg);
|
||||
}
|
||||
|
||||
if (isScrollMessage) {
|
||||
const eventData = JSON.parse(msg.substring(msg.indexOf(':') + 1));
|
||||
|
||||
if (typeof eventData.scrollTop !== 'number') {
|
||||
throw new Error(`Invalid scroll message, ${msg}`);
|
||||
}
|
||||
|
||||
onMainContainerScroll?.(eventData.scrollTop);
|
||||
} else if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const newBody = shared.toggleCheckbox(msg, noteBody);
|
||||
onCheckboxChange?.(newBody);
|
||||
} else if (msg.indexOf('markForDownload:') === 0) {
|
||||
@@ -63,5 +79,6 @@ export default function useOnMessage(
|
||||
onJoplinLinkClick,
|
||||
onResourceLongPress,
|
||||
onRequestEditResource,
|
||||
onMainContainerScroll,
|
||||
]);
|
||||
}
|
||||
|
@@ -40,7 +40,16 @@ const onlyCheckboxHasChangedHack = (previousBody: string, newBody: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export default function useSource(noteBody: string, noteMarkupLanguage: number, themeId: number, highlightedKeywords: string[], noteResources: any, paddingBottom: number, noteHash: string): UseSourceResult {
|
||||
export default function useSource(
|
||||
noteBody: string,
|
||||
noteMarkupLanguage: number,
|
||||
themeId: number,
|
||||
highlightedKeywords: string[],
|
||||
noteResources: any,
|
||||
paddingBottom: number,
|
||||
noteHash: string,
|
||||
initialScroll: number|null,
|
||||
): UseSourceResult {
|
||||
const [html, setHtml] = useState<string>('');
|
||||
const [injectedJs, setInjectedJs] = useState<string[]>([]);
|
||||
const [resourceLoadedTime, setResourceLoadedTime] = useState(0);
|
||||
@@ -142,6 +151,12 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
|
||||
const resourceDownloadMode = Setting.value('sync.resourceDownloadMode');
|
||||
|
||||
// On iOS, the root container has slow inertial scroll, which feels very different from
|
||||
// the native scroll in other apps. This is not the case, however, when a child (e.g. a div)
|
||||
// scrolls the content instead.
|
||||
// Use a div to scroll on iOS instead of the main container:
|
||||
const scrollRenderedMdContainer = shim.mobilePlatform() === 'ios';
|
||||
|
||||
const js = [];
|
||||
js.push('try {');
|
||||
js.push(shim.injectedJs('webviewLib'));
|
||||
@@ -149,15 +164,46 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
// the ReactNativeWebView actually supports only one, so the second arg is ignored (and currently not needed for the mobile app).
|
||||
js.push('window.joplinPostMessage_ = (msg, args) => { return window.ReactNativeWebView.postMessage(msg); };');
|
||||
js.push('webviewLib.initialize({ postMessage: msg => { return window.ReactNativeWebView.postMessage(msg); } });');
|
||||
js.push(`
|
||||
const scrollingElement =
|
||||
${scrollRenderedMdContainer ? 'document.querySelector("#rendered-md")' : 'document.scrollingElement'};
|
||||
let lastScrollTop;
|
||||
const onMainContentScroll = () => {
|
||||
const newScrollTop = scrollingElement.scrollTop;
|
||||
if (lastScrollTop !== newScrollTop) {
|
||||
const eventData = { scrollTop: newScrollTop };
|
||||
window.ReactNativeWebView.postMessage('onscroll:' + JSON.stringify(eventData));
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for events on both scrollingElement and window
|
||||
// - On Android, scrollingElement.addEventListener('scroll', callback) doesn't call callback on
|
||||
// scroll. However, window.addEventListener('scroll', callback) does.
|
||||
// - iOS needs a listener to be added to scrollingElement -- events aren't received when
|
||||
// the listener is added to window with window.addEventListener('scroll', ...).
|
||||
scrollingElement.addEventListener('scroll', onMainContentScroll);
|
||||
window.addEventListener('scroll', onMainContentScroll);
|
||||
|
||||
const scrollContentToPosition = (position) => {
|
||||
scrollingElement.scrollTop = position;
|
||||
};
|
||||
`);
|
||||
js.push(`
|
||||
const readyStateCheckInterval = setInterval(function() {
|
||||
if (document.readyState === "complete") {
|
||||
clearInterval(readyStateCheckInterval);
|
||||
if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload();
|
||||
|
||||
const hash = "${noteHash}";
|
||||
// Gives it a bit of time before scrolling to the anchor
|
||||
// so that images are loaded.
|
||||
if (hash) {
|
||||
const initialScroll = ${JSON.stringify(initialScroll)};
|
||||
|
||||
// Don't scroll to a hash if we're given initial scroll (initial scroll
|
||||
// overrides scrolling to a hash).
|
||||
if ((initialScroll ?? null) !== null) {
|
||||
scrollContentToPosition(initialScroll);
|
||||
} else if (hash) {
|
||||
// Gives it a bit of time before scrolling to the anchor
|
||||
// so that images are loaded.
|
||||
setTimeout(() => {
|
||||
const e = document.getElementById(hash);
|
||||
if (!e) {
|
||||
@@ -171,6 +217,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
}, 10);
|
||||
`);
|
||||
js.push('} catch (e) {');
|
||||
js.push(' console.error(e);');
|
||||
js.push(' window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))');
|
||||
js.push(' true;');
|
||||
js.push('}');
|
||||
@@ -186,10 +233,11 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
iOS seems to increase inertial scrolling friction when the WebView body/root elements
|
||||
scroll. Scroll the main container instead.
|
||||
*/
|
||||
:root > body {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
const scrollRenderedMdContainerCss = `
|
||||
body > #rendered-md {
|
||||
width: 100vw;
|
||||
overflow: auto;
|
||||
@@ -197,10 +245,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
padding-bottom: ${paddingBottom}px;
|
||||
padding-top: ${paddingTop};
|
||||
}
|
||||
|
||||
:root > body {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
const defaultCss = `
|
||||
code {
|
||||
@@ -219,6 +263,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
<style>
|
||||
${defaultCss}
|
||||
${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''}
|
||||
${scrollRenderedMdContainer ? scrollRenderedMdContainerCss : ''}
|
||||
${editPopupCss}
|
||||
</style>
|
||||
${assetsToHeaders(result.pluginAssets, { asHtml: true })}
|
||||
|
@@ -11,18 +11,17 @@ import { WebViewMessageEvent } from 'react-native-webview';
|
||||
import ExtendedWebView, { WebViewControl } from '../../ExtendedWebView';
|
||||
import { clearAutosave, writeAutosave } from './autosave';
|
||||
import { LocalizedStrings } from './js-draw/types';
|
||||
import VersionInfo from 'react-native-version-info';
|
||||
|
||||
|
||||
const logger = Logger.create('ImageEditor');
|
||||
|
||||
type OnSaveCallback = (svgData: string)=> void;
|
||||
type OnCancelCallback = ()=> void;
|
||||
|
||||
// Returns the empty string to load from a template.
|
||||
type LoadInitialSVGCallback = ()=> Promise<string>;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
loadInitialSVGData: LoadInitialSVGCallback;
|
||||
resourceFilename: string|null;
|
||||
onSave: OnSaveCallback;
|
||||
onExit: OnCancelCallback;
|
||||
}
|
||||
@@ -130,20 +129,31 @@ const ImageEditor = (props: Props) => {
|
||||
}, [onRequestCloseEditor]);
|
||||
|
||||
const css = useCss(editorTheme);
|
||||
const html = useMemo(() => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
|
||||
const [html, setHtml] = useState('');
|
||||
|
||||
<style>
|
||||
${css}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`, [css]);
|
||||
useEffect(() => {
|
||||
setHtml(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
|
||||
|
||||
<style id='main-style'>
|
||||
${css}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
// Only set HTML initially (and don't reset). Changing the HTML reloads
|
||||
// the page.
|
||||
//
|
||||
// We need the HTML to initially have the correct CSS to prevent color
|
||||
// changes on load.
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// A set of localization overrides (Joplin is better localized than js-draw).
|
||||
// All localizable strings (some unused?) can be found at
|
||||
@@ -155,10 +165,23 @@ const ImageEditor = (props: Props) => {
|
||||
redo: _('Redo'),
|
||||
}), []);
|
||||
|
||||
const appInfo = useMemo(() => {
|
||||
return {
|
||||
name: 'Joplin',
|
||||
description: `v${VersionInfo.appVersion}`,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const injectedJavaScript = useMemo(() => `
|
||||
window.onerror = (message, source, lineno) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + message + " in file://" + source + ", line " + lineno
|
||||
"error: " + message + " in file://" + source + ", line " + lineno,
|
||||
);
|
||||
};
|
||||
|
||||
window.onunhandledrejection = (error) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + error.reason,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -218,6 +241,7 @@ const ImageEditor = (props: Props) => {
|
||||
${JSON.stringify(Setting.value('imageeditor.jsdrawToolbar'))},
|
||||
${JSON.stringify(Setting.value('locale'))},
|
||||
${JSON.stringify(localizedStrings)},
|
||||
${JSON.stringify({ appInfo })},
|
||||
);
|
||||
|
||||
// Start loading the SVG file (if present) after loading the editor.
|
||||
@@ -231,30 +255,30 @@ const ImageEditor = (props: Props) => {
|
||||
);
|
||||
}
|
||||
true;
|
||||
`, [localizedStrings]);
|
||||
`, [localizedStrings, appInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
webviewRef.current?.injectJS(`
|
||||
document.querySelector('#main-style').innerText = ${JSON.stringify(css)};
|
||||
|
||||
if (window.editorControl) {
|
||||
window.editorControl.onThemeUpdate();
|
||||
}
|
||||
`);
|
||||
}, [editorTheme]);
|
||||
}, [css]);
|
||||
|
||||
const onReadyToLoadData = useCallback(async () => {
|
||||
const initialSVGData = await props.loadInitialSVGData?.() ?? '';
|
||||
|
||||
// It can take some time for initialSVGData to be transferred to the WebView.
|
||||
// Thus, do so after the main content has been loaded.
|
||||
webviewRef.current.injectJS(`(async () => {
|
||||
if (window.editorControl) {
|
||||
const initialSVGData = ${JSON.stringify(initialSVGData)};
|
||||
const initialSVGPath = ${JSON.stringify(props.resourceFilename)};
|
||||
const initialTemplateData = ${JSON.stringify(Setting.value('imageeditor.imageTemplate'))};
|
||||
|
||||
editorControl.loadImageOrTemplate(initialSVGData, initialTemplateData);
|
||||
editorControl.loadImageOrTemplate(initialSVGPath, initialTemplateData);
|
||||
}
|
||||
})();`);
|
||||
}, [webviewRef, props.loadInitialSVGData]);
|
||||
}, [webviewRef, props.resourceFilename]);
|
||||
|
||||
const onMessage = useCallback(async (event: WebViewMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
@@ -293,6 +317,7 @@ const ImageEditor = (props: Props) => {
|
||||
themeId={props.themeId}
|
||||
html={html}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
allowFileAccessFromJs={true}
|
||||
onMessage={onMessage}
|
||||
onError={onError}
|
||||
ref={webviewRef}
|
||||
|
@@ -57,7 +57,7 @@ describe('createJsDrawEditor', () => {
|
||||
});
|
||||
|
||||
// Load no image and an empty template so that autosave can start
|
||||
await editorControl.loadImageOrTemplate(undefined, '{}');
|
||||
await editorControl.loadImageOrTemplate('', '{}');
|
||||
|
||||
expect(calledAutosaveCount).toBe(0);
|
||||
|
||||
|
@@ -120,20 +120,44 @@ export const createJsDrawEditor = (
|
||||
editor.showLoadingWarning(0);
|
||||
editor.setReadOnly(true);
|
||||
|
||||
const fetchInitialSvgData = (resourceUrl: string) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (!resourceUrl) {
|
||||
resolve('');
|
||||
}
|
||||
|
||||
// fetch seems to be unable to request file:// URLs.
|
||||
// https://github.com/react-native-webview/react-native-webview/issues/1560#issuecomment-1783611805
|
||||
const request = new XMLHttpRequest();
|
||||
|
||||
const onError = () => {
|
||||
reject(`Failed to load initial SVG data: ${request.status}, ${request.statusText}, ${request.responseText}`);
|
||||
};
|
||||
|
||||
request.addEventListener('load', _ => {
|
||||
resolve(request.responseText);
|
||||
});
|
||||
request.addEventListener('error', onError);
|
||||
request.addEventListener('abort', onError);
|
||||
|
||||
request.open('GET', resourceUrl);
|
||||
request.send();
|
||||
});
|
||||
};
|
||||
|
||||
const editorControl = {
|
||||
editor,
|
||||
loadImageOrTemplate: async (svgData: string|undefined, templateData: string) => {
|
||||
loadImageOrTemplate: async (resourceUrl: string, templateData: string) => {
|
||||
// loadFromSVG shows its own loading message. Hide the original.
|
||||
editor.hideLoadingWarning();
|
||||
|
||||
if (svgData && svgData.length > 0) {
|
||||
await editor.loadFromSVG(svgData);
|
||||
} else {
|
||||
await applyTemplateToEditor(editor, templateData);
|
||||
const svgData = await fetchInitialSvgData(resourceUrl);
|
||||
|
||||
// The editor expects to be saved initially (without
|
||||
// unsaved changes). Save now.
|
||||
saveNow();
|
||||
// Load from a template if no initial data
|
||||
if (svgData === '') {
|
||||
await applyTemplateToEditor(editor, templateData);
|
||||
} else {
|
||||
await editor.loadFromSVG(svgData);
|
||||
}
|
||||
|
||||
// We can now edit and save safely (without data loss).
|
||||
|
@@ -6,8 +6,8 @@ import { defaultSearchState, SearchPanel } from './SearchPanel';
|
||||
import ExtendedWebView from '../ExtendedWebView';
|
||||
|
||||
import * as React from 'react';
|
||||
import { forwardRef, RefObject, useImperativeHandle } from 'react';
|
||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||
import { forwardRef, useImperativeHandle } from 'react';
|
||||
import { useMemo, useState, useCallback, useRef } from 'react';
|
||||
import { LayoutChangeEvent, View, ViewStyle } from 'react-native';
|
||||
const { editorFont } = require('../global-style');
|
||||
|
||||
@@ -126,7 +126,6 @@ type OnSearchStateChangeCallback = (state: SearchState)=> void;
|
||||
const useEditorControl = (
|
||||
injectJS: OnInjectJSCallback, setLinkDialogVisible: OnSetVisibleCallback,
|
||||
setSearchState: OnSearchStateChangeCallback,
|
||||
searchStateRef: RefObject<SearchState>,
|
||||
): EditorControl => {
|
||||
return useMemo(() => {
|
||||
const execCommand = (command: EditorCommandType) => {
|
||||
@@ -252,16 +251,10 @@ const useEditorControl = (
|
||||
},
|
||||
|
||||
showSearch() {
|
||||
setSearchState({
|
||||
...searchStateRef.current,
|
||||
dialogVisible: true,
|
||||
});
|
||||
execCommand(EditorCommandType.ShowSearch);
|
||||
},
|
||||
hideSearch() {
|
||||
setSearchState({
|
||||
...searchStateRef.current,
|
||||
dialogVisible: false,
|
||||
});
|
||||
execCommand(EditorCommandType.HideSearch);
|
||||
},
|
||||
|
||||
setSearchState: setSearchStateCallback,
|
||||
@@ -269,7 +262,7 @@ const useEditorControl = (
|
||||
};
|
||||
|
||||
return control;
|
||||
}, [injectJS, searchStateRef, setLinkDialogVisible, setSearchState]);
|
||||
}, [injectJS, setLinkDialogVisible, setSearchState]);
|
||||
};
|
||||
|
||||
function NoteEditor(props: Props, ref: any) {
|
||||
@@ -356,22 +349,13 @@ function NoteEditor(props: Props, ref: any) {
|
||||
const [linkDialogVisible, setLinkDialogVisible] = useState(false);
|
||||
const [searchState, setSearchState] = useState(defaultSearchState);
|
||||
|
||||
// Having a [searchStateRef] allows [editorControl] to not be re-created
|
||||
// whenever [searchState] changes.
|
||||
const searchStateRef = useRef(defaultSearchState);
|
||||
|
||||
// Keep the reference and the [searchState] in sync
|
||||
useEffect(() => {
|
||||
searchStateRef.current = searchState;
|
||||
}, [searchState]);
|
||||
|
||||
// Runs [js] in the context of the CodeMirror frame.
|
||||
const injectJS = (js: string) => {
|
||||
webviewRef.current.injectJS(js);
|
||||
};
|
||||
|
||||
const editorControl = useEditorControl(
|
||||
injectJS, setLinkDialogVisible, setSearchState, searchStateRef,
|
||||
injectJS, setLinkDialogVisible, setSearchState,
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
|
@@ -40,9 +40,7 @@ interface ActionButtonProps {
|
||||
onPress: Callback;
|
||||
}
|
||||
|
||||
const ActionButton = (
|
||||
props: ActionButtonProps,
|
||||
) => {
|
||||
const ActionButton = (props: ActionButtonProps) => {
|
||||
return (
|
||||
<CustomButton
|
||||
themeId={props.themeId}
|
||||
|
@@ -70,6 +70,7 @@ interface ScreenHeaderProps {
|
||||
onRedoButtonPress: OnPressCallback;
|
||||
onSaveButtonPress: OnPressCallback;
|
||||
sortButton_press?: OnPressCallback;
|
||||
onSearchButtonPress?: OnPressCallback;
|
||||
|
||||
showSideMenuButton?: boolean;
|
||||
showSearchButton?: boolean;
|
||||
@@ -242,7 +243,11 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
}
|
||||
|
||||
private searchButton_press() {
|
||||
void NavService.go('Search');
|
||||
if (this.props.onSearchButtonPress) {
|
||||
this.props.onSearchButtonPress();
|
||||
} else {
|
||||
void NavService.go('Search');
|
||||
}
|
||||
}
|
||||
|
||||
private async duplicateButton_press() {
|
||||
|
@@ -1,12 +1,12 @@
|
||||
const React = require('react');
|
||||
const { StyleSheet } = require('react-native');
|
||||
import * as React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
const { themeStyle } = require('./global-style.js');
|
||||
|
||||
const rootStyles_ = {};
|
||||
const rootStyles_: Record<number, any> = {};
|
||||
|
||||
class BaseScreenComponent extends React.Component {
|
||||
class BaseScreenComponent<Props, State> extends React.Component<Props, State> {
|
||||
|
||||
rootStyle(themeId) {
|
||||
protected rootStyle(themeId: number) {
|
||||
const theme = themeStyle(themeId);
|
||||
if (rootStyles_[themeId]) return rootStyles_[themeId];
|
||||
rootStyles_[themeId] = StyleSheet.create({
|
||||
@@ -19,4 +19,5 @@ class BaseScreenComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { BaseScreenComponent };
|
||||
export { BaseScreenComponent };
|
||||
export default BaseScreenComponent;
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
import { TouchableNativeFeedback, View, Text } from 'react-native';
|
||||
import Setting, { SettingItem } from '@joplin/lib/models/Setting';
|
||||
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
||||
import { UpdateSettingValueCallback } from './types';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
settingMetadata: SettingItem;
|
||||
updateSettingValue: UpdateSettingValueCallback;
|
||||
}
|
||||
|
||||
const FileSystemPathSelector: FunctionComponent<Props> = props => {
|
||||
const [fileSystemPath, setFileSystemPath] = useState<string>('');
|
||||
|
||||
const settingId = props.settingMetadata.key;
|
||||
|
||||
useEffect(() => {
|
||||
setFileSystemPath(Setting.value(settingId));
|
||||
}, [settingId]);
|
||||
|
||||
const selectDirectoryButtonPress = useCallback(async () => {
|
||||
try {
|
||||
const doc = await openDocumentTree(true);
|
||||
if (doc?.uri) {
|
||||
setFileSystemPath(doc.uri);
|
||||
await props.updateSettingValue(settingId, doc.uri);
|
||||
} else {
|
||||
throw new Error('User cancelled operation');
|
||||
}
|
||||
} catch (e) {
|
||||
reg.logger().info('Didn\'t pick sync dir: ', e);
|
||||
}
|
||||
}, [props.updateSettingValue, settingId]);
|
||||
|
||||
// Unsupported on non-Android platforms.
|
||||
if (!shim.fsDriver().isUsingAndroidSAF()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const styleSheet = props.styles.styleSheet;
|
||||
|
||||
return (
|
||||
<TouchableNativeFeedback
|
||||
onPress={selectDirectoryButtonPress}
|
||||
style={styleSheet.settingContainer}
|
||||
>
|
||||
<View style={styleSheet.settingContainer}>
|
||||
<Text key="label" style={styleSheet.settingText}>
|
||||
{props.settingMetadata.label()}
|
||||
</Text>
|
||||
<Text style={styleSheet.settingControl}>
|
||||
{fileSystemPath}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableNativeFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSystemPathSelector;
|
@@ -0,0 +1,44 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import exportDebugReport from './utils/exportDebugReport';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import SettingsButton from '../SettingsButton';
|
||||
import { ConfigScreenStyles } from '../configScreenStyles';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
}
|
||||
|
||||
export const exportDebugReportTitle = () => _('Export Debug Report');
|
||||
|
||||
const ExportDebugReportButton = (props: Props) => {
|
||||
const [creatingReport, setCreatingReport] = useState(false);
|
||||
|
||||
const exportDebugButtonPress = useCallback(async () => {
|
||||
setCreatingReport(true);
|
||||
|
||||
await exportDebugReport();
|
||||
|
||||
setCreatingReport(false);
|
||||
}, [setCreatingReport]);
|
||||
|
||||
const exportDebugReportButton = (
|
||||
<SettingsButton
|
||||
title={creatingReport ? _('Creating report...') : exportDebugReportTitle()}
|
||||
clickHandler={exportDebugButtonPress}
|
||||
styles={props.styles}
|
||||
disabled={creatingReport}
|
||||
/>
|
||||
);
|
||||
|
||||
// The debug functionality is only supported on Android.
|
||||
if (shim.mobilePlatform() !== 'android') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return exportDebugReportButton;
|
||||
};
|
||||
|
||||
export default ExportDebugReportButton;
|
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { View, Button } from 'react-native';
|
||||
import { TextInput } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import exportProfile from './utils/exportProfile';
|
||||
import { ConfigScreenStyles } from '../configScreenStyles';
|
||||
import SettingsButton from '../SettingsButton';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
}
|
||||
|
||||
export const exportProfileButtonTitle = () => _('Export profile');
|
||||
|
||||
const ExportProfileButton = (props: Props) => {
|
||||
const [profileExportStatus, setProfileExportStatus] = useState<'idle'|'prompt'|'exporting'>('idle');
|
||||
const [profileExportPath, setProfileExportPath] = useState<string>('');
|
||||
|
||||
const exportProfileButtonPress = useCallback(async () => {
|
||||
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
|
||||
if (!externalDir) {
|
||||
return;
|
||||
}
|
||||
const p = profileExportPath ? profileExportPath : `${externalDir}/JoplinProfileExport`;
|
||||
|
||||
setProfileExportStatus('prompt');
|
||||
setProfileExportPath(p);
|
||||
}, [profileExportPath]);
|
||||
|
||||
const exportProfileButton = (
|
||||
<SettingsButton
|
||||
styles={props.styles}
|
||||
title={profileExportStatus === 'exporting' ? _('Exporting profile...') : exportProfileButtonTitle()}
|
||||
clickHandler={exportProfileButtonPress}
|
||||
description={_('For debugging purpose only: export your profile to an external SD card.')}
|
||||
disabled={profileExportStatus === 'exporting'}
|
||||
/>
|
||||
);
|
||||
|
||||
const exportProfileButtonPress2 = useCallback(async () => {
|
||||
setProfileExportStatus('exporting');
|
||||
|
||||
await exportProfile(profileExportPath);
|
||||
|
||||
setProfileExportStatus('idle');
|
||||
}, [profileExportPath]);
|
||||
|
||||
const profileExportPrompt = (
|
||||
<View>
|
||||
<TextInput
|
||||
label={_('Path:')}
|
||||
onChangeText={text => setProfileExportPath(text)}
|
||||
value={profileExportPath}
|
||||
placeholder="/path/to/sdcard"
|
||||
keyboardAppearance={props.styles.keyboardAppearance} />
|
||||
<Button
|
||||
onPress={exportProfileButtonPress2}
|
||||
title={_('OK')}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const mainContent = (
|
||||
<>
|
||||
{exportProfileButton}
|
||||
{profileExportStatus === 'prompt' ? profileExportPrompt : null}
|
||||
</>
|
||||
);
|
||||
|
||||
// The debug functionality is only supported on Android.
|
||||
if (shim.mobilePlatform() !== 'android') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mainContent;
|
||||
};
|
||||
|
||||
export default ExportProfileButton;
|
@@ -7,10 +7,10 @@ import { FunctionComponent, useCallback, useState } from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { join } from 'path';
|
||||
import Share from 'react-native-share';
|
||||
import exportAllFolders, { makeExportCacheDirectory } from './exportAllFolders';
|
||||
import exportAllFolders, { makeExportCacheDirectory } from './utils/exportAllFolders';
|
||||
import { ExportProgressState } from '@joplin/lib/services/interop/types';
|
||||
import { ConfigScreenStyles } from '../configScreenStyles';
|
||||
import ConfigScreenButton from '../ConfigScreenButton';
|
||||
import SettingsButton from '../SettingsButton';
|
||||
|
||||
const logger = Logger.create('NoteExportButton');
|
||||
|
||||
@@ -24,6 +24,9 @@ enum ExportStatus {
|
||||
Exported,
|
||||
}
|
||||
|
||||
export const exportButtonTitle = () => _('Export all notes as JEX');
|
||||
export const exportButtonDescription = () => _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
|
||||
|
||||
const NoteExportButton: FunctionComponent<Props> = props => {
|
||||
const [exportStatus, setExportStatus] = useState<ExportStatus>(ExportStatus.NotStarted);
|
||||
const [exportProgress, setExportProgress] = useState<number|undefined>(0);
|
||||
@@ -80,13 +83,12 @@ const NoteExportButton: FunctionComponent<Props> = props => {
|
||||
indeterminate={exportProgress === undefined}
|
||||
progress={exportProgress}/>
|
||||
);
|
||||
const descriptionText = _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
|
||||
|
||||
const startOrCancelExportButton = (
|
||||
<ConfigScreenButton
|
||||
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : _('Export all notes as JEX')}
|
||||
<SettingsButton
|
||||
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : exportButtonTitle()}
|
||||
disabled={exportStatus === ExportStatus.Exporting}
|
||||
description={exportStatus === ExportStatus.NotStarted ? descriptionText : null}
|
||||
description={exportStatus === ExportStatus.NotStarted ? exportButtonDescription() : null}
|
||||
statusComponent={progressComponent}
|
||||
clickHandler={startExport}
|
||||
styles={props.styles}
|
||||
@@ -96,14 +98,14 @@ const NoteExportButton: FunctionComponent<Props> = props => {
|
||||
return startOrCancelExportButton;
|
||||
} else {
|
||||
const warningComponent = (
|
||||
<Text style={props.styles.warningText}>
|
||||
<Text style={props.styles.styleSheet.warningText}>
|
||||
{_('Warnings:\n%s', warnings)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const exportSummary = (
|
||||
<View style={props.styles.settingContainer}>
|
||||
<Text style={props.styles.descriptionText}>{_('Exported successfully!')}</Text>
|
||||
<View style={props.styles.styleSheet.settingContainer}>
|
||||
<Text style={props.styles.styleSheet.descriptionText}>{_('Exported successfully!')}</Text>
|
||||
{warnings.length > 0 ? warningComponent : null}
|
||||
</View>
|
||||
);
|
||||
|
@@ -0,0 +1,32 @@
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ReportService from '@joplin/lib/services/ReportService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import time from '@joplin/lib/time';
|
||||
|
||||
const exportDebugReport = async () => {
|
||||
const service = new ReportService();
|
||||
|
||||
const logItems = await reg.logger().lastEntries(null);
|
||||
const logItemRows = [['Date', 'Level', 'Message']];
|
||||
for (let i = 0; i < logItems.length; i++) {
|
||||
const item = logItems[i];
|
||||
logItemRows.push([time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss'), item.level, item.message]);
|
||||
}
|
||||
const logItemCsv = service.csvCreate(logItemRows);
|
||||
|
||||
const itemListCsv = await service.basicItemList({ format: 'csv' });
|
||||
|
||||
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
|
||||
|
||||
if (!externalDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = `${externalDir}/syncReport-${new Date().getTime()}.txt`;
|
||||
|
||||
const finalText = [logItemCsv, itemListCsv].join('\n================================================================================\n');
|
||||
await shim.fsDriver().writeFile(filePath, finalText, 'utf8');
|
||||
alert(`Debug report exported to ${filePath}`);
|
||||
};
|
||||
|
||||
export default exportDebugReport;
|
@@ -0,0 +1,35 @@
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const exportProfile = async (profileExportPath: string) => {
|
||||
const dbPath = '/data/data/net.cozic.joplin/databases';
|
||||
const exportPath = profileExportPath;
|
||||
const resourcePath = `${exportPath}/resources`;
|
||||
try {
|
||||
const copyFiles = async (source: string, dest: string) => {
|
||||
await shim.fsDriver().mkdir(dest);
|
||||
|
||||
const files = await shim.fsDriver().readDirStats(source);
|
||||
|
||||
for (const file of files) {
|
||||
const source_ = `${source}/${file.path}`;
|
||||
const dest_ = `${dest}/${file.path}`;
|
||||
if (!file.isDirectory()) {
|
||||
reg.logger().info(`Copying profile: ${source_} => ${dest_}`);
|
||||
await shim.fsDriver().copy(source_, dest_);
|
||||
} else {
|
||||
await copyFiles(source_, dest_);
|
||||
}
|
||||
}
|
||||
};
|
||||
await copyFiles(dbPath, exportPath);
|
||||
await copyFiles(Setting.value('resourceDir'), resourcePath);
|
||||
|
||||
alert('Profile has been exported!');
|
||||
} catch (error) {
|
||||
alert(`Could not export files: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default exportProfile;
|
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ConfigScreenStyleSheet } from './configScreenStyles';
|
||||
import { View, Text, LayoutChangeEvent } from 'react-native';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyleSheet;
|
||||
title: string;
|
||||
onLayout?: (event: LayoutChangeEvent)=> void;
|
||||
}
|
||||
|
||||
const SectionHeader: React.FunctionComponent<Props> = props => {
|
||||
return (
|
||||
<View
|
||||
style={props.styles.headerWrapperStyle}
|
||||
onLayout={props.onLayout}
|
||||
>
|
||||
<Text style={props.styles.headerTextStyle}>
|
||||
{props.title}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionHeader;
|
@@ -0,0 +1,108 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting';
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from 'react';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
import { FlatList, Text, Pressable, View, ViewStyle } from 'react-native';
|
||||
import { settingsSections } from '@joplin/lib/components/shared/config/config-shared';
|
||||
import Icon from '../../Icon';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
|
||||
width: number|undefined;
|
||||
|
||||
settings: any;
|
||||
selectedSectionName: string|null;
|
||||
openSection: (sectionName: string)=> void;
|
||||
}
|
||||
|
||||
const SectionSelector: FunctionComponent<Props> = props => {
|
||||
const sections = useMemo(() => {
|
||||
return settingsSections({ device: AppType.Mobile, settings: props.settings });
|
||||
}, [props.settings]);
|
||||
const styles = props.styles.styleSheet;
|
||||
|
||||
const itemHeight = styles.sidebarButton.height;
|
||||
|
||||
const onRenderButton = ({ item }: { item: SettingMetadataSection }) => {
|
||||
const section = item;
|
||||
const selected = props.selectedSectionName === section.name;
|
||||
const icon = Setting.sectionNameToIcon(section.name, AppType.Mobile);
|
||||
const label = Setting.sectionNameToLabel(section.name);
|
||||
const shortDescription = Setting.sectionMetadataToSummary(section);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={section.name}
|
||||
role='tab'
|
||||
aria-selected={selected}
|
||||
onPress={() => props.openSection(section.name)}
|
||||
style={selected ? styles.selectedSidebarButton : styles.sidebarButton}
|
||||
>
|
||||
<Icon
|
||||
name={icon}
|
||||
accessibilityLabel={null}
|
||||
style={styles.sidebarIcon}
|
||||
/>
|
||||
<View style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
<Text
|
||||
style={selected ? styles.sidebarSelectedButtonText : styles.sidebarButtonMainText}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.sidebarButtonDescriptionText}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{shortDescription ?? ''}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
const [flatListRef, setFlatListRef] = useState<FlatList|null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (flatListRef && props.selectedSectionName) {
|
||||
let selectedIndex = 0;
|
||||
for (const section of sections) {
|
||||
if (section.name === props.selectedSectionName) {
|
||||
break;
|
||||
}
|
||||
selectedIndex ++;
|
||||
}
|
||||
|
||||
flatListRef.scrollToIndex({
|
||||
index: selectedIndex,
|
||||
viewPosition: 0.5,
|
||||
});
|
||||
}
|
||||
}, [props.selectedSectionName, flatListRef, sections]);
|
||||
|
||||
const containerStyle: ViewStyle = useMemo(() => ({
|
||||
width: props.width,
|
||||
maxWidth: props.width,
|
||||
minWidth: props.width,
|
||||
flex: 1,
|
||||
}), [props.width]);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<FlatList
|
||||
role='tablist'
|
||||
ref={setFlatListRef}
|
||||
data={sections}
|
||||
renderItem={onRenderButton}
|
||||
keyExtractor={item => item.name}
|
||||
getItemLayout={(_data, index) => ({
|
||||
length: itemHeight, offset: itemHeight * index, index,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionSelector;
|
@@ -0,0 +1,156 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { UpdateSettingValueCallback } from './types';
|
||||
import { View, Text, TextInput } from 'react-native';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Dropdown from '../../Dropdown';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import SettingsToggle from './SettingsToggle';
|
||||
import FileSystemPathSelector from './FileSystemPathSelector';
|
||||
import shim from '@joplin/lib/shim';
|
||||
const { themeStyle } = require('../../global-style.js');
|
||||
|
||||
interface Props {
|
||||
settingId: string;
|
||||
|
||||
// The value associated with the given settings key
|
||||
value: any;
|
||||
|
||||
styles: ConfigScreenStyles;
|
||||
themeId: number;
|
||||
|
||||
updateSettingValue: UpdateSettingValueCallback;
|
||||
}
|
||||
|
||||
|
||||
const SettingComponent: React.FunctionComponent<Props> = props => {
|
||||
const themeId = props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
const output: any = null;
|
||||
|
||||
const md = Setting.settingMetadata(props.settingId);
|
||||
const settingDescription = md.description ? md.description() : '';
|
||||
|
||||
const styleSheet = props.styles.styleSheet;
|
||||
|
||||
const descriptionComp = !settingDescription ? null : <Text style={styleSheet.settingDescriptionText}>{settingDescription}</Text>;
|
||||
const containerStyle = props.styles.getContainerStyle(!!settingDescription);
|
||||
|
||||
if (md.isEnum) {
|
||||
const value = props.value.toString();
|
||||
|
||||
const items = Setting.enumOptionsToValueLabels(md.options(), md.optionsOrder ? md.optionsOrder() : []);
|
||||
|
||||
return (
|
||||
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
|
||||
<View style={containerStyle}>
|
||||
<Text key="label" style={styleSheet.settingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<Dropdown
|
||||
key="control"
|
||||
items={items as any}
|
||||
selectedValue={value}
|
||||
itemListStyle={{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
}}
|
||||
headerStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
itemStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
onValueChange={(itemValue: string) => {
|
||||
void props.updateSettingValue(props.settingId, itemValue);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{descriptionComp}
|
||||
</View>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BOOL) {
|
||||
return (
|
||||
<SettingsToggle
|
||||
settingId={props.settingId}
|
||||
value={props.value}
|
||||
themeId={props.themeId}
|
||||
styles={props.styles}
|
||||
label={md.label()}
|
||||
updateSettingValue={props.updateSettingValue}
|
||||
description={descriptionComp}
|
||||
/>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_INT) {
|
||||
const unitLabel = md.unitLabel ? md.unitLabel(props.value) : props.value;
|
||||
const minimum = 'minimum' in md ? md.minimum : 0;
|
||||
const maximum = 'maximum' in md ? md.maximum : 10;
|
||||
|
||||
// Note: Do NOT add the minimumTrackTintColor and maximumTrackTintColor props
|
||||
// on the Slider as they are buggy and can crash the app on certain devices.
|
||||
// https://github.com/laurent22/joplin/issues/2733
|
||||
// https://github.com/react-native-community/react-native-slider/issues/161
|
||||
return (
|
||||
<View key={props.settingId} style={styleSheet.settingContainer}>
|
||||
<Text key="label" style={styleSheet.settingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', flex: 1 }}>
|
||||
<Text style={styleSheet.sliderUnits}>{unitLabel}</Text>
|
||||
<Slider
|
||||
key="control"
|
||||
style={{ flex: 1 }}
|
||||
step={md.step}
|
||||
minimumValue={minimum}
|
||||
maximumValue={maximum}
|
||||
value={props.value}
|
||||
onValueChange={newValue => void props.updateSettingValue(props.settingId, newValue)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_STRING) {
|
||||
if (md.key === 'sync.2.path' && shim.fsDriver().isUsingAndroidSAF()) {
|
||||
return (
|
||||
<FileSystemPathSelector
|
||||
styles={props.styles}
|
||||
settingMetadata={md}
|
||||
updateSettingValue={props.updateSettingValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
|
||||
<View key={props.settingId} style={containerStyle}>
|
||||
<Text key="label" style={styleSheet.settingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<TextInput
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.settingKeyboardAppearance}
|
||||
autoCapitalize="none"
|
||||
key="control"
|
||||
style={styleSheet.settingControl}
|
||||
value={props.value}
|
||||
onChangeText={(newValue: string) => void props.updateSettingValue(props.settingId, newValue)}
|
||||
secureTextEntry={!!md.secure}
|
||||
/>
|
||||
</View>
|
||||
{descriptionComp}
|
||||
</View>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BUTTON) {
|
||||
// TODO: Not yet supported
|
||||
} else if (Setting.value('env') === 'dev') {
|
||||
throw new Error(`Unsupported setting type: ${md.type}`);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
export default SettingComponent;
|
@@ -6,25 +6,27 @@ import { ConfigScreenStyles } from './configScreenStyles';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
clickHandler: ()=> void;
|
||||
styles: ConfigScreenStyles;
|
||||
disabled?: boolean;
|
||||
statusComponent?: ReactNode;
|
||||
}
|
||||
|
||||
const ConfigScreenButton: FunctionComponent<Props> = props => {
|
||||
const SettingsButton: FunctionComponent<Props> = props => {
|
||||
const styles = props.styles.styleSheet;
|
||||
|
||||
let descriptionComp = null;
|
||||
if (props.description) {
|
||||
descriptionComp = (
|
||||
<View style={{ flex: 1, marginTop: 10 }}>
|
||||
<Text style={props.styles.descriptionText}>{props.description}</Text>
|
||||
<Text style={styles.descriptionText}>{props.description}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={props.styles.settingContainer}>
|
||||
<View style={styles.settingContainer}>
|
||||
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button title={props.title} onPress={props.clickHandler} disabled={!!props.disabled} />
|
||||
@@ -35,4 +37,4 @@ const ConfigScreenButton: FunctionComponent<Props> = props => {
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default ConfigScreenButton;
|
||||
export default SettingsButton;
|
@@ -0,0 +1,45 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent, ReactNode } from 'react';
|
||||
|
||||
import { View, Text, Switch } from 'react-native';
|
||||
import { UpdateSettingValueCallback } from './types';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
|
||||
interface Props {
|
||||
settingId: string;
|
||||
value: any;
|
||||
|
||||
themeId: number;
|
||||
styles: ConfigScreenStyles;
|
||||
|
||||
label: string;
|
||||
updateSettingValue: UpdateSettingValueCallback;
|
||||
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
const SettingsToggle: FunctionComponent<Props> = props => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const styleSheet = props.styles.styleSheet;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={props.styles.getContainerStyle(false)}>
|
||||
<Text key="label" style={styleSheet.switchSettingText}>
|
||||
{props.label}
|
||||
</Text>
|
||||
<Switch
|
||||
key="control"
|
||||
style={styleSheet.switchSettingControl}
|
||||
trackColor={{ false: theme.dividerColor }}
|
||||
value={props.value}
|
||||
onValueChange={(value: boolean) => void props.updateSettingValue(props.settingId, value)}
|
||||
/>
|
||||
</View>
|
||||
{props.description}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsToggle;
|
@@ -1,13 +1,16 @@
|
||||
import { TextStyle, ViewStyle, StyleSheet } from 'react-native';
|
||||
const { themeStyle } = require('../../global-style.js');
|
||||
|
||||
export interface ConfigScreenStyles {
|
||||
type SidebarButtonStyle = ViewStyle & { height: number };
|
||||
|
||||
export interface ConfigScreenStyleSheet {
|
||||
body: ViewStyle;
|
||||
|
||||
settingContainer: ViewStyle;
|
||||
settingContainerNoBottomBorder: ViewStyle;
|
||||
headerWrapperStyle: ViewStyle;
|
||||
|
||||
headerTextStyle: TextStyle;
|
||||
settingText: TextStyle;
|
||||
linkText: TextStyle;
|
||||
descriptionText: TextStyle;
|
||||
@@ -22,9 +25,24 @@ export interface ConfigScreenStyles {
|
||||
switchSettingContainer: ViewStyle;
|
||||
switchSettingControl: TextStyle;
|
||||
|
||||
sidebarButton: SidebarButtonStyle;
|
||||
sidebarIcon: TextStyle;
|
||||
selectedSidebarButton: SidebarButtonStyle;
|
||||
sidebarButtonMainText: TextStyle;
|
||||
sidebarSelectedButtonText: TextStyle;
|
||||
sidebarButtonDescriptionText: TextStyle;
|
||||
|
||||
settingControl: TextStyle;
|
||||
}
|
||||
|
||||
export interface ConfigScreenStyles {
|
||||
styleSheet: ConfigScreenStyleSheet;
|
||||
|
||||
selectedSectionButtonColor: string;
|
||||
keyboardAppearance: 'default'|'light'|'dark';
|
||||
getContainerStyle(hasDescription: boolean): ViewStyle;
|
||||
}
|
||||
|
||||
const configScreenStyles = (themeId: number): ConfigScreenStyles => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
@@ -54,7 +72,31 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
|
||||
borderBottomColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
const styles: ConfigScreenStyles = {
|
||||
const sidebarButtonHeight = theme.fontSize * 4 + 5;
|
||||
const sidebarButton: SidebarButtonStyle = {
|
||||
height: sidebarButtonHeight,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingEnd: theme.marginRight,
|
||||
};
|
||||
|
||||
const sidebarButtonMainText: TextStyle = {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
};
|
||||
|
||||
const fadedOpacity = 0.75;
|
||||
const sidebarButtonDescriptionText: TextStyle = {
|
||||
...sidebarButtonMainText,
|
||||
fontSize: theme.fontSizeSmaller,
|
||||
color: theme.color,
|
||||
opacity: fadedOpacity,
|
||||
paddingTop: 3,
|
||||
};
|
||||
|
||||
|
||||
const styles: ConfigScreenStyleSheet = {
|
||||
body: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-start',
|
||||
@@ -119,6 +161,8 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
|
||||
headerTextStyle: theme.headerStyle,
|
||||
|
||||
headerWrapperStyle: {
|
||||
...settingContainerStyle,
|
||||
...theme.headerWrapperStyle,
|
||||
@@ -129,9 +173,40 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
|
||||
color: undefined,
|
||||
flex: 0,
|
||||
},
|
||||
|
||||
|
||||
sidebarButton,
|
||||
selectedSidebarButton: {
|
||||
...sidebarButton,
|
||||
backgroundColor: theme.selectedColor,
|
||||
},
|
||||
|
||||
sidebarButtonMainText: sidebarButtonMainText,
|
||||
sidebarIcon: {
|
||||
...sidebarButtonMainText,
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
width: sidebarButtonHeight * 0.8,
|
||||
opacity: fadedOpacity,
|
||||
},
|
||||
sidebarSelectedButtonText: {
|
||||
...sidebarButtonMainText,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
sidebarButtonDescriptionText,
|
||||
};
|
||||
|
||||
return StyleSheet.create(styles);
|
||||
const styleSheet = StyleSheet.create(styles);
|
||||
|
||||
return {
|
||||
styleSheet,
|
||||
|
||||
selectedSectionButtonColor: theme.selectedColor,
|
||||
keyboardAppearance: theme.keyboardAppearance,
|
||||
getContainerStyle: (hasDescription) => {
|
||||
return !hasDescription ? styleSheet.settingContainer : styleSheet.settingContainerNoBottomBorder;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default configScreenStyles;
|
||||
|
10
packages/app-mobile/components/screens/ConfigScreen/types.ts
Normal file
10
packages/app-mobile/components/screens/ConfigScreen/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export interface CustomSettingSection {
|
||||
component: ReactElement;
|
||||
icon: string;
|
||||
title: string;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
export type UpdateSettingValueCallback = (key: string, value: any)=> Promise<void>;
|
@@ -32,7 +32,7 @@ const { Checkbox } = require('../checkbox.js');
|
||||
import { _, currentLocale } from '@joplin/lib/locale';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { themeStyle, editorFont } = require('../global-style.js');
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
@@ -61,6 +61,9 @@ const emptyArray: any[] = [];
|
||||
const logger = Logger.create('screens/Note');
|
||||
|
||||
class NoteScreenComponent extends BaseScreenComponent {
|
||||
// This isn't in this.state because we don't want changing scroll to trigger
|
||||
// a re-render.
|
||||
private lastBodyScroll: number|undefined = undefined;
|
||||
|
||||
public static navigationOptions(): any {
|
||||
return { header: null };
|
||||
@@ -81,7 +84,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
fromShare: false,
|
||||
showCamera: false,
|
||||
showImageEditor: false,
|
||||
loadImageEditorData: null,
|
||||
imageEditorResource: null,
|
||||
noteResources: {},
|
||||
|
||||
@@ -747,7 +749,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
if (this.useEditorBeta()) {
|
||||
// The beta editor needs to be explicitly informed of changes
|
||||
// to the note's body
|
||||
this.editorRef.current.insertText(newText);
|
||||
if (this.editorRef.current) {
|
||||
this.editorRef.current.insertText(newText);
|
||||
} else {
|
||||
logger.error(`Tried to attach resource ${resource.id} to the note when the editor is not visible!`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newNote.body += `\n${resourceTag}`;
|
||||
@@ -812,31 +818,34 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}, 'image');
|
||||
}
|
||||
|
||||
private drawPicture_onPress = async () => {
|
||||
// Create a new empty drawing and attach it now.
|
||||
const resource = await this.attachNewDrawing('');
|
||||
await this.editDrawing(resource);
|
||||
};
|
||||
|
||||
private async updateDrawing(svgData: string) {
|
||||
let resource: ResourceEntity|null = this.state.imageEditorResource;
|
||||
|
||||
if (!resource) {
|
||||
throw new Error('No resource is loaded in the editor');
|
||||
resource = await this.attachNewDrawing(svgData);
|
||||
|
||||
// Set resouce and file path to allow
|
||||
// 1. subsequent saves to update the resource
|
||||
// 2. the editor to load from the resource's filepath (can happen
|
||||
// if the webview is reloaded).
|
||||
this.setState({
|
||||
imageEditorResourceFilepath: Resource.fullPath(resource),
|
||||
imageEditorResource: resource,
|
||||
});
|
||||
} else {
|
||||
logger.info('Saving drawing to resource', resource.id);
|
||||
|
||||
const tempFilePath = join(Setting.value('tempDir'), uuid.createNano());
|
||||
await shim.fsDriver().writeFile(tempFilePath, svgData, 'utf8');
|
||||
|
||||
resource = await Resource.updateResourceBlobContent(
|
||||
resource.id,
|
||||
tempFilePath,
|
||||
);
|
||||
await shim.fsDriver().remove(tempFilePath);
|
||||
|
||||
await this.refreshResource(resource);
|
||||
}
|
||||
|
||||
logger.info('Saving drawing to resource', resource.id);
|
||||
|
||||
const tempFilePath = join(Setting.value('tempDir'), uuid.createNano());
|
||||
await shim.fsDriver().writeFile(tempFilePath, svgData, 'utf8');
|
||||
|
||||
resource = await Resource.updateResourceBlobContent(
|
||||
resource.id,
|
||||
tempFilePath,
|
||||
);
|
||||
await shim.fsDriver().remove(tempFilePath);
|
||||
|
||||
await this.refreshResource(resource);
|
||||
}
|
||||
|
||||
private onSaveDrawing = async (svgData: string) => {
|
||||
@@ -847,13 +856,28 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ showImageEditor: false });
|
||||
};
|
||||
|
||||
private drawPicture_onPress = async () => {
|
||||
if (this.state.mode === 'edit') {
|
||||
// Create a new empty drawing and attach it now, before the image editor is opened.
|
||||
// With the present structure of Note.tsx, the we can't use this.editorRef while
|
||||
// the image editor is open, and thus can't attach drawings at the cursor locaiton.
|
||||
const resource = await this.attachNewDrawing('');
|
||||
await this.editDrawing(resource);
|
||||
} else {
|
||||
logger.info('Showing image editor...');
|
||||
this.setState({
|
||||
showImageEditor: true,
|
||||
imageEditorResourceFilepath: null,
|
||||
imageEditorResource: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private async editDrawing(item: BaseItem) {
|
||||
const filePath = Resource.fullPath(item);
|
||||
this.setState({
|
||||
showImageEditor: true,
|
||||
loadImageEditorData: async () => {
|
||||
return await shim.fsDriver().readFile(filePath);
|
||||
},
|
||||
imageEditorResourceFilepath: filePath,
|
||||
imageEditorResource: item,
|
||||
});
|
||||
}
|
||||
@@ -1260,6 +1284,10 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}, 5);
|
||||
}
|
||||
|
||||
private onBodyViewerScroll = (scrollTop: number) => {
|
||||
this.lastBodyScroll = scrollTop;
|
||||
};
|
||||
|
||||
public onBodyViewerCheckboxChange(newBody: string) {
|
||||
void this.saveOneProperty('body', newBody);
|
||||
}
|
||||
@@ -1302,7 +1330,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return <CameraView themeId={this.props.themeId} style={{ flex: 1 }} onPhoto={this.cameraView_onPhoto} onCancel={this.cameraView_onCancel} />;
|
||||
} else if (this.state.showImageEditor) {
|
||||
return <ImageEditor
|
||||
loadInitialSVGData={this.state.loadImageEditorData}
|
||||
resourceFilename={this.state.imageEditorResourceFilepath}
|
||||
themeId={this.props.themeId}
|
||||
onSave={this.onSaveDrawing}
|
||||
onExit={this.onCloseDrawing}
|
||||
@@ -1334,6 +1362,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
onMarkForDownload={this.onMarkForDownload}
|
||||
onRequestEditResource={this.onEditResource}
|
||||
onLoadEnd={this.onBodyViewerLoadEnd}
|
||||
onScroll={this.onBodyViewerScroll}
|
||||
initialScroll={this.lastBodyScroll}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -1416,7 +1446,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
if (this.state.mode === 'edit') return null;
|
||||
|
||||
return <ActionButton mainButton={editButton} />;
|
||||
return <ActionButton mainButton={editButton} dispatch={this.props.dispatch} />;
|
||||
};
|
||||
|
||||
// Save button is not really needed anymore with the improved save logic
|
||||
|
@@ -13,9 +13,10 @@ import { _ } from '@joplin/lib/locale';
|
||||
import ActionButton from '../ActionButton';
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { BackButtonService } = require('../../services/back-button.js');
|
||||
import { AppState } from '../../utils/types';
|
||||
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids.js');
|
||||
|
||||
class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
|
||||
@@ -108,7 +109,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
}
|
||||
|
||||
public async componentDidUpdate(prevProps: any) {
|
||||
if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId !== this.props.selectedFolderId || prevProps.selectedTagId !== this.props.selectedTagId || prevProps.selectedSmartFilterId !== this.props.selectedSmartFilterId || prevProps.notesParentType !== this.props.notesParentType) {
|
||||
if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId !== this.props.selectedFolderId || prevProps.selectedTagId !== this.props.selectedTagId || prevProps.selectedSmartFilterId !== this.props.selectedSmartFilterId || prevProps.notesParentType !== this.props.notesParentType || prevProps.uncompletedTodosOnTop !== this.props.uncompletedTodosOnTop || prevProps.showCompletedTodos !== this.props.showCompletedTodos) {
|
||||
await this.refreshNotes(this.props);
|
||||
}
|
||||
}
|
||||
@@ -223,17 +224,32 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
let buttonFolderId = this.props.selectedFolderId !== Folder.conflictFolderId() ? this.props.selectedFolderId : null;
|
||||
if (!buttonFolderId) buttonFolderId = this.props.activeFolderId;
|
||||
|
||||
const addFolderNoteButtons = !!buttonFolderId;
|
||||
const isAllNotes =
|
||||
this.props.notesParentType === 'SmartFilter'
|
||||
&& this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID;
|
||||
|
||||
// Usually, when showing all notes, activeFolderId/selectedFolderId is set to the last
|
||||
// active folder.
|
||||
// If the app starts showing all notes, activeFolderId/selectedFolderId are
|
||||
// empty or null. As such, we need a special case to show the buttons:
|
||||
const addFolderNoteButtons = !!buttonFolderId || isAllNotes;
|
||||
const thisComp = this;
|
||||
|
||||
const makeActionButtonComp = () => {
|
||||
const getTargetFolderId = async () => {
|
||||
if (!buttonFolderId && isAllNotes) {
|
||||
return (await Folder.defaultFolder()).id;
|
||||
}
|
||||
return buttonFolderId;
|
||||
};
|
||||
if (addFolderNoteButtons && this.props.folders.length > 0) {
|
||||
const buttons = [];
|
||||
buttons.push({
|
||||
label: _('New to-do'),
|
||||
onPress: () => {
|
||||
onPress: async () => {
|
||||
const folderId = await getTargetFolderId();
|
||||
const isTodo = true;
|
||||
void this.newNoteNavigate(buttonFolderId, isTodo);
|
||||
void this.newNoteNavigate(folderId, isTodo);
|
||||
},
|
||||
color: '#9b59b6',
|
||||
icon: 'checkbox-outline',
|
||||
@@ -241,14 +257,15 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
|
||||
buttons.push({
|
||||
label: _('New note'),
|
||||
onPress: () => {
|
||||
onPress: async () => {
|
||||
const folderId = await getTargetFolderId();
|
||||
const isTodo = false;
|
||||
void this.newNoteNavigate(buttonFolderId, isTodo);
|
||||
void this.newNoteNavigate(folderId, isTodo);
|
||||
},
|
||||
color: '#9b59b6',
|
||||
icon: 'document',
|
||||
});
|
||||
return <ActionButton buttons={buttons}/>;
|
||||
return <ActionButton buttons={buttons} dispatch={this.props.dispatch}/>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@@ -4,7 +4,7 @@ const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const Shared = require('@joplin/lib/components/shared/dropbox-login-shared');
|
||||
|
@@ -5,7 +5,7 @@ const { connect } = require('react-redux');
|
||||
const Folder = require('@joplin/lib/models/Folder').default;
|
||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { default: FolderPicker } = require('../FolderPicker');
|
||||
|
@@ -7,7 +7,7 @@ const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const { reg } = require('@joplin/lib/registry.js');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const parseUri = require('@joplin/lib/parseUri');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
|
@@ -7,7 +7,7 @@ const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
const { NoteItem } = require('../note-item.js');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
import SearchEngineUtils from '@joplin/lib/services/searchengine/SearchEngineUtils';
|
||||
@@ -98,7 +98,7 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
|
||||
if (query) {
|
||||
if (this.props.settings['db.ftsEnabled']) {
|
||||
notes = await SearchEngineUtils.notesForQuery(query, true);
|
||||
notes = await SearchEngineUtils.notesForQuery(query, true, { appendWildCards: true });
|
||||
} else {
|
||||
const p = query.split(' ');
|
||||
const temp = [];
|
||||
|
@@ -6,7 +6,7 @@ const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const ReportService = require('@joplin/lib/services/ReportService').default;
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
|
||||
class StatusScreenComponent extends BaseScreenComponent {
|
||||
|
@@ -6,7 +6,7 @@ const Tag = require('@joplin/lib/models/Tag').default;
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { BaseScreenComponent } = require('../base-screen');
|
||||
|
||||
class TagsScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
|
@@ -1,17 +1,13 @@
|
||||
const gulp = require('gulp');
|
||||
const utils = require('@joplin/tools/gulp/utils');
|
||||
import { buildInjectedJS, watchInjectedJS } from './tools/buildInjectedJs';
|
||||
|
||||
import gulpTasks from './tools/buildInjectedJs/gulpTasks';
|
||||
|
||||
const tasks = {
|
||||
encodeAssets: {
|
||||
fn: require('./tools/encodeAssets'),
|
||||
},
|
||||
buildInjectedJs: {
|
||||
fn: buildInjectedJS,
|
||||
},
|
||||
watchInjectedJs: {
|
||||
fn: watchInjectedJS,
|
||||
},
|
||||
...gulpTasks,
|
||||
podInstall: {
|
||||
fn: require('./tools/podInstall'),
|
||||
},
|
||||
@@ -19,6 +15,22 @@ const tasks = {
|
||||
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
|
||||
gulp.task('buildInjectedJs', gulp.series(
|
||||
'beforeBundle',
|
||||
'buildCodeMirrorEditor',
|
||||
'buildJsDrawEditor',
|
||||
'copyWebviewLib',
|
||||
));
|
||||
|
||||
gulp.task('watchInjectedJs', gulp.series(
|
||||
'beforeBundle',
|
||||
'copyWebviewLib',
|
||||
gulp.parallel(
|
||||
'watchCodeMirrorEditor',
|
||||
'watchJsDrawEditor',
|
||||
),
|
||||
));
|
||||
|
||||
gulp.task('build', gulp.series(
|
||||
'buildInjectedJs',
|
||||
'encodeAssets',
|
||||
|
@@ -381,10 +381,16 @@
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-frameworks.sh",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -517,13 +523,13 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 101;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.13.4;
|
||||
MARKETING_VERSION = 12.13.9;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -546,12 +552,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 101;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.13.4;
|
||||
MARKETING_VERSION = 12.13.9;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -698,14 +704,14 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 101;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.13.4;
|
||||
MARKETING_VERSION = 12.13.9;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
@@ -729,14 +735,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 101;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.13.4;
|
||||
MARKETING_VERSION = 12.13.9;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@@ -1,5 +1,6 @@
|
||||
PODS:
|
||||
- boost (1.76.0)
|
||||
- CocoaAsyncSocket (7.6.5)
|
||||
- DoubleConversion (1.1.6)
|
||||
- FBLazyVector (0.71.10)
|
||||
- FBReactNativeSpec (0.71.10):
|
||||
@@ -9,6 +10,67 @@ PODS:
|
||||
- React-Core (= 0.71.10)
|
||||
- React-jsi (= 0.71.10)
|
||||
- ReactCommon/turbomodule/core (= 0.71.10)
|
||||
- Flipper (0.125.0):
|
||||
- Flipper-Folly (~> 2.6)
|
||||
- Flipper-RSocket (~> 1.4)
|
||||
- Flipper-Boost-iOSX (1.76.0.1.11)
|
||||
- Flipper-DoubleConversion (3.2.0.1)
|
||||
- Flipper-Fmt (7.1.7)
|
||||
- Flipper-Folly (2.6.10):
|
||||
- Flipper-Boost-iOSX
|
||||
- Flipper-DoubleConversion
|
||||
- Flipper-Fmt (= 7.1.7)
|
||||
- Flipper-Glog
|
||||
- libevent (~> 2.1.12)
|
||||
- OpenSSL-Universal (= 1.1.1100)
|
||||
- Flipper-Glog (0.5.0.5)
|
||||
- Flipper-PeerTalk (0.0.4)
|
||||
- Flipper-RSocket (1.4.3):
|
||||
- Flipper-Folly (~> 2.6)
|
||||
- FlipperKit (0.125.0):
|
||||
- FlipperKit/Core (= 0.125.0)
|
||||
- FlipperKit/Core (0.125.0):
|
||||
- Flipper (~> 0.125.0)
|
||||
- FlipperKit/CppBridge
|
||||
- FlipperKit/FBCxxFollyDynamicConvert
|
||||
- FlipperKit/FBDefines
|
||||
- FlipperKit/FKPortForwarding
|
||||
- SocketRocket (~> 0.6.0)
|
||||
- FlipperKit/CppBridge (0.125.0):
|
||||
- Flipper (~> 0.125.0)
|
||||
- FlipperKit/FBCxxFollyDynamicConvert (0.125.0):
|
||||
- Flipper-Folly (~> 2.6)
|
||||
- FlipperKit/FBDefines (0.125.0)
|
||||
- FlipperKit/FKPortForwarding (0.125.0):
|
||||
- CocoaAsyncSocket (~> 7.6)
|
||||
- Flipper-PeerTalk (~> 0.0.4)
|
||||
- FlipperKit/FlipperKitHighlightOverlay (0.125.0)
|
||||
- FlipperKit/FlipperKitLayoutHelpers (0.125.0):
|
||||
- FlipperKit/Core
|
||||
- FlipperKit/FlipperKitHighlightOverlay
|
||||
- FlipperKit/FlipperKitLayoutTextSearchable
|
||||
- FlipperKit/FlipperKitLayoutIOSDescriptors (0.125.0):
|
||||
- FlipperKit/Core
|
||||
- FlipperKit/FlipperKitHighlightOverlay
|
||||
- FlipperKit/FlipperKitLayoutHelpers
|
||||
- YogaKit (~> 1.18)
|
||||
- FlipperKit/FlipperKitLayoutPlugin (0.125.0):
|
||||
- FlipperKit/Core
|
||||
- FlipperKit/FlipperKitHighlightOverlay
|
||||
- FlipperKit/FlipperKitLayoutHelpers
|
||||
- FlipperKit/FlipperKitLayoutIOSDescriptors
|
||||
- FlipperKit/FlipperKitLayoutTextSearchable
|
||||
- YogaKit (~> 1.18)
|
||||
- FlipperKit/FlipperKitLayoutTextSearchable (0.125.0)
|
||||
- FlipperKit/FlipperKitNetworkPlugin (0.125.0):
|
||||
- FlipperKit/Core
|
||||
- FlipperKit/FlipperKitReactPlugin (0.125.0):
|
||||
- FlipperKit/Core
|
||||
- FlipperKit/FlipperKitUserDefaultsPlugin (0.125.0):
|
||||
- FlipperKit/Core
|
||||
- FlipperKit/SKIOSNetworkPlugin (0.125.0):
|
||||
- FlipperKit/Core
|
||||
- FlipperKit/FlipperKitNetworkPlugin
|
||||
- fmt (6.2.1)
|
||||
- glog (0.3.5)
|
||||
- hermes-engine (0.71.10):
|
||||
@@ -19,6 +81,7 @@ PODS:
|
||||
- JoplinCommonShareExtension
|
||||
- React
|
||||
- libevent (2.1.12)
|
||||
- OpenSSL-Universal (1.1.1100)
|
||||
- RCT-Folly (2021.07.22.00):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@@ -296,9 +359,9 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-rsa-native (2.0.5):
|
||||
- React
|
||||
- react-native-saf-x (2.13.0):
|
||||
- react-native-saf-x (2.13.3):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.7.2):
|
||||
- react-native-safe-area-context (4.7.4):
|
||||
- React-Core
|
||||
- react-native-slider (4.4.3):
|
||||
- React-Core
|
||||
@@ -398,7 +461,7 @@ PODS:
|
||||
- React-Core
|
||||
- RNCPushNotificationIOS (1.11.0):
|
||||
- React-Core
|
||||
- RNDateTimePicker (7.6.0):
|
||||
- RNDateTimePicker (7.6.1):
|
||||
- React-Core
|
||||
- RNDeviceInfo (10.11.0):
|
||||
- React-Core
|
||||
@@ -416,7 +479,8 @@ PODS:
|
||||
- React
|
||||
- RNShare (9.4.1):
|
||||
- React-Core
|
||||
- RNVectorIcons (10.0.0):
|
||||
- RNVectorIcons (10.0.1):
|
||||
- RCT-Folly (= 2021.07.22.00)
|
||||
- React-Core
|
||||
- RNZipArchive (6.1.0):
|
||||
- React-Core
|
||||
@@ -425,19 +489,44 @@ PODS:
|
||||
- RNZipArchive/Core (6.1.0):
|
||||
- React-Core
|
||||
- SSZipArchive (~> 2.2)
|
||||
- SocketRocket (0.6.0)
|
||||
- SSZipArchive (2.4.3)
|
||||
- Yoga (1.14.0)
|
||||
- YogaKit (1.18.1):
|
||||
- Yoga (~> 1.14)
|
||||
|
||||
DEPENDENCIES:
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
|
||||
- Flipper (= 0.125.0)
|
||||
- Flipper-Boost-iOSX (= 1.76.0.1.11)
|
||||
- Flipper-DoubleConversion (= 3.2.0.1)
|
||||
- Flipper-Fmt (= 7.1.7)
|
||||
- Flipper-Folly (= 2.6.10)
|
||||
- Flipper-Glog (= 0.5.0.5)
|
||||
- Flipper-PeerTalk (= 0.0.4)
|
||||
- Flipper-RSocket (= 1.4.3)
|
||||
- FlipperKit (= 0.125.0)
|
||||
- FlipperKit/Core (= 0.125.0)
|
||||
- FlipperKit/CppBridge (= 0.125.0)
|
||||
- FlipperKit/FBCxxFollyDynamicConvert (= 0.125.0)
|
||||
- FlipperKit/FBDefines (= 0.125.0)
|
||||
- FlipperKit/FKPortForwarding (= 0.125.0)
|
||||
- FlipperKit/FlipperKitHighlightOverlay (= 0.125.0)
|
||||
- FlipperKit/FlipperKitLayoutPlugin (= 0.125.0)
|
||||
- FlipperKit/FlipperKitLayoutTextSearchable (= 0.125.0)
|
||||
- FlipperKit/FlipperKitNetworkPlugin (= 0.125.0)
|
||||
- FlipperKit/FlipperKitReactPlugin (= 0.125.0)
|
||||
- FlipperKit/FlipperKitUserDefaultsPlugin (= 0.125.0)
|
||||
- FlipperKit/SKIOSNetworkPlugin (= 0.125.0)
|
||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
||||
- JoplinCommonShareExtension (from `ShareExtension`)
|
||||
- JoplinRNShareExtension (from `ShareExtension`)
|
||||
- libevent (~> 2.1.12)
|
||||
- OpenSSL-Universal (= 1.1.1100)
|
||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
|
||||
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
|
||||
@@ -445,6 +534,7 @@ DEPENDENCIES:
|
||||
- React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
|
||||
- React-Codegen (from `build/generated/ios`)
|
||||
- React-Core (from `../node_modules/react-native/`)
|
||||
- React-Core/DevSupport (from `../node_modules/react-native/`)
|
||||
- React-Core/RCTWebSocket (from `../node_modules/react-native/`)
|
||||
- React-CoreModules (from `../node_modules/react-native/React/CoreModules`)
|
||||
- React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`)
|
||||
@@ -500,9 +590,22 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- CocoaAsyncSocket
|
||||
- Flipper
|
||||
- Flipper-Boost-iOSX
|
||||
- Flipper-DoubleConversion
|
||||
- Flipper-Fmt
|
||||
- Flipper-Folly
|
||||
- Flipper-Glog
|
||||
- Flipper-PeerTalk
|
||||
- Flipper-RSocket
|
||||
- FlipperKit
|
||||
- fmt
|
||||
- libevent
|
||||
- OpenSSL-Universal
|
||||
- SocketRocket
|
||||
- SSZipArchive
|
||||
- YogaKit
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
boost:
|
||||
@@ -640,15 +743,26 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
boost: 57d2868c099736d80fcd648bf211b4431e51a558
|
||||
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
|
||||
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
||||
FBLazyVector: ddb55c55295ea51ed98aa7e2e08add2f826309d5
|
||||
FBReactNativeSpec: 90fc1a90b4b7a171e0a7c20ea426c1bf6ce4399c
|
||||
Flipper: 26fc4b7382499f1281eb8cb921e5c3ad6de91fe0
|
||||
Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c
|
||||
Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30
|
||||
Flipper-Fmt: 60cbdd92fc254826e61d669a5d87ef7015396a9b
|
||||
Flipper-Folly: 584845625005ff068a6ebf41f857f468decd26b3
|
||||
Flipper-Glog: 70c50ce58ddaf67dc35180db05f191692570f446
|
||||
Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9
|
||||
Flipper-RSocket: d9d9ade67cbecf6ac10730304bf5607266dd2541
|
||||
FlipperKit: cbdee19bdd4e7f05472a66ce290f1b729ba3cb86
|
||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
|
||||
hermes-engine: d27603b55a48402501ad1928c05411dae9cd6b85
|
||||
JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7
|
||||
JoplinRNShareExtension: 485f3e6dad83b7b77f1572eabc249f869ee55c02
|
||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
|
||||
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
||||
RCTRequired: 8ef706f91e2b643cd32c26a57700b5f24fab0585
|
||||
RCTTypeSafety: 5fbddd8eb9242b91ac0d901c01da3673f358b1b7
|
||||
@@ -673,8 +787,8 @@ SPEC CHECKSUMS:
|
||||
react-native-image-resizer: 681f7607418b97c084ba2d0999b153b103040d8a
|
||||
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
|
||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||
react-native-saf-x: b758d1b38a96a96b8179a16e96ed966a507ca5c2
|
||||
react-native-safe-area-context: 7aa8e6d9d0f3100a820efb1a98af68aa747f9284
|
||||
react-native-saf-x: 0f7531c9f8bdbb62bbd55ceb7433de7bb756cd73
|
||||
react-native-safe-area-context: 2cd91d532de12acdb0a9cbc8d43ac72a8e4c897c
|
||||
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
|
||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
||||
@@ -695,7 +809,7 @@ SPEC CHECKSUMS:
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
|
||||
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
|
||||
RNDateTimePicker: ccd988deb223cbb2e669e157ec576c2c6217128c
|
||||
RNDateTimePicker: 8fb39263b721223e095248acaf6f406d5b7f6713
|
||||
RNDeviceInfo: bf8a32acbcb875f568217285d1793b0e8588c974
|
||||
RNExitApp: 00036cabe7bacbb413d276d5520bf74ba39afa6a
|
||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||
@@ -704,10 +818,12 @@ SPEC CHECKSUMS:
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
||||
RNShare: 32e97adc8d8c97d4a26bcdd3c45516882184f8b6
|
||||
RNVectorIcons: 8b5bb0fa61d54cd2020af4f24a51841ce365c7e9
|
||||
RNVectorIcons: ace237de89f1574ef3c963ae9d5da3bd6fbeb02a
|
||||
RNZipArchive: ef9451b849c45a29509bf44e65b788829ab07801
|
||||
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
Yoga: e7ea9e590e27460d28911403b894722354d73479
|
||||
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
|
||||
|
||||
PODFILE CHECKSUM: 3b2cace838120977b5b54871752c9dddf5a11cea
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user