You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-14 23:26:58 +02:00
Compare commits
25 Commits
server-v2.
...
jcgurango-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0f33b7349 | ||
|
|
1c5bb8f195 | ||
|
|
844f868aaa | ||
|
|
4ec8666368 | ||
|
|
ec5ead7b6f | ||
|
|
2f7f306399 | ||
|
|
4d6bbfa901 | ||
|
|
8e9d1dab2a | ||
|
|
fd733f9079 | ||
|
|
79cacc4d5a | ||
|
|
2c91af21e2 | ||
|
|
4c0eff38ff | ||
|
|
e49d9ebbf0 | ||
|
|
f73eee0d69 | ||
|
|
d5fc645fe6 | ||
|
|
0eded263b3 | ||
|
|
a0c923fab3 | ||
|
|
1e352ec0b1 | ||
|
|
9bf7864142 | ||
|
|
9e587c1b06 | ||
|
|
66755c5386 | ||
|
|
042a8e5d30 | ||
|
|
faade53cf9 | ||
|
|
8f526474ea | ||
|
|
3547211078 |
@@ -358,8 +358,6 @@ packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
|
||||
packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
|
||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
||||
packages/app-desktop/tools/notarizeMacApp.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
packages/app-desktop/utils/markupLanguageUtils.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
@@ -369,7 +367,6 @@ packages/app-mobile/components/CustomButton.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
@@ -410,6 +407,7 @@ packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/ScreenHeader.js
|
||||
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/biometrics/BiometricPopup.js
|
||||
@@ -640,8 +638,6 @@ packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js
|
||||
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.test.js
|
||||
packages/lib/services/interop/Module.js
|
||||
packages/lib/services/interop/Module.test.js
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||
packages/lib/services/keychain/KeychainService.js
|
||||
@@ -880,7 +876,6 @@ packages/tools/spellcheck.js
|
||||
packages/tools/tagServerLatest.js
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/update-readme-download.js
|
||||
packages/tools/update-readme-download.test.js
|
||||
packages/tools/update-readme-sponsors.js
|
||||
packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.js
|
||||
|
||||
11
.eslintrc.js
11
.eslintrc.js
@@ -42,8 +42,6 @@ module.exports = {
|
||||
'zxcvbn': 'readonly',
|
||||
|
||||
'tinymce': 'readonly',
|
||||
|
||||
'JSX': 'readonly',
|
||||
},
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 2018,
|
||||
@@ -110,9 +108,6 @@ module.exports = {
|
||||
'semi': ['error', 'always'],
|
||||
'eol-last': ['error', 'always'],
|
||||
'quotes': ['error', 'single'],
|
||||
|
||||
// Note that "indent" only applies to JavaScript files. See
|
||||
// https://github.com/laurent22/joplin/issues/8360
|
||||
'indent': ['error', 'tab'],
|
||||
'comma-dangle': ['error', {
|
||||
'arrays': 'always-multiline',
|
||||
@@ -189,12 +184,6 @@ module.exports = {
|
||||
'project': './tsconfig.eslint.json',
|
||||
},
|
||||
'rules': {
|
||||
'@typescript-eslint/indent': ['error', 'tab', {
|
||||
'ignoredNodes': [
|
||||
// See https://github.com/typescript-eslint/typescript-eslint/issues/1824
|
||||
'TSUnionType',
|
||||
],
|
||||
}],
|
||||
'@typescript-eslint/ban-ts-comment': ['error'],
|
||||
'@typescript-eslint/ban-types': 'error',
|
||||
'@typescript-eslint/explicit-member-accessibility': ['error'],
|
||||
|
||||
10
.github/scripts/run_ci.sh
vendored
10
.github/scripts/run_ci.sh
vendored
@@ -194,12 +194,7 @@ if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
# It can be removed once we upgrade to electron-builder@23, however we
|
||||
# cannot currently do this due to this error:
|
||||
# https://github.com/laurent22/joplin/issues/8149
|
||||
#
|
||||
# electron-builder@24, however, still expects the python binary to be named
|
||||
# "python" and seems to no longer respect the PYTHON_PATH environment variable.
|
||||
# We work around this by aliasing python.
|
||||
alias python=$(which python3)
|
||||
USE_HARD_LINKS=false yarn run dist
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn run dist
|
||||
else
|
||||
USE_HARD_LINKS=false yarn run dist
|
||||
fi
|
||||
@@ -212,8 +207,7 @@ else
|
||||
|
||||
if [ "$IS_MACOS" == "1" ]; then
|
||||
# See above why we need to specify Python
|
||||
alias python=$(which python3)
|
||||
USE_HARD_LINKS=false yarn run dist --publish=never
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn run dist --publish=never
|
||||
else
|
||||
USE_HARD_LINKS=false yarn run dist --publish=never
|
||||
fi
|
||||
|
||||
72
.github/workflows/build-macos-m1.yml
vendored
72
.github/workflows/build-macos-m1.yml
vendored
@@ -1,72 +0,0 @@
|
||||
name: Build macOS M1
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
pre_job:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
|
||||
Main:
|
||||
needs: pre_job
|
||||
# We always process server or desktop release tags, because they also publish the release
|
||||
if: github.repository == 'laurent22/joplin' && needs.pre_job.outputs.should_skip != 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: olegtarasov/get-tag@v2.1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||
# https://github.com/facebook/react-native/issues/36440
|
||||
node-version: '18.15.0'
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
# https://yarnpkg.com/getting-started/install
|
||||
corepack enable
|
||||
|
||||
- name: Build macOS M1 app
|
||||
env:
|
||||
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CSC_KEY_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
run: |
|
||||
export npm_config_arch=arm64
|
||||
export npm_config_target_arch=arm64
|
||||
yarn install
|
||||
cd packages/app-desktop
|
||||
npm pkg set 'build.mac.artifactName'='${productName}-${version}-${arch}.${ext}'
|
||||
npm pkg set 'build.mac.target.target'='default'
|
||||
npm pkg set 'build.mac.target.arch[0]'='arm64'
|
||||
|
||||
if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
echo "Building and publishing desktop application..."
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn run dist --mac --arm64
|
||||
else
|
||||
echo "Building but *not* publishing desktop application..."
|
||||
|
||||
# We also want to disable signing the app in this case, because
|
||||
# it doesn't work and we don't need it.
|
||||
# https://www.electron.build/code-signing#how-to-disable-code-signing-during-the-build-process-on-macos
|
||||
|
||||
export CSC_IDENTITY_AUTO_DISCOVERY=false
|
||||
npm pkg set 'build.mac.identity'=null --json
|
||||
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn run dist --publish=never
|
||||
fi
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -343,8 +343,6 @@ packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
|
||||
packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
|
||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
||||
packages/app-desktop/tools/notarizeMacApp.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
packages/app-desktop/utils/markupLanguageUtils.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
@@ -354,7 +352,6 @@ packages/app-mobile/components/CustomButton.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
@@ -395,6 +392,7 @@ packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/ScreenHeader.js
|
||||
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/biometrics/BiometricPopup.js
|
||||
@@ -625,8 +623,6 @@ packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js
|
||||
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.test.js
|
||||
packages/lib/services/interop/Module.js
|
||||
packages/lib/services/interop/Module.test.js
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
|
||||
packages/lib/services/keychain/KeychainService.js
|
||||
@@ -865,7 +861,6 @@ packages/tools/spellcheck.js
|
||||
packages/tools/tagServerLatest.js
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/update-readme-download.js
|
||||
packages/tools/update-readme-download.test.js
|
||||
packages/tools/update-readme-sponsors.js
|
||||
packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.js
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/templates/nsis/include/allowOnlyOneInstallerInstance.nsh b/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
|
||||
index a1fd1875d852ff69c087a3103eff827c20d37ca5..5222614ddad3276876857e7a9dde4017a6b9fc85 100644
|
||||
--- a/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
|
||||
+++ b/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
|
||||
@@ -42,7 +42,7 @@
|
||||
${nsProcess::FindProcess} "${_FILE}" ${_ERR}
|
||||
!else
|
||||
# find process owned by current user
|
||||
- nsExec::Exec `cmd /c tasklist /FI "USERNAME eq %USERNAME%" /FI "IMAGENAME eq ${_FILE}" | %SYSTEMROOT%\System32\find.exe "${_FILE}"`
|
||||
+ nsExec::Exec `cmd /c tasklist /FI "USERNAME eq %USERNAME%" /FI "PID ne $pid" /FI "IMAGENAME eq ${_FILE}" | %SYSTEMROOT%\System32\find.exe "${_FILE}"`
|
||||
Pop ${_ERR}
|
||||
!endif
|
||||
!macroend
|
||||
@@ -11,17 +11,11 @@
|
||||
--variationPrimary-VaraintDark: #072445;
|
||||
--variationPrimary-VaraintWhite: #f8f9fa;
|
||||
--variationPrimary-VaraintDarkGray: #212529;
|
||||
--variationAccent-VaraintLightBlue: #F7FBFF;
|
||||
--variationAccent-VaraintLightBlue: #d9e8f8;
|
||||
}
|
||||
* {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.7em;
|
||||
}
|
||||
|
||||
#mobile_icon_logo{
|
||||
display: none;
|
||||
@@ -107,15 +101,14 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
h1.main-logo {
|
||||
header .container nav h1.main-logo {
|
||||
|
||||
font-weight: 600;
|
||||
font-size: 48px;
|
||||
color: var(--primary-DarkBlue);
|
||||
color: var(--primary-White);
|
||||
text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
line-height: 1.3em;
|
||||
padding-bottom: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
@media screen and (max-width: 992px) {
|
||||
header .container nav img {
|
||||
@@ -214,7 +207,7 @@
|
||||
font-weight: 700;
|
||||
font-size: 40px;
|
||||
color: var(--primary-DarkBlue);
|
||||
/* margin-bottom: 45px; */
|
||||
margin-bottom: 45px;
|
||||
}
|
||||
@media screen and (max-width: 767px) {
|
||||
.joplin__logos h2.main-logo {
|
||||
@@ -230,20 +223,16 @@
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.joplin__logos > .wrapper {
|
||||
padding: 40px 0 45px 0;
|
||||
padding: 42px 59px 48px 66px;
|
||||
margin: 40px 45px;
|
||||
/* width: calc(100% - 6.5vw); */
|
||||
width: calc(100% - 6.5vw);
|
||||
border: 1px solid var(--variationPrimary-VaraintDarkGray);
|
||||
border-radius: 5px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
@media screen and (max-width: 1440px) {
|
||||
.joplin__logos > .wrapper {
|
||||
padding: 40px 0 45px 0;
|
||||
margin: 40px auto;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.joplin__logos .wrapper .para-1 {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,34 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 08 May 2023 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 08 May 2023 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 2.11]]></title><description><![CDATA[<h2>Desktop<a name="desktop" href="#desktop" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Add support for plugin user data<a name="add-support-for-plugin-user-data" href="#add-support-for-plugin-user-data" class="heading-anchor">🔗</a></h3>
|
||||
<p>Developers of plugins now have the ability to associate additional information with notes, notebooks, and tags. This data is then synchronized across multiple devices. For instance, it is possible to envision attaching OCR data to an image or incorporating plugin-specific parameters to a note, which can subsequently be synchronized across all devices.</p>
|
||||
<h3>Improved end-to-end encryption<a name="improved-end-to-end-encryption" href="#improved-end-to-end-encryption" class="heading-anchor">🔗</a></h3>
|
||||
<p>End-to-end encryption (E2EE) plays a vital role in safeguarding your data privacy. Our aim is to make it effortlessly user-friendly, seamlessly integrated, and possibly enabled by default.</p>
|
||||
<p>To achieve this objective, we have implemented several enhancements in this version. These improvements encompass more streamlined management of encryption keys and simplified setup processes for both new and existing devices utilising end-to-end encryption.</p>
|
||||
<p>Moreover, we have upgraded the encryption algorithm from AES-128 to AES-256, ensuring heightened security for your data in the long run.</p>
|
||||
<h3>Export a Mermaid graph<a name="export-a-mermaid-graph" href="#export-a-mermaid-graph" class="heading-anchor">🔗</a></h3>
|
||||
<p>There is now a context menu on Mermaid graphs which you can use to export your graph as PNG, SVG, or by copying the vector data to the clipboard.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230711-mermaid-export.png" alt=""></p>
|
||||
<h3>Auto-detect language on startup<a name="auto-detect-language-on-startup" href="#auto-detect-language-on-startup" class="heading-anchor">🔗</a></h3>
|
||||
<p>A small but useful change - the application language is now automatically detected startup. Of course, if needed you can later change it in the configuration screen.</p>
|
||||
<h2>Android<a name="android" href="#android" class="heading-anchor">🔗</a></h2>
|
||||
<p>As always most of these changes also apply to the mobile applications, such as the improved end-to-end encryption support, language auto-detection, and various bug fixes and improvements. But besides those, we also have several mobile-specific updates:</p>
|
||||
<h3>Support for offline voice typing<a name="support-for-offline-voice-typing" href="#support-for-offline-voice-typing" class="heading-anchor">🔗</a></h3>
|
||||
<p>The Android mobile app now support offline voice typing - it means your voice recording does not need to be uploaded to Google or Samsung server - it all takes place on your device for added security. To make use of it, open any note, tap on the three dots menu, then "Voice typing".</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230711-voice-typing-1.png" alt=""></p>
|
||||
<p>The first time, the app will need to download the language files to your device:</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230711-voice-typing-2.png" alt=""></p>
|
||||
<p>Once they are there, simply record your message and it will appear in the note:</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230711-voice-typing-3.png" alt=""></p>
|
||||
<h3>Create and move sub-notebooks<a name="create-and-move-sub-notebooks" href="#create-and-move-sub-notebooks" class="heading-anchor">🔗</a></h3>
|
||||
<p>@jcgurango implemented support for creating sub-notebooks, and moving notebooks around in the mobile app. To do so, long press on any notebook and press "Edit". You can now choose to move it under any other notebook using the provided dropdown list.</p>
|
||||
<h2>Full changelog<a name="full-changelog" href="#full-changelog" class="heading-anchor">🔗</a></h2>
|
||||
<p>This is just an overview of the main features. The full changelog is available there:</p>
|
||||
<ul>
|
||||
<li>Desktop: <a href="https://joplinapp.org/changelog">https://joplinapp.org/changelog</a></li>
|
||||
<li>Android: <a href="https://joplinapp.org/changelog_android">https://joplinapp.org/changelog_android</a></li>
|
||||
<li>iOS: <a href="https://joplinapp.org/changelog_ios">https://joplinapp.org/changelog_ios</a></li>
|
||||
</ul>
|
||||
]]></description><link>https://joplinapp.org/news/20230508-release-2-11/</link><guid isPermaLink="false">20230508-release-2-11</guid><pubDate>Mon, 08 May 2023 00:00:00 GMT</pubDate><twitter-text>What's new in Joplin 2.11</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.10]]></title><description><![CDATA[<p>Great news! Joplin 2.10 is here and we've made some amazing improvements and bug fixes, with a focus on the mobile app.</p>
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 08 May 2023 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 08 May 2023 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 2.10]]></title><description><![CDATA[<p>Great news! Joplin 2.10 is here and we've made some amazing improvements and bug fixes, with a focus on the mobile app.</p>
|
||||
<h1>New design for "New note" and "New to-do" buttons<a name="new-design-for-new-note-and-new-to-do-buttons" href="#new-design-for-new-note-and-new-to-do-buttons" class="heading-anchor">🔗</a></h1>
|
||||
<p>We're excited to announce that we've made it even easier to create new notes and to-do lists with new designs for the "New note" and "New to-do" buttons.</p>
|
||||
<p>If there is enough space, the button labels will be shown in full:</p>
|
||||
@@ -334,4 +304,13 @@
|
||||
<p>As always the mobile apps (to be released soon) benefit from several of the above changes since they share the same codebase as the desktop app.</p>
|
||||
<p>Specific to the mobile version 2.5 are some improvements to the beta editor - in particular the layout has been cleaned up, and the first word of sentences is now automatically capitalised, which makes typing notes easier. If you haven't tried the beta editor yet, you can enable it from the Configuration screen.</p>
|
||||
<p>The full changelog is available there: <a href="https://joplinapp.org/changelog/">https://joplinapp.org/changelog/</a></p>
|
||||
]]></description><link>https://joplinapp.org/news/20211031-115215/</link><guid isPermaLink="false">20211031-115215</guid><pubDate>Sun, 31 Oct 2021 11:52:15 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20211031-115215/</link><guid isPermaLink="false">20211031-115215</guid><pubDate>Sun, 31 Oct 2021 11:52:15 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA["Certificate has expired" error with Joplin Cloud, and workaround]]></title><description><![CDATA[<p>Some of you might be experiencing an error "Certificate has expired" when synchronising with Joplin Cloud (and possibly other services) when using the desktop application.</p>
|
||||
<p>This is due to Let's Encrypt root certificate that expired on 30 September, and the new method they are using is not compatible with the Joplin desktop application.</p>
|
||||
<p>This actually affects thousands of applications, not just Joplin, so various solutions are being considered right now and hopefully a fix will be available relatively soon.</p>
|
||||
<p>For now, as a workaround, you can simply check "<strong>Ignore TLS certificate errors</strong>" in <strong>Configuration > Synchronisation > Advanced Options</strong></p>
|
||||
<p>I will let you know as soon as a fix is available so that you can clear that option.</p>
|
||||
<p>More info:</p>
|
||||
<p>- <a href="https://community.letsencrypt.org/t/issues-with-electron-and-expired-root/160991">Issue with Electron and expired root</a> on Let's Encrypt</p>
|
||||
<p>- <a href="https://github.com/electron/electron/issues/31212">Let's Encrypt root CA isn't working properly</a> on Electron GitHub repository</p>
|
||||
<p><strong>Update:</strong> I have implemented a temporary fix on Joplin Cloud which should solve the issue for now. If you're still having some issues please let me know. An updated desktop app will be available later on with a more permanent fix.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20210930-163458/</link><guid isPermaLink="false">20210930-163458</guid><pubDate>Thu, 30 Sep 2021 16:34:58 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
|
||||
@@ -1,7 +1,6 @@
|
||||
<!-- Logos Section -->
|
||||
<section class="joplin__logos">
|
||||
<div class="container">
|
||||
<h1 class="main-logo">Joplin brand guidelines</h1>
|
||||
<h2 class="main-logo">Logos</h2>
|
||||
<p class="joplin__para">
|
||||
All the variations of Joplin logo are all front facing. <br />
|
||||
@@ -15,10 +14,10 @@
|
||||
<div class="row align-items-center justify-content-tablet-end gy-5 p-row">
|
||||
<div class="col-xxl-4 col-xl-4 col-lg-6 col-md-6 col-sm-12">
|
||||
<div class="box">
|
||||
<h3 class="joplin__tagline1">Lockup logo</h3>
|
||||
<h3 class="joplin__tagline1">Lockup logo</h2>
|
||||
<p class="joplin__para">
|
||||
Joplin lockup is the main logo. It is used for
|
||||
products and marketing communications. To ensure
|
||||
Joplin lockup is the company's main logo. It is used for
|
||||
corporate materials and marketing communications. To ensure
|
||||
sufficient contrast and readability, two following versions of
|
||||
the lockup are provided.
|
||||
</p>
|
||||
@@ -57,7 +56,7 @@
|
||||
<div class="row align-items-center gy-5 p-row justify-between">
|
||||
<div class="col-xxl-6 col-xl-6 col-lg-12 col-md-12 col-sm-12">
|
||||
<div class="box">
|
||||
<h3 class="joplin__tagline1">Brandmark logo</h3>
|
||||
<h3 class="joplin__tagline1">Brandmark logo</h2>
|
||||
<div class="box-wrapper">
|
||||
<img
|
||||
src="{{imageBaseUrl}}/brand/J logo transparent background 1.png"
|
||||
@@ -67,7 +66,7 @@
|
||||
The classic brandmark icon is often used on a bright
|
||||
background to help increase the contrast. It is a reduced
|
||||
version of a logo which should be used in limited spacing or
|
||||
when using a full name is not necessary.
|
||||
when using a full company name is not necessary.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,6 +121,7 @@
|
||||
<div class="info">
|
||||
<p class="joplin__para">Dark blue</p>
|
||||
<p class="joplin__para">HEX - 043873</p>
|
||||
<p class="joplin__para">RGB - 4, 56, 115</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="circle-wrapper">
|
||||
@@ -129,6 +129,7 @@
|
||||
<div class="info">
|
||||
<p class="joplin__para">White</p>
|
||||
<p class="joplin__para">HEX - FFFFFF</p>
|
||||
<p class="joplin__para">RGB - 255, 255,</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="circle-wrapper">
|
||||
@@ -136,6 +137,7 @@
|
||||
<div class="info">
|
||||
<p class="joplin__para">Dark gray</p>
|
||||
<p class="joplin__para">HEX - 32363F</p>
|
||||
<p class="joplin__para">RGB - 50, 54, 63</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,6 +151,7 @@
|
||||
<div class="info">
|
||||
<p class="joplin__para">Light blue</p>
|
||||
<p class="joplin__para">HEX - 4F9CF9</p>
|
||||
<p class="joplin__para">RGB - 79, 156, 249</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="circle-wrapper">
|
||||
@@ -156,6 +159,7 @@
|
||||
<div class="info">
|
||||
<p class="joplin__para">Yellow</p>
|
||||
<p class="joplin__para">HEX - FFE492</p>
|
||||
<p class="joplin__para">RGB - 255, 228, 146</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,6 +180,7 @@
|
||||
<div class="info">
|
||||
<p class="joplin__para">Variant dark</p>
|
||||
<p class="joplin__para">HEX - 072445</p>
|
||||
<p class="joplin__para">RGB - 7, 36, 69</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="circle-wrapper">
|
||||
@@ -183,6 +188,7 @@
|
||||
<div class="info">
|
||||
<p class="joplin__para">Variant white</p>
|
||||
<p class="joplin__para">HEX - F8F9FA</p>
|
||||
<p class="joplin__para">RGB - 248, 249,</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="circle-wrapper">
|
||||
@@ -190,6 +196,7 @@
|
||||
<div class="info">
|
||||
<p class="joplin__para">Variant dark gray</p>
|
||||
<p class="joplin__para">HEX - 212529</p>
|
||||
<p class="joplin__para">RGB - 33, 37, 41</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,7 +212,8 @@
|
||||
<div class="circle variant-light-blue"></div>
|
||||
<div class="info">
|
||||
<p class="joplin__para">Variant light blue</p>
|
||||
<p class="joplin__para">HEX - F7FBFF</p>
|
||||
<p class="joplin__para">HEX - D9E8F8</p>
|
||||
<p class="joplin__para">RGB - 217, 232,</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,10 +250,10 @@
|
||||
<div class="box">
|
||||
<h2>Imagery</h2>
|
||||
<p class="joplin__para">
|
||||
We uses vector art illustration . It ensures high
|
||||
The company uses vector art illustration . It ensures high
|
||||
quality images in any scale, large or small. All the
|
||||
illustrations come from storyset.com and are personalised to
|
||||
match the Joplin's colours.
|
||||
match the company's colours.
|
||||
</p>
|
||||
</div>
|
||||
<div class="box">
|
||||
@@ -297,7 +305,7 @@
|
||||
<b>genuine</b> in engaging with our users and Joplin community.
|
||||
</p>
|
||||
<p class="joplin__para">
|
||||
As an open-source project we stay
|
||||
As a company providing open-source product we stay
|
||||
<b>open-minded</b> and <b>enthusiastic</b> about new ideas and
|
||||
solutions. At the same time we continue to be
|
||||
<b>professional</b> to honour the trust the users place in us.
|
||||
@@ -319,14 +327,13 @@ const ctx = document.getElementById("chart1");
|
||||
const myChart = new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels: ["Dark blue 40%", "Gray 20%", "White 20%", "Light blue 10%", "Yellow 7%", "Dark gray 3%"],
|
||||
labels: ["Dark blue 60%", "White 20%", "light blue 10%", "yellow 7%", "dark gray 3%"],
|
||||
datasets: [
|
||||
{
|
||||
label: "# of Votes",
|
||||
data: [40, 20, 20, 10, 7, 3],
|
||||
data: [60, 20, 10, 7, 3],
|
||||
backgroundColor: [
|
||||
"#043873",
|
||||
"#f7fbff",
|
||||
"#ffffff",
|
||||
"#4f9cf9",
|
||||
"#ffe492",
|
||||
@@ -337,11 +344,6 @@ const myChart = new Chart(ctx, {
|
||||
},
|
||||
options: {
|
||||
layout: {},
|
||||
elements: {
|
||||
arc: {
|
||||
borderColor: '#cccccc',
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
@@ -358,14 +360,13 @@ const ctx2 = document.getElementById("chart2");
|
||||
const myChart2 = new Chart(ctx2, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels: ["White 40%", "Gray 20%", "Dark blue 20%", "Light blue 10%", "Dark gray 7%", "Yellow 3%"],
|
||||
labels: ["White 60%", "Dark 20%", "light blue 10%", "dark gray 7%", "yellow 3%"],
|
||||
datasets: [
|
||||
{
|
||||
label: "# of Votes",
|
||||
data: [40, 20, 20, 10, 7, 3],
|
||||
data: [60, 20, 10, 7, 3],
|
||||
backgroundColor: [
|
||||
"#ffffff",
|
||||
"#f7fbff",
|
||||
"#043873",
|
||||
"#4f9cf9",
|
||||
"#212529",
|
||||
@@ -376,11 +377,6 @@ const myChart2 = new Chart(ctx2, {
|
||||
},
|
||||
options: {
|
||||
layout: {},
|
||||
elements: {
|
||||
arc: {
|
||||
borderColor: '#cccccc',
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
|
||||
@@ -38,7 +38,7 @@ The install and update script supports the [following flags](https://github.com/
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
---|---|---
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the [APK file](https://objects.joplinusercontent.com/v2.11.31/joplin-v2.11.31.apk?source=JoplinWebsite&type=New)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://objects.joplinusercontent.com/android-v2.9.8/joplin-v2.9.8.apk?source=JoplinWebsite&type=New) [32-bit](https://objects.joplinusercontent.com/android-v2.9.8/joplin-v2.9.8-32bit.apk?source=JoplinWebsite&type=New)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"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@8.39.0": "patch:eslint@npm%3A8.39.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"eslint@^8.13.0": "patch:eslint@npm%3A8.39.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"
|
||||
"eslint@^8.13.0": "patch:eslint@npm%3A8.39.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,41 +252,4 @@ describe('MdToHtml', () => {
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should attach source blocks to block KaTeX', async () => {
|
||||
const mdToHtml = newTestMdToHtml();
|
||||
|
||||
const katex = [
|
||||
'3 + 3',
|
||||
'\n\\int_0^1 x dx\n\n',
|
||||
'\n\\int_0^1 x dx\n3 + 3\n',
|
||||
'\n\t2^{3^4}\n\t3 + 3\n',
|
||||
'3\n4',
|
||||
];
|
||||
const surroundingTextChoices = [
|
||||
['', ''],
|
||||
['Test', ''],
|
||||
['Test', 'Test!'],
|
||||
['Test\n\n', '\n\nTest!'],
|
||||
];
|
||||
|
||||
const tests = [];
|
||||
for (const texSource of katex) {
|
||||
for (const [start, end] of surroundingTextChoices) {
|
||||
tests.push([texSource, `${start}\n$$${texSource}$$\n${end}`]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [tex, input] of tests) {
|
||||
const html = await mdToHtml.render(input, null, { bodyOnly: true });
|
||||
|
||||
const opening = '<pre class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$$ " data-joplin-source-close=" $$ ">';
|
||||
const closing = '</pre>';
|
||||
|
||||
// Remove any single leading and trailing newlines, those are included in data-joplin-source-open
|
||||
// and data-joplin-source-close.
|
||||
const trimmedTex = tex.replace(/^[\n]/, '').replace(/[\n]$/, '');
|
||||
expect(html.html).toContain(opening + trimmedTex + closing);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export4.dtd">
|
||||
<en-export export-date="20230809T080314Z" application="Evernote" version="10.58.8">
|
||||
<note>
|
||||
<title>Here is a simple test</title>
|
||||
<created>20230709T080219Z</created>
|
||||
<updated>20230709T080302Z</updated>
|
||||
<note-attributes>
|
||||
</note-attributes>
|
||||
<content>
|
||||
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div>a</div><div><br/></div><div>b</div><div><br/></div><div>List 1</div><div style="--en-task-group:true; --en-id:9876cc26-ebd0-482d-bb36-603e2c0512d0;--en-content-hash:7e7703c4ce2d1805937a6024e5f7b426;-webkit-user-modify:read-only;-moz-user-modify:read-only;user-modify:read-only;border-radius:3px;border:1px solid rgba(182,182,182,0.09);background:rgba(174,174,174,0.09);overflow:hidden;color:#868686"><div style="background:rgba(182,182,182,0.09) no-repeat 6px 6px url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzg2ODY4NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5LjcyNTEgMjBDMjAuMTgxIDIwIDIwLjYwMTEgMTkuNzU2IDIwLjgyOSAxOS4zNkMyMS4wNTcgMTguOTY0IDIxLjA1NyAxOC40NzYgMjAuODI5IDE4LjA4TDEzLjEwMzEgNC42Mzk5OUMxMi44NzYgNC4yNDM5OSAxMi40NTUxIDQgMTIgNEMxMS41NDQ5IDQgMTEuMTI0IDQuMjQzOTkgMTAuODk2MSA0LjYzOTk5TDMuMTcwOTggMTguMDhDMi45NDMwMSAxOC40NzYgMi45NDMwMSAxOC45NjQgMy4xNzA5OCAxOS4zNkMzLjM5ODEgMTkuNzU2IDMuODE4OTMgMjAgNC4yNzQwOCAyMEgxOS43MjUxWk00Ljg2NDg5IDE4LjRMMTIgNS45ODc5OEwxOS4xMzUxIDE4LjRINC44NjQ4OVoiLz4KPHBhdGggZD0iTTEyLjc1IDEwLjU3NTJDMTIuNzUgMTAuMTYxIDEyLjQxNDIgOS44MjUyMyAxMiA5LjgyNTIzQzExLjU4NTggOS44MjUyMyAxMS4yNSAxMC4xNjEgMTEuMjUgMTAuNTc1MlYxMy4yOTc5QzExLjI1IDEzLjcxMjEgMTEuNTg1OCAxNC4wNDc5IDEyIDE0LjA0NzlDMTIuNDE0MiAxNC4wNDc5IDEyLjc1IDEzLjcxMjEgMTIuNzUgMTMuMjk3OVYxMC41NzUyWiIvPgo8cGF0aCBkPSJNMTIuODc1IDE1Ljk1MTFDMTIuODc1IDE2LjQzNDQgMTIuNDgzMiAxNi44MjYxIDEyIDE2LjgyNjFDMTEuNTE2OCAxNi44MjYxIDExLjEyNSAxNi40MzQ0IDExLjEyNSAxNS45NTExQzExLjEyNSAxNS40Njc5IDExLjUxNjggMTUuMDc2MSAxMiAxNS4wNzYxQzEyLjQ4MzIgMTUuMDc2MSAxMi44NzUgMTUuNDY3OSAxMi44NzUgMTUuOTUxMVoiLz4KPC9zdmc+Cg==");padding:8px 38px;font-weight:600">Content not supported</div><div style="padding:2px 6px;margin: 1em">This block is a placeholder for Tasks, which has been officially released on the newest version of Evernote and is no longer supported on this version. Deleting or moving this block may cause unexpected behavior in newer versions of Evernote.</div></div><div>List 2</div><div style="--en-task-group:true; --en-id:b89e9766-5afc-47b8-9c54-e42962d69421;--en-content-hash:85f6897ebdd0d3d12ba8fcc3ce6b8e56;-webkit-user-modify:read-only;-moz-user-modify:read-only;user-modify:read-only;border-radius:3px;border:1px solid rgba(182,182,182,0.09);background:rgba(174,174,174,0.09);overflow:hidden;color:#868686"><div style="background:rgba(182,182,182,0.09) no-repeat 6px 6px url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzg2ODY4NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5LjcyNTEgMjBDMjAuMTgxIDIwIDIwLjYwMTEgMTkuNzU2IDIwLjgyOSAxOS4zNkMyMS4wNTcgMTguOTY0IDIxLjA1NyAxOC40NzYgMjAuODI5IDE4LjA4TDEzLjEwMzEgNC42Mzk5OUMxMi44NzYgNC4yNDM5OSAxMi40NTUxIDQgMTIgNEMxMS41NDQ5IDQgMTEuMTI0IDQuMjQzOTkgMTAuODk2MSA0LjYzOTk5TDMuMTcwOTggMTguMDhDMi45NDMwMSAxOC40NzYgMi45NDMwMSAxOC45NjQgMy4xNzA5OCAxOS4zNkMzLjM5ODEgMTkuNzU2IDMuODE4OTMgMjAgNC4yNzQwOCAyMEgxOS43MjUxWk00Ljg2NDg5IDE4LjRMMTIgNS45ODc5OEwxOS4xMzUxIDE4LjRINC44NjQ4OVoiLz4KPHBhdGggZD0iTTEyLjc1IDEwLjU3NTJDMTIuNzUgMTAuMTYxIDEyLjQxNDIgOS44MjUyMyAxMiA5LjgyNTIzQzExLjU4NTggOS44MjUyMyAxMS4yNSAxMC4xNjEgMTEuMjUgMTAuNTc1MlYxMy4yOTc5QzExLjI1IDEzLjcxMjEgMTEuNTg1OCAxNC4wNDc5IDEyIDE0LjA0NzlDMTIuNDE0MiAxNC4wNDc5IDEyLjc1IDEzLjcxMjEgMTIuNzUgMTMuMjk3OVYxMC41NzUyWiIvPgo8cGF0aCBkPSJNMTIuODc1IDE1Ljk1MTFDMTIuODc1IDE2LjQzNDQgMTIuNDgzMiAxNi44MjYxIDEyIDE2LjgyNjFDMTEuNTE2OCAxNi44MjYxIDExLjEyNSAxNi40MzQ0IDExLjEyNSAxNS45NTExQzExLjEyNSAxNS40Njc5IDExLjUxNjggMTUuMDc2MSAxMiAxNS4wNzYxQzEyLjQ4MzIgMTUuMDc2MSAxMi44NzUgMTUuNDY3OSAxMi44NzUgMTUuOTUxMVoiLz4KPC9zdmc+Cg==");padding:8px 38px;font-weight:600">Content not supported</div><div style="padding:2px 6px;margin: 1em">This block is a placeholder for Tasks, which has been officially released on the newest version of Evernote and is no longer supported on this version. Deleting or moving this block may cause unexpected behavior in newer versions of Evernote.</div></div></en-note> ]]>
|
||||
</content>
|
||||
<task>
|
||||
<title>Clara</title>
|
||||
<created>20230709T080237Z</created>
|
||||
<updated>20230709T080300Z</updated>
|
||||
<taskStatus>completed</taskStatus>
|
||||
<taskFlag>false</taskFlag>
|
||||
<sortWeight>J</sortWeight>
|
||||
<noteLevelID>7ab2b90f-4aed-405f-8541-cb56132a2e1a</noteLevelID>
|
||||
<taskGroupNoteLevelID>9876cc26-ebd0-482d-bb36-603e2c0512d0</taskGroupNoteLevelID>
|
||||
<dueDate>2757600913T000000Z</dueDate>
|
||||
<statusUpdated>20230709T080237Z</statusUpdated>
|
||||
<creator>73671305</creator>
|
||||
<lastEditor>73671305</lastEditor>
|
||||
</task>
|
||||
<task>
|
||||
<title>Bob</title>
|
||||
<created>20230709T080237Z</created>
|
||||
<updated>20230709T080250Z</updated>
|
||||
<taskStatus>open</taskStatus>
|
||||
<taskFlag>false</taskFlag>
|
||||
<sortWeight>B</sortWeight>
|
||||
<noteLevelID>98185f99-1779-4af3-988b-4af054a2c8c0</noteLevelID>
|
||||
<taskGroupNoteLevelID>9876cc26-ebd0-482d-bb36-603e2c0512d0</taskGroupNoteLevelID>
|
||||
<dueDate>2757600913T000000Z</dueDate>
|
||||
<statusUpdated>20230709T080237Z</statusUpdated>
|
||||
<creator>73671305</creator>
|
||||
<lastEditor>73671305</lastEditor>
|
||||
</task>
|
||||
<task>
|
||||
<title>Jeff</title>
|
||||
<created>20230709T080300Z</created>
|
||||
<updated>20230709T080302Z</updated>
|
||||
<taskStatus>open</taskStatus>
|
||||
<taskFlag>false</taskFlag>
|
||||
<sortWeight>B</sortWeight>
|
||||
<noteLevelID>8458b06b-0007-4c3d-b61b-87abe15f55b8</noteLevelID>
|
||||
<taskGroupNoteLevelID>b89e9766-5afc-47b8-9c54-e42962d69421</taskGroupNoteLevelID>
|
||||
<dueDate>2757600913T000000Z</dueDate>
|
||||
<statusUpdated>20230709T080300Z</statusUpdated>
|
||||
<creator>73671305</creator>
|
||||
<lastEditor>73671305</lastEditor>
|
||||
</task>
|
||||
</note>
|
||||
</en-export>
|
||||
@@ -1,12 +0,0 @@
|
||||
a
|
||||
|
||||
b
|
||||
|
||||
List 1
|
||||
|
||||
- [x] Clara
|
||||
- [ ] Bob
|
||||
|
||||
List 2
|
||||
|
||||
- [ ] Jeff
|
||||
@@ -1,17 +0,0 @@
|
||||
<en-note>
|
||||
<div>a</div>
|
||||
<div><br/></div>
|
||||
<div>b</div>
|
||||
<div><br/></div>
|
||||
<div>List 1</div>
|
||||
<div style="--en-task-group:true; --en-id:9876cc26-ebd0-482d-bb36-603e2c0512d0;--en-content-hash:7e7703c4ce2d1805937a6024e5f7b426;-webkit-user-modify:read-only;-moz-user-modify:read-only;user-modify:read-only;border-radius:3px;border:1px solid rgba(182,182,182,0.09);background:rgba(174,174,174,0.09);overflow:hidden;color:#868686">
|
||||
<div style="background:rgba(182,182,182,0.09) no-repeat 6px 6px url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzg2ODY4NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5LjcyNTEgMjBDMjAuMTgxIDIwIDIwLjYwMTEgMTkuNzU2IDIwLjgyOSAxOS4zNkMyMS4wNTcgMTguOTY0IDIxLjA1NyAxOC40NzYgMjAuODI5IDE4LjA4TDEzLjEwMzEgNC42Mzk5OUMxMi44NzYgNC4yNDM5OSAxMi40NTUxIDQgMTIgNEMxMS41NDQ5IDQgMTEuMTI0IDQuMjQzOTkgMTAuODk2MSA0LjYzOTk5TDMuMTcwOTggMTguMDhDMi45NDMwMSAxOC40NzYgMi45NDMwMSAxOC45NjQgMy4xNzA5OCAxOS4zNkMzLjM5ODEgMTkuNzU2IDMuODE4OTMgMjAgNC4yNzQwOCAyMEgxOS43MjUxWk00Ljg2NDg5IDE4LjRMMTIgNS45ODc5OEwxOS4xMzUxIDE4LjRINC44NjQ4OVoiLz4KPHBhdGggZD0iTTEyLjc1IDEwLjU3NTJDMTIuNzUgMTAuMTYxIDEyLjQxNDIgOS44MjUyMyAxMiA5LjgyNTIzQzExLjU4NTggOS44MjUyMyAxMS4yNSAxMC4xNjEgMTEuMjUgMTAuNTc1MlYxMy4yOTc5QzExLjI1IDEzLjcxMjEgMTEuNTg1OCAxNC4wNDc5IDEyIDE0LjA0NzlDMTIuNDE0MiAxNC4wNDc5IDEyLjc1IDEzLjcxMjEgMTIuNzUgMTMuMjk3OVYxMC41NzUyWiIvPgo8cGF0aCBkPSJNMTIuODc1IDE1Ljk1MTFDMTIuODc1IDE2LjQzNDQgMTIuNDgzMiAxNi44MjYxIDEyIDE2LjgyNjFDMTEuNTE2OCAxNi44MjYxIDExLjEyNSAxNi40MzQ0IDExLjEyNSAxNS45NTExQzExLjEyNSAxNS40Njc5IDExLjUxNjggMTUuMDc2MSAxMiAxNS4wNzYxQzEyLjQ4MzIgMTUuMDc2MSAxMi44NzUgMTUuNDY3OSAxMi44NzUgMTUuOTUxMVoiLz4KPC9zdmc+Cg==");padding:8px 38px;font-weight:600">Content not supported</div>
|
||||
<div style="padding:2px 6px;margin: 1em">This block is a placeholder for Tasks, which has been officially released on the newest version of Evernote and is no longer supported on this version. Deleting or moving this block may cause unexpected behavior in newer versions of Evernote.</div>
|
||||
</div>
|
||||
<div>List 2</div>
|
||||
<div style="--en-task-group:true; --en-id:b89e9766-5afc-47b8-9c54-e42962d69421;--en-content-hash:85f6897ebdd0d3d12ba8fcc3ce6b8e56;-webkit-user-modify:read-only;-moz-user-modify:read-only;user-modify:read-only;border-radius:3px;border:1px solid rgba(182,182,182,0.09);background:rgba(174,174,174,0.09);overflow:hidden;color:#868686">
|
||||
<div style="background:rgba(182,182,182,0.09) no-repeat 6px 6px url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzg2ODY4NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5LjcyNTEgMjBDMjAuMTgxIDIwIDIwLjYwMTEgMTkuNzU2IDIwLjgyOSAxOS4zNkMyMS4wNTcgMTguOTY0IDIxLjA1NyAxOC40NzYgMjAuODI5IDE4LjA4TDEzLjEwMzEgNC42Mzk5OUMxMi44NzYgNC4yNDM5OSAxMi40NTUxIDQgMTIgNEMxMS41NDQ5IDQgMTEuMTI0IDQuMjQzOTkgMTAuODk2MSA0LjYzOTk5TDMuMTcwOTggMTguMDhDMi45NDMwMSAxOC40NzYgMi45NDMwMSAxOC45NjQgMy4xNzA5OCAxOS4zNkMzLjM5ODEgMTkuNzU2IDMuODE4OTMgMjAgNC4yNzQwOCAyMEgxOS43MjUxWk00Ljg2NDg5IDE4LjRMMTIgNS45ODc5OEwxOS4xMzUxIDE4LjRINC44NjQ4OVoiLz4KPHBhdGggZD0iTTEyLjc1IDEwLjU3NTJDMTIuNzUgMTAuMTYxIDEyLjQxNDIgOS44MjUyMyAxMiA5LjgyNTIzQzExLjU4NTggOS44MjUyMyAxMS4yNSAxMC4xNjEgMTEuMjUgMTAuNTc1MlYxMy4yOTc5QzExLjI1IDEzLjcxMjEgMTEuNTg1OCAxNC4wNDc5IDEyIDE0LjA0NzlDMTIuNDE0MiAxNC4wNDc5IDEyLjc1IDEzLjcxMjEgMTIuNzUgMTMuMjk3OVYxMC41NzUyWiIvPgo8cGF0aCBkPSJNMTIuODc1IDE1Ljk1MTFDMTIuODc1IDE2LjQzNDQgMTIuNDgzMiAxNi44MjYxIDEyIDE2LjgyNjFDMTEuNTE2OCAxNi44MjYxIDExLjEyNSAxNi40MzQ0IDExLjEyNSAxNS45NTExQzExLjEyNSAxNS40Njc5IDExLjUxNjggMTUuMDc2MSAxMiAxNS4wNzYxQzEyLjQ4MzIgMTUuMDc2MSAxMi44NzUgMTUuNDY3OSAxMi44NzUgMTUuOTUxMVoiLz4KPC9zdmc+Cg==");padding:8px 38px;font-weight:600">Content not supported</div>
|
||||
<div style="padding:2px 6px;margin: 1em">This block is a placeholder for Tasks, which has been officially released on the newest version of Evernote and is no longer supported on this version. Deleting or moving this block may cause unexpected behavior in newer versions of Evernote.</div>
|
||||
</div>
|
||||
</en-note>
|
||||
|
||||
@@ -100,10 +100,7 @@ export default class ElectronAppWrapper {
|
||||
webviewTag: true,
|
||||
// We start with a hidden window, which is then made visible depending on the showTrayIcon setting
|
||||
// https://github.com/laurent22/joplin/issues/2031
|
||||
//
|
||||
// On Linux/GNOME, however, the window doesn't show correctly if show is false initially:
|
||||
// https://github.com/laurent22/joplin/issues/8256
|
||||
show: debugEarlyBugs || shim.isGNOME(),
|
||||
show: debugEarlyBugs,
|
||||
};
|
||||
|
||||
// Linux icon workaround for bug https://github.com/electron-userland/electron-builder/issues/2098
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { ExportOptions, FileSystemItem } from '@joplin/lib/services/interop/types';
|
||||
import { ExportModule } from '@joplin/lib/services/interop/Module';
|
||||
import { ExportOptions, FileSystemItem, Module } from '@joplin/lib/services/interop/types';
|
||||
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
@@ -153,7 +152,7 @@ export default class InteropServiceHelper {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
public static async export(_dispatch: Function, module: ExportModule, options: ExportNoteOptions = null) {
|
||||
public static async export(_dispatch: Function, module: Module, options: ExportNoteOptions = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
let path = null;
|
||||
|
||||
@@ -494,7 +494,7 @@ class Application extends BaseApplication {
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) {
|
||||
bridge().window().hide();
|
||||
// Keep it hidden
|
||||
} else {
|
||||
bridge().window().show();
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import Logger from '@joplin/lib/Logger';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from './services/bridge';
|
||||
import KvStore from '@joplin/lib/services/KvStore';
|
||||
const { fileExtension } = require('@joplin/lib/path-utils');
|
||||
import * as ArrayUtils from '@joplin/lib/ArrayUtils';
|
||||
import { CheckForUpdateOptions, extractVersionInfo, GitHubRelease } from './utils/checkForUpdatesUtils';
|
||||
const packageInfo = require('./packageInfo.js');
|
||||
const compareVersions = require('compare-versions');
|
||||
|
||||
@@ -13,6 +13,10 @@ const logger = Logger.create('checkForUpdates');
|
||||
let checkInBackground_ = false;
|
||||
let isCheckingForUpdate_ = false;
|
||||
|
||||
interface CheckForUpdateOptions {
|
||||
includePreReleases?: boolean;
|
||||
}
|
||||
|
||||
function onCheckStarted() {
|
||||
logger.info('Starting...');
|
||||
isCheckingForUpdate_ = true;
|
||||
@@ -23,7 +27,15 @@ function onCheckEnded() {
|
||||
isCheckingForUpdate_ = false;
|
||||
}
|
||||
|
||||
async function fetchLatestRelease() {
|
||||
function getMajorMinorTagName(tagName: string) {
|
||||
const s = tagName.split('.');
|
||||
s.pop();
|
||||
return s.join('.');
|
||||
}
|
||||
|
||||
async function fetchLatestRelease(options: CheckForUpdateOptions) {
|
||||
options = { includePreReleases: false, ...options };
|
||||
|
||||
const response = await shim.fetch('https://api.github.com/repos/laurent22/joplin/releases');
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -31,7 +43,89 @@ async function fetchLatestRelease() {
|
||||
throw new Error(`Cannot get latest release info: ${responseText.substr(0, 500)}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as GitHubRelease[];
|
||||
const releases = await response.json();
|
||||
if (!releases.length) throw new Error('Cannot get latest release info (JSON)');
|
||||
|
||||
let release = null;
|
||||
|
||||
if (options.includePreReleases) {
|
||||
release = releases[0];
|
||||
} else {
|
||||
for (const r of releases) {
|
||||
if (!r.prerelease) {
|
||||
release = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!release) throw new Error('Could not get tag name');
|
||||
|
||||
const version = release.tag_name.substr(1);
|
||||
|
||||
// We concatenate all the release notes of the major/minor versions
|
||||
// corresponding to the latest version. For example, if the latest version
|
||||
// is 1.8.3, we concatenate all the 1.8.x versions. This is so that no
|
||||
// matter from which version you upgrade, you always see the full changes,
|
||||
// with the latest changes being on top.
|
||||
|
||||
const fullReleaseNotes = [];
|
||||
const majorMinorTagName = getMajorMinorTagName(release.tag_name);
|
||||
|
||||
for (const release of releases) {
|
||||
if (getMajorMinorTagName(release.tag_name) === majorMinorTagName) {
|
||||
fullReleaseNotes.push(release.body.trim());
|
||||
}
|
||||
}
|
||||
|
||||
let downloadUrl = null;
|
||||
const platform = process.platform;
|
||||
for (let i = 0; i < release.assets.length; i++) {
|
||||
const asset = release.assets[i];
|
||||
let found = false;
|
||||
const ext = fileExtension(asset.name);
|
||||
if (platform === 'win32' && ext === 'exe') {
|
||||
if (shim.isPortable()) {
|
||||
found = asset.name === 'JoplinPortable.exe';
|
||||
} else {
|
||||
found = !!asset.name.match(/^Joplin-Setup-[\d.]+\.exe$/);
|
||||
}
|
||||
} else if (platform === 'darwin' && ext === 'dmg') {
|
||||
found = true;
|
||||
} else if (platform === 'linux' && ext === '.AppImage') {
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
downloadUrl = asset.browser_download_url.replace('github.com/laurent22/joplin/releases/download', 'objects.joplinusercontent.com');
|
||||
downloadUrl.concat('?source=DesktopApp&type=Update');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanUpReleaseNotes(releaseNotes: string[]) {
|
||||
const lines = releaseNotes.join('\n\n* * *\n\n').split('\n');
|
||||
const output = [];
|
||||
for (const line of lines) {
|
||||
const r = line
|
||||
.replace(/\(#.* by .*\)/g, '') // Removes issue numbers and names - (#3157 by [@user](https://github.com/user))
|
||||
.replace(/\([0-9a-z]{7}\)/g, '') // Removes commits - "sync state or data (a6caa35)"
|
||||
.replace(/\(#[0-9]+\)/g, '') // Removes issue numbers - "(#4727)"
|
||||
.replace(/ {2}/g, ' ')
|
||||
.trim();
|
||||
|
||||
output.push(r);
|
||||
}
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
return {
|
||||
version: version,
|
||||
downloadUrl: downloadUrl,
|
||||
notes: cleanUpReleaseNotes(fullReleaseNotes),
|
||||
pageUrl: release.html_url,
|
||||
prerelease: release.prerelease,
|
||||
};
|
||||
}
|
||||
|
||||
function truncateText(text: string, length: number) {
|
||||
@@ -76,8 +170,7 @@ export default async function checkForUpdates(inBackground: boolean, parentWindo
|
||||
logger.info(`Checking with options ${JSON.stringify(options)}`);
|
||||
|
||||
try {
|
||||
const releases = await fetchLatestRelease();
|
||||
const release = extractVersionInfo(releases, process.platform, options);
|
||||
const release = await fetchLatestRelease(options);
|
||||
|
||||
logger.info(`Current version: ${packageInfo.version}`);
|
||||
logger.info(`Latest version: ${release.version}`);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import versionInfo from '@joplin/lib/versionInfo';
|
||||
import { ImportModule } from '@joplin/lib/services/interop/Module';
|
||||
import { Module } from '@joplin/lib/services/interop/types';
|
||||
import InteropServiceHelper from '../InteropServiceHelper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { isContextMenuItemLocation, MenuItem, MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
@@ -230,7 +230,7 @@ function useMenu(props: Props) {
|
||||
void CommandService.instance().execute(commandName);
|
||||
}, []);
|
||||
|
||||
const onImportModuleClick = useCallback(async (module: ImportModule, moduleSource: string) => {
|
||||
const onImportModuleClick = useCallback(async (module: Module, moduleSource: string) => {
|
||||
let path = null;
|
||||
|
||||
if (moduleSource === 'file') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.12.5",
|
||||
"version": "2.12.0",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -78,6 +78,7 @@
|
||||
},
|
||||
"mac": {
|
||||
"icon": "../../Assets/macOs.icns",
|
||||
"target": "dmg",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "./build-mac/entitlements.mac.inherit.plist",
|
||||
"extendInfo": {
|
||||
@@ -107,7 +108,6 @@
|
||||
},
|
||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "3.2.13",
|
||||
"@joplin/tools": "~2.12",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.1",
|
||||
@@ -115,8 +115,9 @@
|
||||
"@types/react": "16.14.41",
|
||||
"@types/react-redux": "7.1.25",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"electron": "25.2.0",
|
||||
"electron-builder": "24.4.0",
|
||||
"electron": "19.1.4",
|
||||
"electron-builder": "22.11.7",
|
||||
"electron-rebuild": "3.2.9",
|
||||
"glob": "10.2.7",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.5.0",
|
||||
|
||||
@@ -2,10 +2,8 @@ import { useEffect } from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
export default function(frameWindow: any, onSubmit: Function, onDismiss: Function, loadedHtmlHash: string) {
|
||||
const document = frameWindow && frameWindow.document ? frameWindow.document : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!document) return () => {};
|
||||
if (!frameWindow) return () => {};
|
||||
|
||||
function onFormSubmit(event: any) {
|
||||
event.preventDefault();
|
||||
@@ -22,18 +20,18 @@ export default function(frameWindow: any, onSubmit: Function, onDismiss: Functio
|
||||
// Disable enter key from submitting when a text area is in focus!
|
||||
// https://github.com/laurent22/joplin/issues/4766
|
||||
//
|
||||
if (document.activeElement.tagName !== 'TEXTAREA') {
|
||||
if (frameWindow.document.activeElement.tagName !== 'TEXTAREA') {
|
||||
if (onSubmit) onSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('submit', onFormSubmit);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
frameWindow.document.addEventListener('submit', onFormSubmit);
|
||||
frameWindow.document.addEventListener('keydown', onKeyDown);
|
||||
|
||||
return () => {
|
||||
if (document) document.removeEventListener('submit', onFormSubmit);
|
||||
if (document) document.removeEventListener('keydown', onKeyDown);
|
||||
if (frameWindow) frameWindow.document.removeEventListener('submit', onFormSubmit);
|
||||
if (frameWindow) frameWindow.document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [document, loadedHtmlHash, onSubmit, onDismiss]);
|
||||
}, [frameWindow, loadedHtmlHash, onSubmit, onDismiss]);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ async function main() {
|
||||
// wrong one. However it means it will have to be manually upgraded for each
|
||||
// new Electron release. Some ABI map there:
|
||||
// https://github.com/electron/node-abi/tree/master/test
|
||||
const forceAbiArgs = '--force-abi 116';
|
||||
const forceAbiArgs = '--force-abi 89';
|
||||
|
||||
if (isWindows()) {
|
||||
// Cannot run this in parallel, or the 64-bit version might end up
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,119 +0,0 @@
|
||||
import { fileExtension } from '@joplin/lib/path-utils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
export interface CheckForUpdateOptions {
|
||||
includePreReleases?: boolean;
|
||||
}
|
||||
|
||||
export interface GitHubRelease {
|
||||
tag_name: string;
|
||||
prerelease: boolean;
|
||||
body: string;
|
||||
assets: {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
}[];
|
||||
html_url: string;
|
||||
}
|
||||
|
||||
interface Release {
|
||||
version: string;
|
||||
prerelease: boolean;
|
||||
downloadUrl: string;
|
||||
notes: string;
|
||||
pageUrl: string;
|
||||
}
|
||||
|
||||
function getMajorMinorTagName(tagName: string) {
|
||||
const s = tagName.split('.');
|
||||
s.pop();
|
||||
return s.join('.');
|
||||
}
|
||||
|
||||
export const extractVersionInfo = (releases: GitHubRelease[], platform: typeof process.platform, options: CheckForUpdateOptions) => {
|
||||
options = { includePreReleases: false, ...options };
|
||||
|
||||
if (!releases.length) throw new Error('Cannot get latest release info (JSON)');
|
||||
|
||||
let release = null;
|
||||
|
||||
if (options.includePreReleases) {
|
||||
release = releases[0];
|
||||
} else {
|
||||
for (const r of releases) {
|
||||
if (!r.prerelease) {
|
||||
release = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!release) throw new Error('Could not get tag name');
|
||||
|
||||
const version = release.tag_name.substr(1);
|
||||
|
||||
// We concatenate all the release notes of the major/minor versions
|
||||
// corresponding to the latest version. For example, if the latest version
|
||||
// is 1.8.3, we concatenate all the 1.8.x versions. This is so that no
|
||||
// matter from which version you upgrade, you always see the full changes,
|
||||
// with the latest changes being on top.
|
||||
|
||||
const fullReleaseNotes = [];
|
||||
const majorMinorTagName = getMajorMinorTagName(release.tag_name);
|
||||
|
||||
for (const release of releases) {
|
||||
if (getMajorMinorTagName(release.tag_name) === majorMinorTagName) {
|
||||
fullReleaseNotes.push(release.body.trim());
|
||||
}
|
||||
}
|
||||
|
||||
let downloadUrl = null;
|
||||
for (let i = 0; i < release.assets.length; i++) {
|
||||
const asset = release.assets[i];
|
||||
let found = false;
|
||||
const ext = fileExtension(asset.name);
|
||||
if (platform === 'win32' && ext === 'exe') {
|
||||
if (shim.isPortable()) {
|
||||
found = asset.name === 'JoplinPortable.exe';
|
||||
} else {
|
||||
found = !!asset.name.match(/^Joplin-Setup-[\d.]+\.exe$/);
|
||||
}
|
||||
} else if (platform === 'darwin' && ext === 'dmg' && !asset.name.endsWith('arm64.dmg')) { // We don't return the arm64 version for now
|
||||
found = true;
|
||||
} else if (platform === 'linux' && ext === '.AppImage') {
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
downloadUrl = asset.browser_download_url.replace('github.com/laurent22/joplin/releases/download', 'objects.joplinusercontent.com');
|
||||
downloadUrl.concat('?source=DesktopApp&type=Update');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanUpReleaseNotes(releaseNotes: string[]) {
|
||||
const lines = releaseNotes.join('\n\n* * *\n\n').split('\n');
|
||||
const output = [];
|
||||
for (const line of lines) {
|
||||
const r = line
|
||||
.replace(/\(#.* by .*\)/g, '') // Removes issue numbers and names - (#3157 by [@user](https://github.com/user))
|
||||
.replace(/\([0-9a-z]{7}\)/g, '') // Removes commits - "sync state or data (a6caa35)"
|
||||
.replace(/\(#[0-9]+\)/g, '') // Removes issue numbers - "(#4727)"
|
||||
.replace(/ {2}/g, ' ')
|
||||
.trim();
|
||||
|
||||
output.push(r);
|
||||
}
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
const output: Release = {
|
||||
version: version,
|
||||
downloadUrl: downloadUrl,
|
||||
notes: cleanUpReleaseNotes(fullReleaseNotes),
|
||||
pageUrl: release.html_url,
|
||||
prerelease: release.prerelease,
|
||||
};
|
||||
|
||||
return output;
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
plugins: ['react-native-reanimated/plugin'],
|
||||
};
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, ModalProps, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import { hasNotch } from 'react-native-device-info';
|
||||
|
||||
interface ModalElementProps extends ModalProps {
|
||||
children: React.ReactNode;
|
||||
containerStyle?: ViewStyle;
|
||||
elevation?: number;
|
||||
}
|
||||
|
||||
const ModalElement: React.FC<ModalElementProps> = ({
|
||||
children,
|
||||
containerStyle,
|
||||
...modalProps
|
||||
}) => {
|
||||
return (
|
||||
<Modal {...modalProps}>
|
||||
<View style={[styleSheet.modalContainer, containerStyle ? containerStyle : null]}>
|
||||
{children}
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styleSheet = StyleSheet.create({
|
||||
modalContainer: {
|
||||
marginTop: hasNotch() ? 65 : 15,
|
||||
marginBottom: hasNotch() ? 35 : 15,
|
||||
},
|
||||
});
|
||||
|
||||
export default ModalElement;
|
||||
@@ -1,10 +1,8 @@
|
||||
const React = require('react');
|
||||
const { Text, View, StyleSheet, Button } = require('react-native');
|
||||
const { Text, Modal, View, StyleSheet, Button } = require('react-native');
|
||||
const { themeStyle } = require('./global-style.js');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
|
||||
import Modal from './Modal';
|
||||
|
||||
class ModalDialog extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -32,7 +30,6 @@ class ModalDialog extends React.Component {
|
||||
margin: 20,
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
elevation: 10,
|
||||
},
|
||||
modalContentWrapper2: {
|
||||
flex: 1,
|
||||
@@ -59,15 +56,17 @@ class ModalDialog extends React.Component {
|
||||
|
||||
return (
|
||||
<View style={this.styles().modalWrapper}>
|
||||
<Modal transparent={true} visible={true} onRequestClose={() => {}} containerStyle={this.styles().modalContentWrapper}>
|
||||
<Text style={this.styles().title}>{this.props.title}</Text>
|
||||
<View style={this.styles().modalContentWrapper2}>{ContentComponent}</View>
|
||||
<View style={this.styles().buttonRow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button disabled={!buttonBarEnabled} title={_('OK')} onPress={this.props.onOkPress}></Button>
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 5 }}>
|
||||
<Button disabled={!buttonBarEnabled} title={_('Cancel')} onPress={this.props.onCancelPress}></Button>
|
||||
<Modal transparent={true} visible={true} onRequestClose={() => {}}>
|
||||
<View elevation={10} style={this.styles().modalContentWrapper}>
|
||||
<Text style={this.styles().title}>{this.props.title}</Text>
|
||||
<View style={this.styles().modalContentWrapper2}>{ContentComponent}</View>
|
||||
<View style={this.styles().buttonRow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button disabled={!buttonBarEnabled} title={_('OK')} onPress={this.props.onOkPress}></Button>
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 5 }}>
|
||||
<Button disabled={!buttonBarEnabled} title={_('Cancel')} onPress={this.props.onCancelPress}></Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
const React = require('react');
|
||||
const { useState, useEffect, useMemo, useRef } = require('react');
|
||||
const { StyleSheet } = require('react-native');
|
||||
const { View, Text, TextInput, Button } = require('react-native');
|
||||
const { View, Modal, Text, TextInput, Button } = require('react-native');
|
||||
|
||||
import Modal from '../Modal';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { EditorControl } from './types';
|
||||
@@ -44,6 +43,7 @@ const EditLinkDialog = (props: LinkDialogProps) => {
|
||||
margin: 15,
|
||||
padding: 30,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
|
||||
elevation: 5,
|
||||
shadowOffset: {
|
||||
width: 1,
|
||||
@@ -132,22 +132,23 @@ const EditLinkDialog = (props: LinkDialogProps) => {
|
||||
return (
|
||||
<Modal
|
||||
animationType="slide"
|
||||
containerStyle={styles.modalContent}
|
||||
transparent={true}
|
||||
visible={props.visible}
|
||||
onRequestClose={() => {
|
||||
props.editorControl.hideLinkDialog();
|
||||
}}>
|
||||
<Text style={styles.header}>{_('Edit link')}</Text>
|
||||
<View>
|
||||
{linkTextInput}
|
||||
{linkURLInput}
|
||||
<View style={styles.modalContent}>
|
||||
<Text style={styles.header}>{_('Edit link')}</Text>
|
||||
<View>
|
||||
{linkTextInput}
|
||||
{linkURLInput}
|
||||
</View>
|
||||
<Button
|
||||
style={styles.button}
|
||||
onPress={onSubmit}
|
||||
title={_('Done')}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
style={styles.button}
|
||||
onPress={onSubmit}
|
||||
title={_('Done')}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,9 +25,9 @@ export const defaultSearchState: SearchState = {
|
||||
};
|
||||
|
||||
export interface SearchPanelProps {
|
||||
searchControl: SearchControl;
|
||||
searchState: SearchState;
|
||||
editorSettings: EditorSettings;
|
||||
searchControl: SearchControl;
|
||||
searchState: SearchState;
|
||||
editorSettings: EditorSettings;
|
||||
}
|
||||
|
||||
interface ActionButtonProps {
|
||||
|
||||
@@ -15,9 +15,9 @@ export interface EditorSettings {
|
||||
// [themeStyle(themeId: number)] doesn't work. As such, we need both
|
||||
// the [themeId] and [themeData].
|
||||
themeId: number;
|
||||
themeData: Theme;
|
||||
themeData: Theme;
|
||||
|
||||
katexEnabled: boolean;
|
||||
katexEnabled: boolean;
|
||||
spellcheckEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -41,14 +41,14 @@ export interface SelectionChangeEvent {
|
||||
}
|
||||
|
||||
export interface SearchControl {
|
||||
findNext(): void;
|
||||
findPrevious(): void;
|
||||
replaceCurrent(): void;
|
||||
replaceAll(): void;
|
||||
findNext(): void;
|
||||
findPrevious(): void;
|
||||
replaceCurrent(): void;
|
||||
replaceAll(): void;
|
||||
setSearchState(state: SearchState): void;
|
||||
|
||||
showSearch(): void;
|
||||
hideSearch(): void;
|
||||
showSearch(): void;
|
||||
hideSearch(): void;
|
||||
}
|
||||
|
||||
export interface SearchState {
|
||||
@@ -57,7 +57,7 @@ export interface SearchState {
|
||||
|
||||
searchText: string;
|
||||
replaceText: string;
|
||||
dialogVisible: boolean;
|
||||
dialogVisible: boolean;
|
||||
}
|
||||
|
||||
// Possible types of lists in the editor
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
const React = require('react');
|
||||
|
||||
import { Component } from 'react';
|
||||
import { Component, FunctionComponent } from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { FlatList, Text, StyleSheet, Button, View } from 'react-native';
|
||||
import { FlatList, Text, StyleSheet, Button, View, ViewStyle, TextStyle, ImageStyle, PanResponder } from 'react-native';
|
||||
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { AppState } from '../utils/types';
|
||||
import DraggableFlatList, { ScaleDecorator } from 'react-native-draggable-flatlist';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { NoteItem } = require('./note-item.js');
|
||||
const { themeStyle } = require('./global-style.js');
|
||||
const { dialogs } = require('../utils/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
|
||||
interface NoteListProps {
|
||||
themeId: string;
|
||||
@@ -19,17 +23,91 @@ interface NoteListProps {
|
||||
folders: FolderEntity[];
|
||||
noteSelectionEnabled?: boolean;
|
||||
selectedFolderId?: string;
|
||||
onSorted: (sortedId: string, newIndex: number)=> void;
|
||||
}
|
||||
|
||||
class NoteListComponent extends Component<NoteListProps> {
|
||||
interface NoteListState {
|
||||
items: NoteEntity[];
|
||||
selectedItemIds: string[];
|
||||
}
|
||||
|
||||
interface NoteItemWrapperProps {
|
||||
note: NoteEntity;
|
||||
dialogbox: any;
|
||||
drag: ()=> void;
|
||||
isActive: boolean;
|
||||
style: ViewStyle | TextStyle | ImageStyle;
|
||||
noteSelectionEnabled?: boolean;
|
||||
dispatch: (payload: any)=> void;
|
||||
}
|
||||
|
||||
const NoteItemWrapper: FunctionComponent<NoteItemWrapperProps> = ({
|
||||
note,
|
||||
drag,
|
||||
isActive,
|
||||
style,
|
||||
dialogbox,
|
||||
noteSelectionEnabled,
|
||||
dispatch,
|
||||
}) => {
|
||||
if (Setting.value('notes.sortOrder.field') !== 'order') {
|
||||
drag = async () => {
|
||||
const doIt = await dialogs.confirmRef(dialogbox, `${_('To manually sort the notes, the sort order must be changed to "%s" in the menu "%s" > "%s"', _('Custom order'), _('View'), _('Sort notes by'))}\n\n${_('Do you want to do this')}`);
|
||||
|
||||
if (doIt) {
|
||||
Setting.setValue('notes.sortOrder.field', 'order');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const panResponder = React.useRef(
|
||||
PanResponder.create({
|
||||
// Ask to be the responder:
|
||||
onStartShouldSetPanResponder: () => false,
|
||||
onStartShouldSetPanResponderCapture: () => false,
|
||||
onMoveShouldSetPanResponder: (_) => true,
|
||||
onMoveShouldSetPanResponderCapture: (_) => true,
|
||||
onPanResponderGrant: () => {
|
||||
void drag();
|
||||
},
|
||||
onShouldBlockNativeResponder: () => false,
|
||||
})
|
||||
).current;
|
||||
|
||||
return (
|
||||
<View style={style} {...panResponder.panHandlers}>
|
||||
<NoteItem
|
||||
note={note}
|
||||
onLongPress={() => {
|
||||
dispatch({
|
||||
type: noteSelectionEnabled ? 'NOTE_SELECTION_TOGGLE' : 'NOTE_SELECTION_START',
|
||||
id: note.id,
|
||||
});
|
||||
}}
|
||||
disabled={isActive}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectedNoteItemWrapper = connect((state: AppState) => {
|
||||
return {
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
};
|
||||
})(NoteItemWrapper);
|
||||
|
||||
class NoteListComponent extends Component<NoteListProps, NoteListState> {
|
||||
private rootRef_: FlatList;
|
||||
private styles_: Record<string, StyleSheet.NamedStyles<any>>;
|
||||
|
||||
/** DialogBox isn't a type, so we can't use it here */
|
||||
private dialogbox: any;
|
||||
|
||||
public constructor(props: NoteListProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
items: [],
|
||||
items: props.items || [],
|
||||
selectedItemIds: [],
|
||||
};
|
||||
this.rootRef_ = null;
|
||||
@@ -45,7 +123,7 @@ class NoteListComponent extends Component<NoteListProps> {
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
const styles = {
|
||||
const styles: Record<string, ViewStyle | TextStyle | ImageStyle> = {
|
||||
noItemMessage: {
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
@@ -55,8 +133,22 @@ class NoteListComponent extends Component<NoteListProps> {
|
||||
color: theme.color,
|
||||
textAlign: 'center',
|
||||
},
|
||||
selectAction: {
|
||||
flex: 1,
|
||||
paddingLeft: theme.marginLeft,
|
||||
// Reverse the color 4 to use for e.x. white text over blue bg
|
||||
backgroundColor: theme.color4,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
actionText: {
|
||||
// Reverse the color 4 to use for e.x. white text over blue bg
|
||||
color: theme.backgroundColor4,
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
noteContainer: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
noNotebookView: {
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
@@ -77,18 +169,51 @@ class NoteListComponent extends Component<NoteListProps> {
|
||||
if (this.rootRef_ && newProps.notesSource !== this.props.notesSource) {
|
||||
this.rootRef_.scrollToOffset({ offset: 0, animated: false });
|
||||
}
|
||||
|
||||
this.setState({
|
||||
items: newProps.items || [],
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
public renderMainContent() {
|
||||
// `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39
|
||||
|
||||
if (this.props.items.length) {
|
||||
return <FlatList
|
||||
ref={ref => (this.rootRef_ = ref)}
|
||||
data={this.props.items}
|
||||
renderItem={({ item }) => <NoteItem note={item} />}
|
||||
keyExtractor={item => item.id}
|
||||
/>;
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<DraggableFlatList
|
||||
ref={(ref: any) => (this.rootRef_ = ref)}
|
||||
data={this.state.items}
|
||||
renderItem={({ item, drag, isActive }) => (
|
||||
<ScaleDecorator>
|
||||
<ConnectedNoteItemWrapper
|
||||
note={item}
|
||||
drag={drag}
|
||||
isActive={isActive}
|
||||
style={this.styles().noteContainer}
|
||||
dialogbox={this.dialogbox}
|
||||
/>
|
||||
</ScaleDecorator>
|
||||
)}
|
||||
keyExtractor={item => item.id}
|
||||
onDragEnd={async ({ data, to, from }) => {
|
||||
if (this.props.selectedFolderId) {
|
||||
this.setState({ items: data });
|
||||
|
||||
if (this.props.onSorted) {
|
||||
let newIndex = to;
|
||||
|
||||
if (to > from) {
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
this.props.onSorted(data[to].id, newIndex);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
if (!this.props.folders.length) {
|
||||
const noItemMessage = _('You currently have no notebooks.');
|
||||
@@ -104,15 +229,27 @@ class NoteListComponent extends Component<NoteListProps> {
|
||||
}
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{this.renderMainContent()}
|
||||
<DialogBox
|
||||
ref={(dialogbox: any) => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const NoteList = connect((state: AppState) => {
|
||||
return {
|
||||
items: state.notes,
|
||||
folders: state.folders,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
notesSource: state.notesSource,
|
||||
themeId: state.settings.theme,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
};
|
||||
})(NoteListComponent);
|
||||
|
||||
|
||||
22
packages/app-mobile/components/SideMenu.ts
Normal file
22
packages/app-mobile/components/SideMenu.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const { connect } = require('react-redux');
|
||||
const SideMenu_ = require('react-native-side-menu-updated').default;
|
||||
import { Dimensions } from 'react-native';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
|
||||
class SideMenuComponent extends SideMenu_ {
|
||||
public onLayoutChange(e: any) {
|
||||
const { width, height } = e.nativeEvent.layout;
|
||||
const openMenuOffsetPercentage = this.props.openMenuOffset / Dimensions.get('window').width;
|
||||
const openMenuOffset = width * openMenuOffsetPercentage;
|
||||
const hiddenMenuOffset = width * this.state.hiddenMenuOffsetPercentage;
|
||||
this.setState({ width, height, openMenuOffset, hiddenMenuOffset });
|
||||
}
|
||||
}
|
||||
|
||||
const SideMenu = connect((state: State) => {
|
||||
return {
|
||||
isOpen: state.showSideMenu,
|
||||
};
|
||||
})(SideMenuComponent);
|
||||
|
||||
export default SideMenu;
|
||||
@@ -17,15 +17,15 @@ import { Dimensions } from 'react-native';
|
||||
|
||||
export interface ValueMap {
|
||||
// Value to use on small-width displays
|
||||
sm?: number;
|
||||
sm?: number;
|
||||
// Value to use on medium-width displays
|
||||
md?: number;
|
||||
md?: number;
|
||||
// Value to use on large-width displays
|
||||
lg?: number;
|
||||
lg?: number;
|
||||
// Value to use on extra-large width displays
|
||||
xl?: number;
|
||||
xl?: number;
|
||||
// Value to use on extra-extra-large width displays
|
||||
xxl?: number;
|
||||
xxl?: number;
|
||||
}
|
||||
|
||||
export default function getResponsiveValue(valueMap: ValueMap): number {
|
||||
|
||||
@@ -98,10 +98,9 @@ class NoteItemComponent extends Component {
|
||||
onLongPress() {
|
||||
if (!this.props.note) return;
|
||||
|
||||
this.props.dispatch({
|
||||
type: this.props.noteSelectionEnabled ? 'NOTE_SELECTION_TOGGLE' : 'NOTE_SELECTION_START',
|
||||
id: this.props.note.id,
|
||||
});
|
||||
if (this.props.onLongPress) {
|
||||
this.props.onLongPress();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -132,7 +131,7 @@ class NoteItemComponent extends Component {
|
||||
const noteTitle = Note.displayTitle(note);
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={() => this.onPress()} onLongPress={() => this.onLongPress()} activeOpacity={0.5}>
|
||||
<TouchableOpacity onPress={() => this.onPress()} onLongPress={() => this.onLongPress()} disabled={this.props.disabled} activeOpacity={0.5}>
|
||||
<View style={selectionWrapperStyle}>
|
||||
<View style={opacityStyle}>
|
||||
<View style={listItemStyle}>
|
||||
|
||||
@@ -69,6 +69,8 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
Setting.setValue(r.name, r.value);
|
||||
};
|
||||
|
||||
this.noteList_onSort = this.noteList_onSort.bind(this);
|
||||
|
||||
this.backHandler = () => {
|
||||
if (this.dialogbox && this.dialogbox.state && this.dialogbox.state.isVisible) {
|
||||
this.dialogbox.close();
|
||||
@@ -113,7 +115,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
}
|
||||
}
|
||||
|
||||
public async refreshNotes(props: any = null) {
|
||||
public async refreshNotes(props: any = null, force?: boolean) {
|
||||
if (props === null) props = this.props;
|
||||
|
||||
const options = {
|
||||
@@ -131,7 +133,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
parentId: parent.id,
|
||||
});
|
||||
|
||||
if (source === props.notesSource) return;
|
||||
if (source === props.notesSource && !force) return;
|
||||
|
||||
let notes = [];
|
||||
if (props.notesParentType === 'Folder') {
|
||||
@@ -149,6 +151,18 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
});
|
||||
}
|
||||
|
||||
private async noteList_onSort(sortedId: string, newIndex: number) {
|
||||
await Note.insertNotesAt(
|
||||
this.props.selectedFolderId,
|
||||
[sortedId],
|
||||
newIndex,
|
||||
this.props.uncompletedTodosOnTop,
|
||||
this.props.showCompletedTodos
|
||||
);
|
||||
|
||||
await this.refreshNotes(null, true);
|
||||
}
|
||||
|
||||
public newNoteNavigate = async (folderId: string, isTodo: boolean) => {
|
||||
const newNote = await Note.save({
|
||||
parent_id: folderId,
|
||||
@@ -254,7 +268,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
<ScreenHeader title={iconString + title} showBackButton={false} parentComponent={thisComp} sortButton_press={this.sortButton_press} folderPickerOptions={this.folderPickerOptions()} showSearchButton={true} showSideMenuButton={true} />
|
||||
<NoteList />
|
||||
<NoteList onSorted={this.noteList_onSort} />
|
||||
{actionButtonComp}
|
||||
<DialogBox
|
||||
ref={(dialogbox: any) => {
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
import 'react-native-get-random-values';
|
||||
import 'react-native-url-polyfill/auto';
|
||||
|
||||
// Set up required for react-native-drawer-layout (See: https://reactnavigation.org/docs/drawer-layout/ v6.x)
|
||||
import 'react-native-gesture-handler';
|
||||
|
||||
import { LogBox, AppRegistry } from 'react-native';
|
||||
const Root = require('./root').default;
|
||||
|
||||
|
||||
@@ -343,7 +343,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-camera/RN (4.2.1):
|
||||
- React-Core
|
||||
- react-native-document-picker (8.2.1):
|
||||
- react-native-document-picker (8.2.0):
|
||||
- React-Core
|
||||
- react-native-fingerprint-scanner (6.0.0):
|
||||
- React
|
||||
@@ -361,7 +361,7 @@ PODS:
|
||||
- React
|
||||
- react-native-saf-x (2.12.0):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.5.5):
|
||||
- react-native-safe-area-context (4.5.3):
|
||||
- RCT-Folly
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
@@ -467,25 +467,24 @@ PODS:
|
||||
- React-Core
|
||||
- RNDateTimePicker (7.1.0):
|
||||
- React-Core
|
||||
- RNDeviceInfo (10.6.0):
|
||||
- React-Core
|
||||
- RNExitApp (1.1.0):
|
||||
- React
|
||||
- RNFileViewer (2.1.5):
|
||||
- React-Core
|
||||
- RNFS (2.20.0):
|
||||
- React-Core
|
||||
- RNGestureHandler (2.10.2):
|
||||
- RNGestureHandler (2.12.0):
|
||||
- React-Core
|
||||
- RNLocalize (3.0.0):
|
||||
- React-Core
|
||||
- RNQuickAction (0.3.13):
|
||||
- React
|
||||
- RNReanimated (3.2.0):
|
||||
- RNReanimated (3.3.0):
|
||||
- DoubleConversion
|
||||
- FBLazyVector
|
||||
- FBReactNativeSpec
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
@@ -495,6 +494,7 @@ PODS:
|
||||
- React-Core/RCTWebSocket
|
||||
- React-CoreModules
|
||||
- React-cxxreact
|
||||
- React-hermes
|
||||
- React-jsi
|
||||
- React-jsiexecutor
|
||||
- React-jsinspector
|
||||
@@ -608,7 +608,6 @@ DEPENDENCIES:
|
||||
- "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)"
|
||||
- "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)"
|
||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||
- RNExitApp (from `../node_modules/react-native-exit-app`)
|
||||
- RNFileViewer (from `../node_modules/react-native-file-viewer`)
|
||||
- RNFS (from `../node_modules/react-native-fs`)
|
||||
@@ -752,8 +751,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/@react-native-community/push-notification-ios"
|
||||
RNDateTimePicker:
|
||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||
RNDeviceInfo:
|
||||
:path: "../node_modules/react-native-device-info"
|
||||
RNExitApp:
|
||||
:path: "../node_modules/react-native-exit-app"
|
||||
RNFileViewer:
|
||||
@@ -817,7 +814,7 @@ SPEC CHECKSUMS:
|
||||
React-logger: ef2269b3afa6ba868da90496c3e17a4ec4f4cee0
|
||||
react-native-alarm-notification: 0732f97be04975a23ba60e675bdb961a0aaf6aa6
|
||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||
react-native-document-picker: 69ca2094d8780cfc1e7e613894d15290fdc54bba
|
||||
react-native-document-picker: 495c444c0c773c6e83a5d91165890ecb1c0a399a
|
||||
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||
react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903
|
||||
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
|
||||
@@ -826,7 +823,7 @@ SPEC CHECKSUMS:
|
||||
react-native-netinfo: ccbe1085dffd16592791d550189772e13bf479e2
|
||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||
react-native-saf-x: 129cd2ddf120a1f6164c724b2846d172666b33de
|
||||
react-native-safe-area-context: 33e71d7408dffe148b08968d97a524009634ccc8
|
||||
react-native-safe-area-context: b8979f5eda6ed5903d4dbc885be3846ea3daa753
|
||||
react-native-slider: 33b8d190b59d4f67a541061bb91775d53d617d9d
|
||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
||||
@@ -848,14 +845,13 @@ SPEC CHECKSUMS:
|
||||
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
|
||||
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
|
||||
RNDateTimePicker: 7ecd54a97fc3749f38c3c89a171f6cbd52f3c142
|
||||
RNDeviceInfo: 475a4c447168d0ad4c807e48ef5e0963a0f4eb1b
|
||||
RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3
|
||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNGestureHandler: f75d81410b40aaa99e71ae8f8bb7a88620c95042
|
||||
RNGestureHandler: dec4645026e7401a0899f2846d864403478ff6a5
|
||||
RNLocalize: 5944c97d2fe8150913a51ddd5eab4e23a82bd80d
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
RNReanimated: c931b356bda56c0196110bdc462e48e4c2668dc3
|
||||
RNReanimated: 9976fbaaeb8a188c36026154c844bf374b3b7eeb
|
||||
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
||||
RNShare: d82e10f6b7677f4b0048c23709bd04098d5aee6c
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
|
||||
@@ -43,16 +43,16 @@
|
||||
"react-native": "0.71.10",
|
||||
"react-native-action-button": "2.8.5",
|
||||
"react-native-camera": "4.2.1",
|
||||
"react-native-device-info": "10.6.0",
|
||||
"react-native-dialogbox": "0.6.10",
|
||||
"react-native-document-picker": "8.2.1",
|
||||
"react-native-drawer-layout": "3.2.1",
|
||||
"react-native-document-picker": "8.2.0",
|
||||
"react-native-draggable-flatlist": "4.0.1",
|
||||
"react-native-drawer-layout": "3.2.0",
|
||||
"react-native-dropdownalert": "4.5.1",
|
||||
"react-native-exit-app": "1.1.0",
|
||||
"react-native-file-viewer": "2.1.5",
|
||||
"react-native-fingerprint-scanner": "6.0.0",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-gesture-handler": "2.10.2",
|
||||
"react-native-gesture-handler": "2.12.0",
|
||||
"react-native-get-random-values": "1.9.0",
|
||||
"react-native-image-picker": "5.4.2",
|
||||
"react-native-image-resizer": "1.4.5",
|
||||
@@ -61,11 +61,12 @@
|
||||
"react-native-paper": "5.8.0",
|
||||
"react-native-popup-menu": "0.16.1",
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-reanimated": "3.2.0",
|
||||
"react-native-reanimated": "3.3.0",
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "4.5.5",
|
||||
"react-native-safe-area-context": "4.5.3",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "8.2.2",
|
||||
"react-native-side-menu-updated": "1.3.2",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
"react-native-vector-icons": "9.2.0",
|
||||
|
||||
@@ -28,7 +28,7 @@ import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
|
||||
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
|
||||
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
|
||||
const VersionInfo = require('react-native-version-info').default;
|
||||
const { Keyboard, BackHandler, View, StatusBar, Platform, Dimensions } = require('react-native');
|
||||
const { Keyboard, BackHandler, Animated, View, StatusBar, Platform, Dimensions } = require('react-native');
|
||||
import { AppState as RNAppState, EmitterSubscription, Linking, NativeEventSubscription } from 'react-native';
|
||||
import getResponsiveValue from './components/getResponsiveValue';
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
@@ -67,7 +67,7 @@ const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js'
|
||||
import EncryptionConfigScreen from './components/screens/encryption-config';
|
||||
const { DropboxLoginScreen } = require('./components/screens/dropbox-login.js');
|
||||
const { MenuContext } = require('react-native-popup-menu');
|
||||
import { Drawer } from 'react-native-drawer-layout';
|
||||
import SideMenu from './components/SideMenu';
|
||||
import SideMenuContent from './components/side-menu-content';
|
||||
const { SideMenuContentNote } = require('./components/side-menu-content-note.js');
|
||||
const { DatabaseDriverReactNative } = require('./utils/database-driver-react-native');
|
||||
@@ -116,9 +116,7 @@ import ProfileEditor from './components/ProfileSwitcher/ProfileEditor';
|
||||
import sensorInfo, { SensorInfo } from './components/biometrics/sensorInfo';
|
||||
import { getCurrentProfile } from '@joplin/lib/services/profileConfig';
|
||||
import { getDatabaseName, getProfilesRootDir, getResourceDir, setDispatch } from './services/profiles';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type SideMenuPosition = 'left' | 'right';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
|
||||
const logger = Logger.create('root');
|
||||
|
||||
@@ -715,6 +713,7 @@ class AppComponent extends React.Component {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
sideMenuContentOpacity: new Animated.Value(0),
|
||||
sideMenuWidth: this.getSideMenuWidth(),
|
||||
sensorInfo: null,
|
||||
};
|
||||
@@ -958,8 +957,8 @@ class AppComponent extends React.Component {
|
||||
if (this.props.appState !== 'ready') return null;
|
||||
const theme: Theme = themeStyle(this.props.themeId);
|
||||
|
||||
let sideMenuContent: ReactNode = null;
|
||||
let menuPosition: SideMenuPosition = 'left';
|
||||
let sideMenuContent = null;
|
||||
let menuPosition = 'left';
|
||||
|
||||
if (this.props.routeName === 'Note') {
|
||||
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>;
|
||||
@@ -998,20 +997,18 @@ class AppComponent extends React.Component {
|
||||
|
||||
const mainContent = (
|
||||
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
|
||||
<Drawer
|
||||
// Need to reset the key here based on menu position, otherwise
|
||||
// the drawer will flash open on screen and close every time the
|
||||
// drawer position switches (i.e. when opening or closing a note)
|
||||
key={`main-drawer-${menuPosition}`}
|
||||
open={this.props.showSideMenu}
|
||||
onOpen={() => this.sideMenu_change(true)}
|
||||
onClose={() => this.sideMenu_change(false)}
|
||||
drawerPosition={menuPosition}
|
||||
swipeEdgeWidth={15}
|
||||
drawerStyle={{
|
||||
width: this.state.sideMenuWidth,
|
||||
<SideMenu
|
||||
menu={sideMenuContent}
|
||||
edgeHitWidth={5}
|
||||
openMenuOffset={this.state.sideMenuWidth}
|
||||
menuPosition={menuPosition}
|
||||
onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)}
|
||||
onSliding={(percent: number) => {
|
||||
this.props.dispatch({
|
||||
type: 'SIDE_MENU_OPEN_PERCENT',
|
||||
value: percent,
|
||||
});
|
||||
}}
|
||||
renderDrawerContent={() => sideMenuContent}
|
||||
>
|
||||
<StatusBar barStyle={statusBarStyle} />
|
||||
<MenuContext style={{ flex: 1 }}>
|
||||
@@ -1028,7 +1025,7 @@ class AppComponent extends React.Component {
|
||||
/> }
|
||||
</SafeAreaView>
|
||||
</MenuContext>
|
||||
</Drawer>
|
||||
</SideMenu>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1037,20 +1034,22 @@ class AppComponent extends React.Component {
|
||||
|
||||
// Wrap everything in a PaperProvider -- this allows using components from react-native-paper
|
||||
return (
|
||||
<PaperProvider theme={{
|
||||
...paperTheme,
|
||||
version: 3,
|
||||
colors: {
|
||||
...paperTheme.colors,
|
||||
onPrimaryContainer: theme.color5,
|
||||
primaryContainer: theme.backgroundColor5,
|
||||
surfaceVariant: theme.backgroundColor,
|
||||
onSurfaceVariant: theme.color,
|
||||
primary: theme.color,
|
||||
},
|
||||
}}>
|
||||
{mainContent}
|
||||
</PaperProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<PaperProvider theme={{
|
||||
...paperTheme,
|
||||
version: 3,
|
||||
colors: {
|
||||
...paperTheme.colors,
|
||||
onPrimaryContainer: theme.color5,
|
||||
primaryContainer: theme.backgroundColor5,
|
||||
surfaceVariant: theme.backgroundColor,
|
||||
onSurfaceVariant: theme.color,
|
||||
primary: theme.color,
|
||||
},
|
||||
}}>
|
||||
{mainContent}
|
||||
</PaperProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Dispatch } from 'redux';
|
||||
const { NativeEventEmitter, NativeModules, Platform } = require('react-native');
|
||||
|
||||
interface NotificationData {
|
||||
joplinNotificationId: string;
|
||||
noteId: string;
|
||||
joplinNotificationId: string;
|
||||
noteId: string;
|
||||
}
|
||||
|
||||
export default async (dispatch: Dispatch) => {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"README.md"
|
||||
],
|
||||
"devDependencies": {
|
||||
"standard": "17.1.0",
|
||||
"tap": "16.3.7"
|
||||
"standard": "17.0.0",
|
||||
"tap": "16.3.6"
|
||||
},
|
||||
"gitHead": "eb4b0e64eab40a51b0895d3a40a9d8c3cb7b1b14"
|
||||
}
|
||||
|
||||
@@ -542,7 +542,7 @@ export default class BaseApplication {
|
||||
const newState = store.getState();
|
||||
|
||||
if (this.hasGui() && ['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
|
||||
if (!(await reg.syncTarget().syncStarted())) void reg.scheduleSync(1000, { syncSteps: ['update_remote', 'delete_remote'] });
|
||||
if (!(await reg.syncTarget().syncStarted())) void reg.scheduleSync(30 * 1000, { syncSteps: ['update_remote', 'delete_remote'] });
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import EventDispatcher from './EventDispatcher';
|
||||
|
||||
enum TestKey {
|
||||
FooEvent,
|
||||
BarEvent,
|
||||
BazEvent,
|
||||
FooEvent,
|
||||
BarEvent,
|
||||
BazEvent,
|
||||
}
|
||||
|
||||
describe('EventDispatcher', () => {
|
||||
|
||||
@@ -19,14 +19,14 @@ export function getTagCallbackUrl(tagId: string) {
|
||||
}
|
||||
|
||||
export const enum CallbackUrlCommand {
|
||||
OpenNote = 'openNote',
|
||||
OpenFolder = 'openFolder',
|
||||
OpenTag = 'openTag',
|
||||
OpenNote = 'openNote',
|
||||
OpenFolder = 'openFolder',
|
||||
OpenTag = 'openTag',
|
||||
}
|
||||
|
||||
export interface CallbackUrlInfo {
|
||||
command: CallbackUrlCommand;
|
||||
params: Record<string, string>;
|
||||
command: CallbackUrlCommand;
|
||||
params: Record<string, string>;
|
||||
}
|
||||
|
||||
export function parseCallbackUrl(s: string): CallbackUrlInfo {
|
||||
|
||||
@@ -3,8 +3,8 @@ const { useCallback, useEffect, useState } = shim.react();
|
||||
import useEventListener from './useEventListener';
|
||||
|
||||
interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
function useElementSize(elementRef: any): Size {
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('import-enex-md-gen', () => {
|
||||
|
||||
it('should convert ENEX content to Markdown', async () => {
|
||||
const files = await shim.fsDriver().readDirStats(enexSampleBaseDir);
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const htmlFilename = files[i].path;
|
||||
if (htmlFilename.indexOf('.html') < 0) continue;
|
||||
@@ -107,14 +108,6 @@ describe('import-enex-md-gen', () => {
|
||||
expect(all[0].size).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle tasks', async () => {
|
||||
const filePath = `${enexSampleBaseDir}/tasks.enex`;
|
||||
await importEnex('', filePath);
|
||||
const expectedMd = await shim.fsDriver().readFile(`${enexSampleBaseDir}/tasks.md`);
|
||||
const note: NoteEntity = (await Note.all())[0];
|
||||
expect(note.body).toEqual(expectedMd);
|
||||
});
|
||||
|
||||
it('should handle empty note content', async () => {
|
||||
const filePath = `${enexSampleBaseDir}/empty_content.enex`;
|
||||
await expectNotThrow(() => importEnex('', filePath));
|
||||
|
||||
@@ -39,7 +39,6 @@ enum ListTag {
|
||||
Ul = 'ul',
|
||||
Ol = 'ol',
|
||||
CheckboxList = 'checkboxList',
|
||||
TaskList = 'taskList',
|
||||
}
|
||||
|
||||
interface ParserStateList {
|
||||
@@ -59,13 +58,6 @@ interface ParserState {
|
||||
currentCode?: string;
|
||||
}
|
||||
|
||||
|
||||
interface ExtractedTask {
|
||||
title: string;
|
||||
completed: boolean;
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
interface EnexXmlToMdArrayResult {
|
||||
content: Section;
|
||||
resources: ResourceEntity[];
|
||||
@@ -562,7 +554,7 @@ function isHighlight(context: any, _nodeName: string, attributes: any) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function enexXmlToMdArray(stream: any, resources: ResourceEntity[], tasks: ExtractedTask[]): Promise<EnexXmlToMdArrayResult> {
|
||||
function enexXmlToMdArray(stream: any, resources: ResourceEntity[]): Promise<EnexXmlToMdArrayResult> {
|
||||
const remainingResources = resources.slice();
|
||||
|
||||
const removeRemainingResource = (id: string) => {
|
||||
@@ -626,12 +618,6 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[], tasks: Extra
|
||||
saxStream.on('text', (text: string) => {
|
||||
if (['table', 'tr', 'tbody'].indexOf(section.type) >= 0) return;
|
||||
|
||||
const currentList = state.lists && state.lists.length ? state.lists[state.lists.length - 1] : null;
|
||||
if ((currentList) && (currentList.tag === ListTag.TaskList)) {
|
||||
// skip text on task lists
|
||||
return;
|
||||
}
|
||||
|
||||
text = !state.inPre ? unwrapInnerText(text) : text;
|
||||
section.lines = collapseWhiteSpaceAndAppend(section.lines, state, text);
|
||||
});
|
||||
@@ -755,20 +741,7 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[], tasks: Extra
|
||||
section.lines.push(newSection);
|
||||
section = newSection;
|
||||
} else if (isBlockTag(n)) {
|
||||
const isTodosList = cssValue(this, nodeAttributes.style, '--en-task-group') === 'true';
|
||||
if (isTodosList) {
|
||||
const todoGroup = cssValue(this, nodeAttributes.style, '--en-id');
|
||||
section.lines.push(BLOCK_OPEN);
|
||||
for (const t of tasks) {
|
||||
if (t.groupId === todoGroup) {
|
||||
section.lines.push(`- [${t.completed ? 'x' : ' '}] ${t.title}\n`);
|
||||
}
|
||||
}
|
||||
tagInfo.name = ListTag.TaskList;
|
||||
state.lists.push({ tag: ListTag.TaskList, counter: 1, startedText: false });
|
||||
} else {
|
||||
section.lines.push(BLOCK_OPEN);
|
||||
}
|
||||
section.lines.push(BLOCK_OPEN);
|
||||
} else if (isListTag(n)) {
|
||||
section.lines.push(BLOCK_OPEN);
|
||||
const isCheckboxList = cssValue(this, nodeAttributes.style, '--en-todo') === 'true';
|
||||
@@ -984,9 +957,6 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[], tasks: Extra
|
||||
if (section && section.parent) section = section.parent;
|
||||
}
|
||||
} else if (isNewLineOnlyEndTag(n)) {
|
||||
if (poppedTag.name === ListTag.TaskList) {
|
||||
state.lists.pop();
|
||||
}
|
||||
section.lines.push(BLOCK_CLOSE);
|
||||
} else if (n === 'td' || n === 'th') {
|
||||
if (section && section.parent) section = section.parent;
|
||||
@@ -1400,9 +1370,9 @@ function renderLines(lines: any[]) {
|
||||
return mdLines;
|
||||
}
|
||||
|
||||
async function enexXmlToMd(xmlString: string, resources: ResourceEntity[], tasks: ExtractedTask[]) {
|
||||
async function enexXmlToMd(xmlString: string, resources: ResourceEntity[]) {
|
||||
const stream = stringToStream(xmlString);
|
||||
const result = await enexXmlToMdArray(stream, resources, tasks);
|
||||
const result = await enexXmlToMdArray(stream, resources);
|
||||
|
||||
let mdLines = renderLines(result.content.lines);
|
||||
|
||||
|
||||
@@ -138,15 +138,8 @@ interface ExtractedResource {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface ExtractedTask {
|
||||
title: string;
|
||||
completed: boolean;
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
interface ExtractedNote extends NoteEntity {
|
||||
resources?: ExtractedResource[];
|
||||
tasks: ExtractedTask[];
|
||||
tags?: string[];
|
||||
title?: string;
|
||||
bodyXml?: string;
|
||||
@@ -376,7 +369,6 @@ export default async function importEnex(parentFolderId: string, filePath: strin
|
||||
let note: ExtractedNote = null;
|
||||
let noteAttributes: Record<string, any> = null;
|
||||
let noteResource: ExtractedResource = null;
|
||||
let noteTask: ExtractedTask = null;
|
||||
let noteResourceAttributes: Record<string, any> = null;
|
||||
let noteResourceRecognition: NoteResourceRecognition = null;
|
||||
const notes: ExtractedNote[] = [];
|
||||
@@ -440,7 +432,7 @@ export default async function importEnex(parentFolderId: string, filePath: strin
|
||||
|
||||
const body = importOptions.outputFormat === 'html' ?
|
||||
await enexXmlToHtml(note.bodyXml, note.resources) :
|
||||
await enexXmlToMd(note.bodyXml, note.resources, note.tasks);
|
||||
await enexXmlToMd(note.bodyXml, note.resources);
|
||||
delete note.bodyXml;
|
||||
|
||||
note.markup_language = importOptions.outputFormat === 'html' ?
|
||||
@@ -518,14 +510,6 @@ export default async function importEnex(parentFolderId: string, filePath: strin
|
||||
if (!(n in noteResource)) (noteResource as any)[n] = '';
|
||||
(noteResource as any)[n] += text;
|
||||
}
|
||||
} else if (noteTask) {
|
||||
if (n === 'title') {
|
||||
noteTask.title = text;
|
||||
} else if (n === 'taskStatus') {
|
||||
noteTask.completed = text === 'completed';
|
||||
} else if (n === 'taskGroupNoteLevelID') {
|
||||
noteTask.groupId = text;
|
||||
}
|
||||
} else if (note) {
|
||||
if (n === 'title') {
|
||||
note.title = text;
|
||||
@@ -552,7 +536,6 @@ export default async function importEnex(parentFolderId: string, filePath: strin
|
||||
if (n === 'note') {
|
||||
note = {
|
||||
resources: [],
|
||||
tasks: [],
|
||||
tags: [],
|
||||
bodyXml: '',
|
||||
};
|
||||
@@ -566,12 +549,6 @@ export default async function importEnex(parentFolderId: string, filePath: strin
|
||||
noteResource = {
|
||||
hasData: false,
|
||||
};
|
||||
} else if (n === 'task') {
|
||||
noteTask = {
|
||||
title: '',
|
||||
completed: false,
|
||||
groupId: '',
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -625,9 +602,6 @@ export default async function importEnex(parentFolderId: string, filePath: strin
|
||||
note.source_url = noteAttributes['source-url'] ? noteAttributes['source-url'].trim() : '';
|
||||
|
||||
noteAttributes = null;
|
||||
} else if (n === 'task') {
|
||||
note.tasks.push(noteTask);
|
||||
noteTask = null;
|
||||
} else if (n === 'resource') {
|
||||
let mimeType = noteResource.mime ? noteResource.mime.trim() : '';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Setting from '../models/Setting';
|
||||
import checkProviderIsSupported from '../utils/webDAVUtils';
|
||||
|
||||
interface Script {
|
||||
exec: ()=> Promise<void>;
|
||||
exec: ()=> Promise<void>;
|
||||
}
|
||||
|
||||
const script: Script = <Script>{};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import InteropService from '../../services/interop/InteropService';
|
||||
import { CustomExportContext, CustomImportContext, ModuleType } from '../../services/interop/types';
|
||||
import { CustomExportContext, CustomImportContext, Module, ModuleType } from '../../services/interop/types';
|
||||
import shim from '../../shim';
|
||||
import { fileContentEqual, setupDatabaseAndSynchronizer, switchClient, checkThrowAsync, exportDir, supportDir } from '../../testing/test-utils';
|
||||
import Folder from '../../models/Folder';
|
||||
@@ -10,9 +10,6 @@ import * as fs from 'fs-extra';
|
||||
import { FolderEntity, NoteEntity, ResourceEntity } from '../database/types';
|
||||
import { ModelType } from '../../BaseModel';
|
||||
import * as ArrayUtils from '../../ArrayUtils';
|
||||
import InteropService_Importer_Custom from './InteropService_Importer_Custom';
|
||||
import InteropService_Exporter_Custom from './InteropService_Exporter_Custom';
|
||||
import Module, { makeExportModule, makeImportModule } from './Module';
|
||||
|
||||
async function recreateExportDir() {
|
||||
const dir = exportDir();
|
||||
@@ -50,35 +47,35 @@ function memoryExportModule() {
|
||||
resources: [],
|
||||
};
|
||||
|
||||
const module: Module = makeExportModule({
|
||||
const module: Module = {
|
||||
type: ModuleType.Exporter,
|
||||
description: 'Memory Export Module',
|
||||
format: 'memory',
|
||||
fileExtensions: ['memory'],
|
||||
}, () => {
|
||||
return new InteropService_Exporter_Custom({
|
||||
onInit: async (context: CustomExportContext) => {
|
||||
result.destPath = context.destPath;
|
||||
},
|
||||
isCustom: true,
|
||||
|
||||
onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => {
|
||||
result.items.push({
|
||||
type: itemType,
|
||||
object: item,
|
||||
});
|
||||
},
|
||||
onInit: async (context: CustomExportContext) => {
|
||||
result.destPath = context.destPath;
|
||||
},
|
||||
|
||||
onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => {
|
||||
result.resources.push({
|
||||
filePath: filePath,
|
||||
object: resource,
|
||||
});
|
||||
},
|
||||
onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => {
|
||||
result.items.push({
|
||||
type: itemType,
|
||||
object: item,
|
||||
});
|
||||
},
|
||||
|
||||
onClose: async (_context: CustomExportContext) => {
|
||||
// nothing
|
||||
},
|
||||
});
|
||||
});
|
||||
onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => {
|
||||
result.resources.push({
|
||||
filePath: filePath,
|
||||
object: resource,
|
||||
});
|
||||
},
|
||||
|
||||
onClose: async (_context: CustomExportContext) => {
|
||||
// nothing
|
||||
},
|
||||
};
|
||||
|
||||
return { result, module };
|
||||
}
|
||||
@@ -558,19 +555,18 @@ describe('services_InteropService', () => {
|
||||
sourcePath: '',
|
||||
};
|
||||
|
||||
const module = makeImportModule({
|
||||
const module: Module = {
|
||||
type: ModuleType.Importer,
|
||||
description: 'Test Import Module',
|
||||
format: 'testing',
|
||||
fileExtensions: ['test'],
|
||||
}, () => {
|
||||
return new InteropService_Importer_Custom({
|
||||
onExec: async (context: CustomImportContext) => {
|
||||
result.hasBeenExecuted = true;
|
||||
result.sourcePath = context.sourcePath;
|
||||
},
|
||||
});
|
||||
});
|
||||
isCustom: true,
|
||||
|
||||
onExec: async (context: CustomImportContext) => {
|
||||
result.hasBeenExecuted = true;
|
||||
result.sourcePath = context.sourcePath;
|
||||
},
|
||||
};
|
||||
|
||||
const service = InteropService.instance();
|
||||
service.registerModule(module);
|
||||
@@ -600,32 +596,31 @@ describe('services_InteropService', () => {
|
||||
closeCalled: false,
|
||||
};
|
||||
|
||||
const module: Module = makeExportModule({
|
||||
const module: Module = {
|
||||
type: ModuleType.Exporter,
|
||||
description: 'Test Export Module',
|
||||
format: 'testing',
|
||||
fileExtensions: ['test'],
|
||||
}, () => {
|
||||
return new InteropService_Exporter_Custom({
|
||||
onInit: async (context: CustomExportContext) => {
|
||||
result.destPath = context.destPath;
|
||||
},
|
||||
isCustom: true,
|
||||
|
||||
onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => {
|
||||
result.itemTypes.push(itemType);
|
||||
result.items.push(item);
|
||||
},
|
||||
onInit: async (context: CustomExportContext) => {
|
||||
result.destPath = context.destPath;
|
||||
},
|
||||
|
||||
onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => {
|
||||
result.resources.push(resource);
|
||||
result.filePaths.push(filePath);
|
||||
},
|
||||
onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => {
|
||||
result.itemTypes.push(itemType);
|
||||
result.items.push(item);
|
||||
},
|
||||
|
||||
onClose: async (_context: CustomExportContext) => {
|
||||
result.closeCalled = true;
|
||||
},
|
||||
});
|
||||
});
|
||||
onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => {
|
||||
result.resources.push(resource);
|
||||
result.filePaths.push(filePath);
|
||||
},
|
||||
|
||||
onClose: async (_context: CustomExportContext) => {
|
||||
result.closeCalled = true;
|
||||
},
|
||||
};
|
||||
|
||||
const service = InteropService.instance();
|
||||
service.registerModule(module);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ModuleType, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ExportOptions, ImportExportResult } from './types';
|
||||
import { ModuleType, FileSystemItem, ImportModuleOutputFormat, Module, ImportOptions, ExportOptions, ImportExportResult, defaultImportExportModule } from './types';
|
||||
import InteropService_Importer_Custom from './InteropService_Importer_Custom';
|
||||
import InteropService_Exporter_Custom from './InteropService_Exporter_Custom';
|
||||
import shim from '../../shim';
|
||||
import { _ } from '../../locale';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
@@ -8,22 +10,9 @@ import Folder from '../../models/Folder';
|
||||
import NoteTag from '../../models/NoteTag';
|
||||
import Note from '../../models/Note';
|
||||
import * as ArrayUtils from '../../ArrayUtils';
|
||||
import InteropService_Importer_Jex from './InteropService_Importer_Jex';
|
||||
import InteropService_Importer_Md from './InteropService_Importer_Md';
|
||||
import InteropService_Importer_Md_frontmatter from './InteropService_Importer_Md_frontmatter';
|
||||
import InteropService_Importer_Raw from './InteropService_Importer_Raw';
|
||||
import InteropService_Importer_EnexToMd from './InteropService_Importer_EnexToMd';
|
||||
import InteropService_Importer_EnexToHtml from './InteropService_Importer_EnexToHtml';
|
||||
import InteropService_Exporter_Jex from './InteropService_Exporter_Jex';
|
||||
import InteropService_Exporter_Raw from './InteropService_Exporter_Raw';
|
||||
import InteropService_Exporter_Md from './InteropService_Exporter_Md';
|
||||
import InteropService_Exporter_Md_frontmatter from './InteropService_Exporter_Md_frontmatter';
|
||||
import InteropService_Exporter_Html from './InteropService_Exporter_Html';
|
||||
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
||||
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
|
||||
import Module, { makeExportModule, makeImportModule } from './Module';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { fileExtension } = require('../../path-utils');
|
||||
const { toTitleCase } = require('../../string-utils');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
export default class InteropService {
|
||||
@@ -54,46 +43,47 @@ export default class InteropService {
|
||||
|
||||
public modules() {
|
||||
if (!this.defaultModules_) {
|
||||
const importModules = [
|
||||
makeImportModule({
|
||||
const importModules: Module[] = [
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Importer),
|
||||
format: 'jex',
|
||||
fileExtensions: ['jex'],
|
||||
sources: [FileSystemItem.File],
|
||||
description: _('Joplin Export File'),
|
||||
}, () => new InteropService_Importer_Jex()),
|
||||
|
||||
makeImportModule({
|
||||
},
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Importer),
|
||||
format: 'md',
|
||||
fileExtensions: ['md', 'markdown', 'txt', 'html'],
|
||||
sources: [FileSystemItem.File, FileSystemItem.Directory],
|
||||
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
|
||||
description: _('Markdown'),
|
||||
}, () => new InteropService_Importer_Md()),
|
||||
|
||||
makeImportModule({
|
||||
},
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Importer),
|
||||
format: 'md_frontmatter',
|
||||
fileExtensions: ['md', 'markdown', 'txt', 'html'],
|
||||
sources: [FileSystemItem.File, FileSystemItem.Directory],
|
||||
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
|
||||
description: _('Markdown + Front Matter'),
|
||||
}, () => new InteropService_Importer_Md_frontmatter()),
|
||||
|
||||
makeImportModule({
|
||||
},
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Importer),
|
||||
format: 'raw',
|
||||
sources: [FileSystemItem.Directory],
|
||||
description: _('Joplin Export Directory'),
|
||||
}, () => new InteropService_Importer_Raw()),
|
||||
|
||||
makeImportModule({
|
||||
},
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Importer),
|
||||
format: 'enex',
|
||||
fileExtensions: ['enex'],
|
||||
sources: [FileSystemItem.File],
|
||||
description: _('Evernote Export File (as Markdown)'),
|
||||
importerClass: 'InteropService_Importer_EnexToMd',
|
||||
isDefault: true,
|
||||
}, () => new InteropService_Importer_EnexToMd()),
|
||||
|
||||
makeImportModule({
|
||||
},
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Importer),
|
||||
format: 'enex',
|
||||
fileExtensions: ['enex'],
|
||||
sources: [FileSystemItem.File],
|
||||
@@ -101,58 +91,65 @@ export default class InteropService {
|
||||
// TODO: Consider doing this the same way as the multiple `md` importers are handled
|
||||
importerClass: 'InteropService_Importer_EnexToHtml',
|
||||
outputFormat: ImportModuleOutputFormat.Html,
|
||||
}, () => new InteropService_Importer_EnexToHtml()),
|
||||
},
|
||||
];
|
||||
|
||||
const exportModules = [
|
||||
makeExportModule({
|
||||
const exportModules: Module[] = [
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Exporter),
|
||||
format: 'jex',
|
||||
fileExtensions: ['jex'],
|
||||
target: FileSystemItem.File,
|
||||
description: _('Joplin Export File'),
|
||||
}, () => new InteropService_Exporter_Jex()),
|
||||
|
||||
makeExportModule({
|
||||
},
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Exporter),
|
||||
format: 'raw',
|
||||
target: FileSystemItem.Directory,
|
||||
description: _('Joplin Export Directory'),
|
||||
}, () => new InteropService_Exporter_Raw()),
|
||||
|
||||
makeExportModule({
|
||||
},
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Exporter),
|
||||
format: 'md',
|
||||
target: FileSystemItem.Directory,
|
||||
description: _('Markdown'),
|
||||
}, () => new InteropService_Exporter_Md()),
|
||||
|
||||
makeExportModule({
|
||||
},
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Exporter),
|
||||
format: 'md_frontmatter',
|
||||
target: FileSystemItem.Directory,
|
||||
description: _('Markdown + Front Matter'),
|
||||
}, () => new InteropService_Exporter_Md_frontmatter()),
|
||||
|
||||
makeExportModule({
|
||||
},
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Exporter),
|
||||
format: 'html',
|
||||
fileExtensions: ['html', 'htm'],
|
||||
target: FileSystemItem.File,
|
||||
isNoteArchive: false,
|
||||
description: _('HTML File'),
|
||||
}, () => new InteropService_Exporter_Html()),
|
||||
|
||||
makeExportModule({
|
||||
},
|
||||
{
|
||||
...defaultImportExportModule(ModuleType.Exporter),
|
||||
format: 'html',
|
||||
target: FileSystemItem.Directory,
|
||||
description: _('HTML Directory'),
|
||||
}, () => new InteropService_Exporter_Html()),
|
||||
},
|
||||
];
|
||||
|
||||
this.defaultModules_ = (importModules as Module[]).concat(exportModules);
|
||||
this.defaultModules_ = importModules.concat(exportModules);
|
||||
}
|
||||
|
||||
return this.defaultModules_.concat(this.userModules_);
|
||||
}
|
||||
|
||||
public registerModule(module: Module) {
|
||||
module = {
|
||||
...defaultImportExportModule(module.type),
|
||||
...module,
|
||||
};
|
||||
|
||||
this.userModules_.push(module);
|
||||
|
||||
this.eventEmitter_.emit('modulesChanged');
|
||||
}
|
||||
|
||||
@@ -169,13 +166,9 @@ export default class InteropService {
|
||||
if (m.format === format && m.type === type) {
|
||||
if (!target && !outputFormat) {
|
||||
matches.push(m);
|
||||
} else if (
|
||||
m.type === ModuleType.Exporter && target && target === m.target
|
||||
) {
|
||||
} else if (target && target === m.target) {
|
||||
matches.push(m);
|
||||
} else if (
|
||||
m.type === ModuleType.Importer && outputFormat && outputFormat === m.outputFormat
|
||||
) {
|
||||
} else if (outputFormat && outputFormat === m.outputFormat) {
|
||||
matches.push(m);
|
||||
}
|
||||
}
|
||||
@@ -187,6 +180,24 @@ export default class InteropService {
|
||||
return matches.length ? matches[0] : null;
|
||||
}
|
||||
|
||||
private modulePath(module: Module) {
|
||||
let className = '';
|
||||
if (module.type === ModuleType.Importer) {
|
||||
className = module.importerClass || `InteropService_Importer_${toTitleCase(module.format)}`;
|
||||
} else {
|
||||
className = `InteropService_Exporter_${toTitleCase(module.format)}`;
|
||||
}
|
||||
return `./${className}`;
|
||||
}
|
||||
|
||||
private newModuleFromCustomFactory(module: Module) {
|
||||
if (module.type === ModuleType.Importer) {
|
||||
return new InteropService_Importer_Custom(module);
|
||||
} else {
|
||||
return new InteropService_Exporter_Custom(module);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE TO FUTURE SELF: It might make sense to simply move all the existing
|
||||
// formatters to the `newModuleFromPath_` approach, so that there's only one way
|
||||
// to do this mapping. This isn't a priority right now (per the convo in:
|
||||
@@ -196,7 +207,18 @@ export default class InteropService {
|
||||
const moduleMetadata = this.findModuleByFormat_(type, format, null, outputFormat);
|
||||
if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s" and output "%s"', type, format, outputFormat));
|
||||
|
||||
return moduleMetadata.factory();
|
||||
let output = null;
|
||||
|
||||
if (moduleMetadata.isCustom) {
|
||||
output = this.newModuleFromCustomFactory(moduleMetadata);
|
||||
} else {
|
||||
const ModuleClass = shim.requireDynamic(this.modulePath(moduleMetadata)).default;
|
||||
output = new ModuleClass();
|
||||
}
|
||||
|
||||
output.setMetadata(moduleMetadata);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// The existing `newModuleByFormat_` fn would load by the input format. This
|
||||
@@ -209,7 +231,19 @@ export default class InteropService {
|
||||
const moduleMetadata = this.findModuleByFormat_(type, options.format, options.target);
|
||||
if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s" and target "%s"', type, options.format, options.target));
|
||||
|
||||
return moduleMetadata.factory(options);
|
||||
let output = null;
|
||||
|
||||
if (moduleMetadata.isCustom) {
|
||||
output = this.newModuleFromCustomFactory(moduleMetadata);
|
||||
} else {
|
||||
const modulePath = this.modulePath(moduleMetadata);
|
||||
const ModuleClass = shim.requireDynamic(modulePath).default;
|
||||
output = new ModuleClass();
|
||||
}
|
||||
|
||||
output.setMetadata({ options, ...moduleMetadata });
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private moduleByFileExtension_(type: ModuleType, ext: string) {
|
||||
@@ -220,7 +254,7 @@ export default class InteropService {
|
||||
for (let i = 0; i < modules.length; i++) {
|
||||
const m = modules[i];
|
||||
if (type !== m.type) continue;
|
||||
if (m.fileExtensions.includes(ext)) return m;
|
||||
if (m.fileExtensions && m.fileExtensions.indexOf(ext) >= 0) return m;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -239,12 +273,14 @@ export default class InteropService {
|
||||
if (options.format === 'auto') {
|
||||
const module = this.moduleByFileExtension_(ModuleType.Importer, fileExtension(options.path));
|
||||
if (!module) throw new Error(_('Please specify import format for %s', options.path));
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
options.format = module.format;
|
||||
}
|
||||
|
||||
if (options.destinationFolderId) {
|
||||
const folder = await Folder.load(options.destinationFolderId);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', options.destinationFolderId));
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
options.destinationFolder = folder;
|
||||
}
|
||||
|
||||
@@ -252,10 +288,6 @@ export default class InteropService {
|
||||
|
||||
const importer = this.newModuleByFormat_(ModuleType.Importer, options.format, options.outputFormat);
|
||||
|
||||
if (!(importer instanceof InteropService_Importer_Base)) {
|
||||
throw new Error('Resolved importer is not an importer');
|
||||
}
|
||||
|
||||
await importer.init(options.path, options);
|
||||
result = await importer.exec(result);
|
||||
|
||||
@@ -354,10 +386,6 @@ export default class InteropService {
|
||||
}
|
||||
|
||||
const exporter = this.newModuleFromPath_(ModuleType.Exporter, options);
|
||||
if (!(exporter instanceof InteropService_Exporter_Base)) {
|
||||
throw new Error('Resolved exporter is not an exporter');
|
||||
}
|
||||
|
||||
await exporter.init(exportPath, options);
|
||||
|
||||
const typeOrder = [BaseModel.TYPE_FOLDER, BaseModel.TYPE_RESOURCE, BaseModel.TYPE_NOTE, BaseModel.TYPE_TAG, BaseModel.TYPE_NOTE_TAG];
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { ExportContext } from '../plugins/api/types';
|
||||
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
|
||||
import { ExportOptions } from './types';
|
||||
|
||||
interface CustomImporter {
|
||||
onInit(context: any): Promise<void>;
|
||||
onProcessItem(context: any, itemType: number, item: any): Promise<void>;
|
||||
onProcessResource(context: any, resource: any, filePath: string): Promise<void>;
|
||||
onClose(context: any): Promise<void>;
|
||||
}
|
||||
import { ExportOptions, Module } from './types';
|
||||
|
||||
export default class InteropService_Exporter_Custom extends InteropService_Exporter_Base {
|
||||
|
||||
private customContext_: ExportContext;
|
||||
private module_: CustomImporter = null;
|
||||
private module_: Module = null;
|
||||
|
||||
public constructor(module: CustomImporter) {
|
||||
public constructor(module: Module) {
|
||||
super();
|
||||
this.module_ = module;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
||||
import { ImportExportResult } from './types';
|
||||
|
||||
interface CustomImporter {
|
||||
onExec(
|
||||
context: { sourcePath: string; options: any; warnings: string[] }
|
||||
): Promise<void>;
|
||||
}
|
||||
import { ImportExportResult, Module } from './types';
|
||||
|
||||
export default class InteropService_Importer_Custom extends InteropService_Importer_Base {
|
||||
|
||||
private module_: CustomImporter = null;
|
||||
private module_: Module = null;
|
||||
|
||||
public constructor(handler: CustomImporter) {
|
||||
public constructor(handler: Module) {
|
||||
super();
|
||||
this.module_ = handler;
|
||||
}
|
||||
@@ -29,12 +23,10 @@ export default class InteropService_Importer_Custom extends InteropService_Impor
|
||||
}
|
||||
}
|
||||
|
||||
await this.module_.onExec({
|
||||
return this.module_.onExec({
|
||||
sourcePath: this.sourcePath_,
|
||||
options: processedOptions,
|
||||
warnings: result.warnings,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
|
||||
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
||||
import { makeExportModule, makeImportModule } from './Module';
|
||||
import { FileSystemItem } from './types';
|
||||
|
||||
describe('Module', () => {
|
||||
it('should return correct default fullLabel for an ImportModule', () => {
|
||||
const baseMetadata = {
|
||||
format: 'Foo_test',
|
||||
description: 'Some description here',
|
||||
sources: [FileSystemItem.File, FileSystemItem.Directory],
|
||||
};
|
||||
|
||||
const importModuleMultiSource = makeImportModule(
|
||||
baseMetadata,
|
||||
() => new InteropService_Importer_Base()
|
||||
);
|
||||
|
||||
const importModuleSingleSource = makeImportModule({
|
||||
...baseMetadata,
|
||||
sources: [FileSystemItem.File],
|
||||
}, () => new InteropService_Importer_Base());
|
||||
|
||||
// The two modules should have the same data, except for their sources.
|
||||
expect(importModuleMultiSource.format).toBe('Foo_test');
|
||||
expect(importModuleSingleSource.format).toBe(importModuleMultiSource.format);
|
||||
expect(importModuleMultiSource.sources).toHaveLength(2);
|
||||
expect(importModuleSingleSource.sources).toHaveLength(1);
|
||||
|
||||
const baseLabel = 'FOO - Some description here';
|
||||
expect(importModuleMultiSource.fullLabel()).toBe(baseLabel);
|
||||
expect(importModuleSingleSource.fullLabel()).toBe(baseLabel);
|
||||
|
||||
// Should only include (File) if the import module has more than one source
|
||||
expect(importModuleMultiSource.fullLabel(FileSystemItem.File)).toBe(`${baseLabel} (File)`);
|
||||
expect(importModuleSingleSource.fullLabel(FileSystemItem.File)).toBe(baseLabel);
|
||||
});
|
||||
|
||||
it('should return correct default fullLabel for an ExportModule', () => {
|
||||
const testExportModule = makeExportModule({
|
||||
format: 'format_test_______TEST',
|
||||
description: 'Testing...',
|
||||
}, () => new InteropService_Exporter_Base());
|
||||
|
||||
// Should only include the portion of format before the first underscore
|
||||
const label = 'FORMAT - Testing...';
|
||||
expect(testExportModule.fullLabel()).toBe(label);
|
||||
|
||||
// Sources should only be shown for import modules
|
||||
expect(testExportModule.fullLabel(FileSystemItem.File)).toBe(label);
|
||||
expect(testExportModule.fullLabel(FileSystemItem.Directory)).toBe(label);
|
||||
});
|
||||
});
|
||||
@@ -1,123 +0,0 @@
|
||||
import { _ } from '../../locale';
|
||||
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
|
||||
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
||||
import { ExportOptions, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ModuleType } from './types';
|
||||
|
||||
// Metadata shared between importers and exporters.
|
||||
interface BaseMetadata {
|
||||
format: string;
|
||||
fileExtensions: string[];
|
||||
description: string;
|
||||
isDefault: boolean;
|
||||
|
||||
// Returns the full label to be displayed in the UI.
|
||||
fullLabel(moduleSource?: FileSystemItem): string;
|
||||
|
||||
// Only applies to single file exporters or importers
|
||||
// It tells whether the format can package multiple notes into one file.
|
||||
// For example JEX or ENEX can, but HTML cannot.
|
||||
// Default: true.
|
||||
isNoteArchive: boolean;
|
||||
}
|
||||
|
||||
interface ImportMetadata extends BaseMetadata {
|
||||
type: ModuleType.Importer;
|
||||
|
||||
sources: FileSystemItem[];
|
||||
importerClass: string;
|
||||
outputFormat: ImportModuleOutputFormat;
|
||||
}
|
||||
|
||||
export interface ImportModule extends ImportMetadata {
|
||||
factory(options?: ImportOptions): InteropService_Importer_Base;
|
||||
}
|
||||
|
||||
interface ExportMetadata extends BaseMetadata {
|
||||
type: ModuleType.Exporter;
|
||||
|
||||
target: FileSystemItem;
|
||||
}
|
||||
|
||||
export interface ExportModule extends ExportMetadata {
|
||||
factory(options?: ExportOptions): InteropService_Exporter_Base;
|
||||
}
|
||||
|
||||
const defaultBaseMetadata = {
|
||||
format: '',
|
||||
fileExtensions: [] as string[],
|
||||
description: '',
|
||||
isNoteArchive: true,
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
const moduleFullLabel = (metadata: ImportMetadata|ExportMetadata, moduleSource: FileSystemItem = null) => {
|
||||
const format = metadata.format.split('_')[0];
|
||||
const label = [`${format.toUpperCase()} - ${metadata.description}`];
|
||||
if (moduleSource && metadata.type === ModuleType.Importer && metadata.sources.length > 1) {
|
||||
label.push(`(${moduleSource === FileSystemItem.File ? _('File') : _('Directory')})`);
|
||||
}
|
||||
return label.join(' ');
|
||||
};
|
||||
|
||||
export const makeImportModule = (
|
||||
metadata: Partial<ImportMetadata>, factory: ()=> InteropService_Importer_Base
|
||||
): ImportModule => {
|
||||
const importerDefaults: ImportMetadata = {
|
||||
...defaultBaseMetadata,
|
||||
type: ModuleType.Importer,
|
||||
sources: [],
|
||||
importerClass: '',
|
||||
outputFormat: ImportModuleOutputFormat.Markdown,
|
||||
|
||||
fullLabel: (moduleSource?: FileSystemItem) => {
|
||||
return moduleFullLabel(fullMetadata, moduleSource);
|
||||
},
|
||||
};
|
||||
|
||||
const fullMetadata = {
|
||||
...importerDefaults,
|
||||
...metadata,
|
||||
};
|
||||
|
||||
return {
|
||||
...fullMetadata,
|
||||
factory: (options: ImportOptions = {}) => {
|
||||
const result = factory();
|
||||
result.setMetadata({ ...fullMetadata, ...(options ?? {}) });
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const makeExportModule = (
|
||||
metadata: Partial<ExportMetadata>, factory: ()=> InteropService_Exporter_Base
|
||||
): ExportModule => {
|
||||
const exporterDefaults: ExportMetadata = {
|
||||
...defaultBaseMetadata,
|
||||
type: ModuleType.Exporter,
|
||||
target: FileSystemItem.File,
|
||||
|
||||
fullLabel: (moduleSource?: FileSystemItem) => {
|
||||
return moduleFullLabel(fullMetadata, moduleSource);
|
||||
},
|
||||
};
|
||||
|
||||
const fullMetadata = {
|
||||
...exporterDefaults,
|
||||
...metadata,
|
||||
};
|
||||
|
||||
return {
|
||||
...fullMetadata,
|
||||
factory: (options: ExportOptions = {}) => {
|
||||
const result = factory();
|
||||
result.setMetadata({ ...fullMetadata, ...(options ?? {}) });
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type Module = ImportModule|ExportModule;
|
||||
export default Module;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { _ } from '../../locale';
|
||||
import { PluginStates } from '../plugins/reducer';
|
||||
|
||||
export interface CustomImportContext {
|
||||
@@ -26,6 +27,58 @@ export enum ImportModuleOutputFormat {
|
||||
Html = 'html',
|
||||
}
|
||||
|
||||
// For historical reasons the import and export modules share the same
|
||||
// interface, except that some properties are used only for import
|
||||
// and others only for export.
|
||||
export interface Module {
|
||||
// ---------------------------------------
|
||||
// Shared properties
|
||||
// ---------------------------------------
|
||||
|
||||
type: ModuleType;
|
||||
format: string;
|
||||
fileExtensions: string[];
|
||||
description: string;
|
||||
// path?: string;
|
||||
|
||||
// Only applies to single file exporters or importers
|
||||
// It tells whether the format can package multiple notes into one file.
|
||||
// For example JEX or ENEX can, but HTML cannot.
|
||||
// Default: true.
|
||||
isNoteArchive?: boolean;
|
||||
|
||||
// A custom module is one that was not hard-coded, that was created at runtime
|
||||
// by a plugin for example. If `isCustom` is `true` if it is expected that all
|
||||
// the event handlers below are defined (it's enforced by the plugin API).
|
||||
isCustom?: boolean;
|
||||
|
||||
// ---------------------------------------
|
||||
// Import-only properties
|
||||
// ---------------------------------------
|
||||
|
||||
sources?: FileSystemItem[];
|
||||
importerClass?: string;
|
||||
outputFormat?: ImportModuleOutputFormat;
|
||||
isDefault?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
fullLabel?: Function;
|
||||
|
||||
// Used only if `isCustom` is true
|
||||
onExec?(context: any): Promise<any>;
|
||||
|
||||
// ---------------------------------------
|
||||
// Export-only properties
|
||||
// ---------------------------------------
|
||||
|
||||
target?: FileSystemItem;
|
||||
|
||||
// Used only if `isCustom` is true
|
||||
onInit?(context: any): Promise<void>;
|
||||
onProcessItem?(context: any, itemType: number, item: any): Promise<void>;
|
||||
onProcessResource?(context: any, resource: any, filePath: string): Promise<void>;
|
||||
onClose?(context: any): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ImportOptions {
|
||||
path?: string;
|
||||
format?: string;
|
||||
@@ -66,3 +119,29 @@ export interface MdFrontMatterExport {
|
||||
'created'?: string;
|
||||
'tags'?: string[];
|
||||
}
|
||||
|
||||
function moduleFullLabel(moduleSource: FileSystemItem = null): string {
|
||||
const format = this.format.split('_')[0];
|
||||
const label = [`${format.toUpperCase()} - ${this.description}`];
|
||||
if (moduleSource && this.sources.length > 1) {
|
||||
label.push(`(${moduleSource === 'file' ? _('File') : _('Directory')})`);
|
||||
}
|
||||
return label.join(' ');
|
||||
}
|
||||
|
||||
export function defaultImportExportModule(type: ModuleType): Module {
|
||||
return {
|
||||
type: type,
|
||||
format: '',
|
||||
fileExtensions: [],
|
||||
sources: [],
|
||||
description: '',
|
||||
isNoteArchive: true,
|
||||
importerClass: '',
|
||||
outputFormat: ImportModuleOutputFormat.Markdown,
|
||||
isDefault: false,
|
||||
fullLabel: moduleFullLabel,
|
||||
isCustom: false,
|
||||
target: FileSystemItem.File,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface DefaultPluginSettings {
|
||||
}
|
||||
|
||||
export interface DefaultPluginsInfo {
|
||||
[pluginId: string]: DefaultPluginSettings;
|
||||
[pluginId: string]: DefaultPluginSettings;
|
||||
}
|
||||
|
||||
export interface PluginSetting {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
import InteropService from '../../interop/InteropService';
|
||||
import InteropService_Exporter_Custom from '../../interop/InteropService_Exporter_Custom';
|
||||
import InteropService_Importer_Custom from '../../interop/InteropService_Importer_Custom';
|
||||
import { makeExportModule, makeImportModule } from '../../interop/Module';
|
||||
import { ModuleType } from '../../interop/types';
|
||||
import { Module, ModuleType } from '../../interop/types';
|
||||
import { ExportModule, ImportModule } from './types';
|
||||
|
||||
/**
|
||||
@@ -22,23 +19,23 @@ import { ExportModule, ImportModule } from './types';
|
||||
export default class JoplinInterop {
|
||||
|
||||
public async registerExportModule(module: ExportModule) {
|
||||
const internalModule = makeExportModule({
|
||||
const internalModule: Module = {
|
||||
...module,
|
||||
type: ModuleType.Exporter,
|
||||
isCustom: true,
|
||||
fileExtensions: module.fileExtensions ? module.fileExtensions : [],
|
||||
}, () => new InteropService_Exporter_Custom(module));
|
||||
};
|
||||
|
||||
return InteropService.instance().registerModule(internalModule);
|
||||
}
|
||||
|
||||
public async registerImportModule(module: ImportModule) {
|
||||
const internalModule = makeImportModule({
|
||||
const internalModule: Module = {
|
||||
...module,
|
||||
type: ModuleType.Importer,
|
||||
isCustom: true,
|
||||
fileExtensions: module.fileExtensions ? module.fileExtensions : [],
|
||||
}, () => {
|
||||
return new InteropService_Importer_Custom(module);
|
||||
});
|
||||
};
|
||||
|
||||
return InteropService.instance().registerModule(internalModule);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ interface Term {
|
||||
}
|
||||
|
||||
enum Relation {
|
||||
OR = 'OR',
|
||||
AND = 'AND',
|
||||
OR = 'OR',
|
||||
AND = 'AND',
|
||||
}
|
||||
|
||||
enum Operation {
|
||||
|
||||
@@ -57,13 +57,6 @@ const shim = {
|
||||
return process && process.platform === 'linux';
|
||||
},
|
||||
|
||||
isGNOME: () => {
|
||||
// XDG_CURRENT_DESKTOP may be something like "ubuntu:GNOME" and not just "GNOME".
|
||||
// Thus, we use .includes and not ===.
|
||||
return (shim.isLinux() || shim.isFreeBSD())
|
||||
&& process && (process.env['XDG_CURRENT_DESKTOP'] ?? '').includes('GNOME');
|
||||
},
|
||||
|
||||
isFreeBSD: () => {
|
||||
return process && process.platform === 'freebsd';
|
||||
},
|
||||
|
||||
@@ -4,8 +4,8 @@ import { reg } from './registry';
|
||||
import { Plugins } from './services/plugins/PluginService';
|
||||
|
||||
interface PluginList {
|
||||
completeList: string;
|
||||
summary: string;
|
||||
completeList: string;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
function getPluginLists(plugins: Plugins): PluginList {
|
||||
|
||||
@@ -2,9 +2,9 @@ import { useRef, useEffect, MutableRefObject } from 'react';
|
||||
|
||||
export interface VisibleOnSelect {
|
||||
container: MutableRefObject<HTMLElement>;
|
||||
wrapperRef: MutableRefObject<HTMLElement>;
|
||||
wrapperRef: MutableRefObject<HTMLElement>;
|
||||
isVisible: boolean;
|
||||
isSelected: boolean;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
// Used in thumbnail view, to scroll to the newly selected page.
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"babel-jest": "29.5.0",
|
||||
"css-loader": "6.8.1",
|
||||
"css-loader": "6.7.4",
|
||||
"jest": "29.5.0",
|
||||
"jest-environment-jsdom": "29.5.0",
|
||||
"style-loader": "3.3.3",
|
||||
|
||||
@@ -5,9 +5,9 @@ export interface ScaledSize {
|
||||
}
|
||||
|
||||
export interface IconButtonProps {
|
||||
onClick: ()=> void;
|
||||
size?: number;
|
||||
color?: string;
|
||||
onClick: ()=> void;
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface RenderRequest {
|
||||
|
||||
@@ -24,12 +24,12 @@ const ButtonElement = styled.button<{ hoverColor?: string; size?: number; color?
|
||||
`;
|
||||
|
||||
interface BaseButtonProps {
|
||||
icon: IconDefinition;
|
||||
onClick: ()=> void;
|
||||
name: string;
|
||||
size: number;
|
||||
color: string;
|
||||
hoverColor?: string;
|
||||
icon: IconDefinition;
|
||||
onClick: ()=> void;
|
||||
name: string;
|
||||
size: number;
|
||||
color: string;
|
||||
hoverColor?: string;
|
||||
}
|
||||
|
||||
function BaseButton({ onClick, icon, name, size, color, hoverColor }: BaseButtonProps) {
|
||||
|
||||
@@ -29,66 +29,66 @@ export type Encoding = 'utf8' | 'base64' | 'ascii';
|
||||
|
||||
/** Native interface of the module */
|
||||
interface SafxInterface {
|
||||
openDocumentTree(persist: boolean): Promise<DocumentFileDetail | null>;
|
||||
openDocument(
|
||||
persist: boolean,
|
||||
multiple: boolean,
|
||||
): Promise<DocumentFileDetail[] | null>;
|
||||
createDocument(
|
||||
data: string,
|
||||
encoding?: string,
|
||||
initialName?: string,
|
||||
mimeType?: string,
|
||||
): Promise<DocumentFileDetail | null>;
|
||||
hasPermission(uriString: string): Promise<boolean>;
|
||||
exists(uriString: string): Promise<boolean>;
|
||||
readFile(uriString: string, encoding?: Encoding): Promise<string>;
|
||||
writeFile(
|
||||
uriString: string,
|
||||
data: string,
|
||||
encoding?: Encoding,
|
||||
mimeType?: string,
|
||||
append?: boolean,
|
||||
): Promise<void>;
|
||||
createFile(uriString: string, mimeType?: string): Promise<DocumentFileDetail>;
|
||||
unlink(uriString: string): Promise<boolean>;
|
||||
mkdir(uriString: string): Promise<DocumentFileDetail>;
|
||||
rename(uriString: string, newName: string): Promise<DocumentFileDetail>;
|
||||
getPersistedUriPermissions(): Promise<string[]>;
|
||||
releasePersistableUriPermission(uriString: string): Promise<void>;
|
||||
listFiles(uriString: string): Promise<DocumentFileDetail[]>;
|
||||
stat(uriString: string): Promise<DocumentFileDetail>;
|
||||
transferFile(
|
||||
srcUri: string,
|
||||
destUri: string,
|
||||
replaceIfDestExist: boolean,
|
||||
copy: boolean,
|
||||
): Promise<DocumentFileDetail | null>;
|
||||
openDocumentTree(persist: boolean): Promise<DocumentFileDetail | null>;
|
||||
openDocument(
|
||||
persist: boolean,
|
||||
multiple: boolean,
|
||||
): Promise<DocumentFileDetail[] | null>;
|
||||
createDocument(
|
||||
data: string,
|
||||
encoding?: string,
|
||||
initialName?: string,
|
||||
mimeType?: string,
|
||||
): Promise<DocumentFileDetail | null>;
|
||||
hasPermission(uriString: string): Promise<boolean>;
|
||||
exists(uriString: string): Promise<boolean>;
|
||||
readFile(uriString: string, encoding?: Encoding): Promise<string>;
|
||||
writeFile(
|
||||
uriString: string,
|
||||
data: string,
|
||||
encoding?: Encoding,
|
||||
mimeType?: string,
|
||||
append?: boolean,
|
||||
): Promise<void>;
|
||||
createFile(uriString: string, mimeType?: string): Promise<DocumentFileDetail>;
|
||||
unlink(uriString: string): Promise<boolean>;
|
||||
mkdir(uriString: string): Promise<DocumentFileDetail>;
|
||||
rename(uriString: string, newName: string): Promise<DocumentFileDetail>;
|
||||
getPersistedUriPermissions(): Promise<string[]>;
|
||||
releasePersistableUriPermission(uriString: string): Promise<void>;
|
||||
listFiles(uriString: string): Promise<DocumentFileDetail[]>;
|
||||
stat(uriString: string): Promise<DocumentFileDetail>;
|
||||
transferFile(
|
||||
srcUri: string,
|
||||
destUri: string,
|
||||
replaceIfDestExist: boolean,
|
||||
copy: boolean,
|
||||
): Promise<DocumentFileDetail | null>;
|
||||
}
|
||||
|
||||
export type DocumentFileDetail = {
|
||||
uri: string;
|
||||
name: string;
|
||||
type: 'directory' | 'file';
|
||||
lastModified: number;
|
||||
mime: string;
|
||||
size: number;
|
||||
uri: string;
|
||||
name: string;
|
||||
type: 'directory' | 'file';
|
||||
lastModified: number;
|
||||
mime: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type FileOperationOptions = {
|
||||
/** Defaults to `'utf8'` */
|
||||
encoding?: Encoding;
|
||||
/** Defaults to `'utf8'` */
|
||||
encoding?: Encoding;
|
||||
|
||||
/** Append data to the file. If not set file content will be overwritten. */
|
||||
append?: boolean;
|
||||
/** Append data to the file. If not set file content will be overwritten. */
|
||||
append?: boolean;
|
||||
|
||||
/** mime type of the file being saved. Defaults to '\*\/\*' */
|
||||
mimeType?: string;
|
||||
/** mime type of the file being saved. Defaults to '\*\/\*' */
|
||||
mimeType?: string;
|
||||
};
|
||||
|
||||
export type CreateDocumentOptions = FileOperationOptions & {
|
||||
/** initial display name when opening file picker */
|
||||
initialName?: string;
|
||||
/** initial display name when opening file picker */
|
||||
initialName?: string;
|
||||
};
|
||||
|
||||
// Open the Document Picker to select a folder. Read/Write Permission will be granted to the selected folder.
|
||||
@@ -98,10 +98,10 @@ export function openDocumentTree(persist: boolean) {
|
||||
}
|
||||
|
||||
export type OpenDocumentOptions = {
|
||||
/** should the permission of returned document(s) be persisted ? */
|
||||
persist?: boolean;
|
||||
/** should the file picker allow multiple documents ? */
|
||||
multiple?: boolean;
|
||||
/** should the permission of returned document(s) be persisted ? */
|
||||
persist?: boolean;
|
||||
/** should the file picker allow multiple documents ? */
|
||||
multiple?: boolean;
|
||||
};
|
||||
|
||||
// Open the Document Picker to select a file.
|
||||
@@ -206,7 +206,7 @@ export function stat(uriString: string) {
|
||||
}
|
||||
|
||||
type FileTransferOptions = {
|
||||
replaceIfDestinationExists?: boolean;
|
||||
replaceIfDestinationExists?: boolean;
|
||||
};
|
||||
|
||||
// Copy file from source uri to destination uri.
|
||||
|
||||
@@ -246,24 +246,9 @@ function math_block(state: any, start: number, end: number, silent: boolean) {
|
||||
|
||||
state.line = next + 1;
|
||||
|
||||
const contentLines = [];
|
||||
if (firstLine && firstLine.trim()) {
|
||||
contentLines.push(firstLine);
|
||||
}
|
||||
|
||||
const includeTrailingNewline = false;
|
||||
const interiorLines = state.getLines(start + 1, next, state.tShift[start], includeTrailingNewline);
|
||||
if (interiorLines.length > 0) {
|
||||
contentLines.push(interiorLines);
|
||||
}
|
||||
|
||||
if (lastLine && lastLine.trim()) {
|
||||
contentLines.push(lastLine);
|
||||
}
|
||||
|
||||
const token = state.push('math_block', 'math', 0);
|
||||
token.block = true;
|
||||
token.content = contentLines.join('\n');
|
||||
token.content = (firstLine && firstLine.trim() ? `${firstLine}\n` : '') + state.getLines(start + 1, next, state.tShift[start], true) + (lastLine && lastLine.trim() ? lastLine : '');
|
||||
token.map = [start, state.line];
|
||||
token.markup = '$$';
|
||||
return true;
|
||||
@@ -327,7 +312,6 @@ export default {
|
||||
} catch (error) {
|
||||
outputHtml = renderKatexError(latex, error);
|
||||
}
|
||||
|
||||
return `<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$$ " data-joplin-source-close=" $$ ">${markdownIt.utils.escapeHtml(latex)}</pre>${outputHtml}</div>`;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.12.1",
|
||||
"version": "2.12.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "yarn run build && JOPLIN_IS_TESTING=1 nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
|
||||
|
||||
@@ -18,9 +18,9 @@ require('pg').types.setTypeParser(20, (val: any) => {
|
||||
// Also need this to get integers for count() queries.
|
||||
// https://knexjs.org/#Builder-count
|
||||
declare module 'knex/types/result' {
|
||||
interface Registry {
|
||||
Count: number;
|
||||
}
|
||||
interface Registry {
|
||||
Count: number;
|
||||
}
|
||||
}
|
||||
|
||||
const logger = Logger.create('db');
|
||||
|
||||
@@ -12,7 +12,7 @@ export enum NotificationLevel {
|
||||
}
|
||||
|
||||
export enum ItemType {
|
||||
Item = 1,
|
||||
Item = 1,
|
||||
UserItem = 2,
|
||||
User,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { execCommand } from '@joplin/utils';
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
interface PluginAndVersion {
|
||||
[pluginId: string]: string;
|
||||
[pluginId: string]: string;
|
||||
}
|
||||
|
||||
interface PluginIdAndName {
|
||||
|
||||
@@ -6,16 +6,14 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: qx100 <ztymaxwell@gmail.com>\n"
|
||||
"Last-Translator: wh201906 <wh201906@yandex.com>\n"
|
||||
"Language-Team: zh_CN <737445366KG@Gmail.com>\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Poedit 3.3.2\n"
|
||||
"X-Generator: Poedit 3.2.2\n"
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.tsx:687
|
||||
msgid "- Camera: to allow taking a picture and attaching it to a note."
|
||||
@@ -126,8 +124,9 @@ msgstr "有 %d 条笔记匹配。是否删除?"
|
||||
|
||||
#: packages/app-desktop/gui/NoteListControls/NoteListControls.tsx:254
|
||||
#: packages/app-desktop/gui/NoteListControls/NoteListControls.tsx:263
|
||||
#, fuzzy
|
||||
msgid "%s"
|
||||
msgstr "%s"
|
||||
msgstr "(%s)"
|
||||
|
||||
#: packages/app-desktop/gui/utils/NoteListUtils.ts:61
|
||||
msgid "%s - Copy"
|
||||
@@ -275,8 +274,9 @@ msgid "Add body"
|
||||
msgstr "添加内容"
|
||||
|
||||
#: packages/app-mobile/components/ActionButton.tsx:59
|
||||
#, fuzzy
|
||||
msgid "Add new"
|
||||
msgstr "新增"
|
||||
msgstr "添加标题"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/setTags.ts:38
|
||||
msgid "Add or remove tags:"
|
||||
@@ -377,7 +377,7 @@ msgstr "确定要更新授权令牌吗?"
|
||||
msgid ""
|
||||
"Are you sure you want to return to the default layout? The current layout "
|
||||
"configuration will be lost."
|
||||
msgstr "确定要返回到默认布局吗?当前布局配置将丢失。"
|
||||
msgstr ""
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx:501
|
||||
msgid "Arguments:"
|
||||
@@ -488,8 +488,6 @@ msgid ""
|
||||
"unlock Joplin. If the device is on lockout, consider switching it off and on "
|
||||
"to reset biometrics scanning."
|
||||
msgstr ""
|
||||
"设备上未设置生物识别解锁。请设置它以解锁Joplin。如果设备处于锁定状态,请考虑"
|
||||
"将其关闭和打开以重置生物特征扫描。"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:33
|
||||
#: packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.tsx:132
|
||||
@@ -510,6 +508,7 @@ msgid "Bulleted List"
|
||||
msgstr "无序列表"
|
||||
|
||||
#: packages/server/src/routes/admin/users.ts:154
|
||||
#, fuzzy
|
||||
msgid "Can Share"
|
||||
msgstr "分享"
|
||||
|
||||
@@ -593,8 +592,9 @@ msgid "Cannot find \"%s\"."
|
||||
msgstr "无法找到 “%s”。"
|
||||
|
||||
#: packages/app-cli/app/command-mkbook.ts:28
|
||||
#, fuzzy
|
||||
msgid "Cannot find: \"%s\""
|
||||
msgstr "无法找到 “%s”"
|
||||
msgstr "无法找到 “%s”。"
|
||||
|
||||
#: packages/app-cli/app/command-sync.ts:164
|
||||
msgid "Cannot initialise synchroniser."
|
||||
@@ -730,8 +730,9 @@ msgid "Close"
|
||||
msgstr "关闭"
|
||||
|
||||
#: packages/app-mobile/components/Dropdown.tsx:156
|
||||
#, fuzzy
|
||||
msgid "Close dropdown"
|
||||
msgstr "关闭下拉列表"
|
||||
msgstr "关闭窗口"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.ts:24
|
||||
#: packages/app-desktop/gui/MenuBar.tsx:596
|
||||
@@ -859,8 +860,9 @@ msgid "Convert to todo"
|
||||
msgstr "转换为待办事项"
|
||||
|
||||
#: packages/app-mobile/components/voiceTyping/VoiceTypingDialog.tsx:88
|
||||
#, fuzzy
|
||||
msgid "Converting speech to text..."
|
||||
msgstr "正在将语音转换为文本..."
|
||||
msgstr "转换为笔记"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.tsx:495
|
||||
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:13
|
||||
@@ -968,16 +970,18 @@ msgid ""
|
||||
msgstr "无法验证此笔记的分享状态 — 正在终止。请在连接到互联网后再次尝试。"
|
||||
|
||||
#: packages/app-mobile/components/biometrics/biometricAuthenticate.ts:20
|
||||
#, fuzzy
|
||||
msgid "Could not verify your identify: %s"
|
||||
msgstr "无法验证您的身份: %s"
|
||||
msgstr "无法验证您的身份"
|
||||
|
||||
#: packages/app-desktop/gui/PromptDialog.tsx:276
|
||||
msgid "Create"
|
||||
msgstr "创建"
|
||||
|
||||
#: packages/app-cli/app/command-mkbook.ts:19
|
||||
#, fuzzy
|
||||
msgid "Create a new notebook under a parent notebook."
|
||||
msgstr "在父笔记本下创建新笔记本。"
|
||||
msgstr "新建笔记本。"
|
||||
|
||||
#: packages/app-mobile/components/NoteList.tsx:98
|
||||
msgid "Create a notebook"
|
||||
@@ -1006,15 +1010,15 @@ msgstr "创建日期"
|
||||
|
||||
#: packages/lib/Synchronizer.ts:182
|
||||
msgid "Created local items: %d."
|
||||
msgstr "已新建本地条目:%d。"
|
||||
msgstr "已新建本地项目:%d。"
|
||||
|
||||
#: packages/lib/services/ReportService.ts:246
|
||||
msgid "Created locally"
|
||||
msgstr "已新建本地条目"
|
||||
msgstr "已新建本地项目"
|
||||
|
||||
#: packages/lib/Synchronizer.ts:184
|
||||
msgid "Created remote items: %d."
|
||||
msgstr "已新建远程条目:%d。"
|
||||
msgstr "已新建远程项目:%d。"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:152
|
||||
msgid "Created: "
|
||||
@@ -1218,11 +1222,11 @@ msgstr "是否删除此配置文件?"
|
||||
|
||||
#: packages/lib/Synchronizer.ts:186
|
||||
msgid "Deleted local items: %d."
|
||||
msgstr "已删除本地条目:%d。"
|
||||
msgstr "已删除本地项目:%d。"
|
||||
|
||||
#: packages/lib/Synchronizer.ts:187
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr "已删除远程条目:%d。"
|
||||
msgstr "已删除远程项目:%d。"
|
||||
|
||||
#: packages/app-cli/app/command-rmbook.js:13
|
||||
msgid "Deletes the given notebook."
|
||||
@@ -1310,7 +1314,7 @@ msgid ""
|
||||
"for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the "
|
||||
"to-dos, while `-tnt` would display notes and to-dos."
|
||||
msgstr ""
|
||||
"仅显示指定类型的条目。可以把 `n` 用于笔记,`t` 用于待办事项,或者 `nt` 用于笔"
|
||||
"仅显示指定类型的项目。可以把 `n` 用于笔记,`t` 用于待办事项,或者 `nt` 用于笔"
|
||||
"记和待办事项(示例:`-tt` 只会显示待办事项,当使用 `-tnt` 时将会显示笔记和待"
|
||||
"办事项。)"
|
||||
|
||||
@@ -1388,8 +1392,9 @@ msgid "Downloading"
|
||||
msgstr "下载中"
|
||||
|
||||
#: packages/app-mobile/components/voiceTyping/VoiceTypingDialog.tsx:89
|
||||
#, fuzzy
|
||||
msgid "Downloading %s language files..."
|
||||
msgstr "下载%s语言文件中..."
|
||||
msgstr "下载资源中..."
|
||||
|
||||
#: packages/app-cli/app/command-sync.ts:218
|
||||
msgid "Downloading resources..."
|
||||
@@ -1552,7 +1557,7 @@ msgstr "启用音频播放器"
|
||||
|
||||
#: packages/app-mobile/components/biometrics/BiometricPopup.tsx:81
|
||||
msgid "Enable biometrics authentication?"
|
||||
msgstr "启用生物特征验证?"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/models/Setting.ts:1159
|
||||
msgid "Enable deflist syntax"
|
||||
@@ -1605,15 +1610,16 @@ msgstr "启用软换行"
|
||||
|
||||
#: packages/lib/models/Setting.ts:1050
|
||||
msgid "Enable spellcheck in the text editor"
|
||||
msgstr "在文本编辑器中启用拼写检查"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/models/Setting.ts:1156
|
||||
msgid "Enable table of contents extension"
|
||||
msgstr "启用目录扩展 ([TOC])"
|
||||
|
||||
#: packages/lib/models/Setting.ts:1061
|
||||
#, fuzzy
|
||||
msgid "Enable the Markdown toolbar"
|
||||
msgstr "启用“标记”工具栏"
|
||||
msgstr "启用 Markdown Emoji"
|
||||
|
||||
#: packages/lib/models/Setting.ts:1144
|
||||
msgid "Enable typographer support"
|
||||
@@ -1805,7 +1811,7 @@ msgstr "特性标志"
|
||||
|
||||
#: packages/lib/Synchronizer.ts:188
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr "已获取条目:%d/%d."
|
||||
msgstr "已获取项目:%d/%d."
|
||||
|
||||
#: packages/app-desktop/gui/Sidebar/Sidebar.tsx:754
|
||||
#: packages/app-mobile/components/side-menu-content.tsx:431
|
||||
@@ -1830,7 +1836,7 @@ msgstr "查找和替换"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/SearchPanel.tsx:250
|
||||
msgid "Find: "
|
||||
msgstr "查找: "
|
||||
msgstr "查找:"
|
||||
|
||||
#: packages/app-desktop/gui/ExtensionBadge.tsx:63
|
||||
msgid "Firefox Extension"
|
||||
@@ -1914,8 +1920,9 @@ msgid "Full changelog"
|
||||
msgstr "完整更新记录"
|
||||
|
||||
#: packages/server/src/routes/admin/users.ts:130
|
||||
#, fuzzy
|
||||
msgid "Full name"
|
||||
msgstr "完整名称"
|
||||
msgstr "完整更新记录"
|
||||
|
||||
#: packages/lib/models/Setting.ts:2510
|
||||
#: packages/server/src/services/MustacheService.ts:109
|
||||
@@ -1978,8 +1985,9 @@ msgid "Help"
|
||||
msgstr "帮助"
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.tsx:759
|
||||
#, fuzzy
|
||||
msgid "Hermes enabled: %d"
|
||||
msgstr "已启用Hermes:%d"
|
||||
msgstr "FTS 已开启:%d"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.tsx:567
|
||||
msgid "Hide %s"
|
||||
@@ -2097,7 +2105,7 @@ msgid ""
|
||||
"note or notebook. `$c` can be used to refer to the currently selected item."
|
||||
msgstr ""
|
||||
"在任何命令中,某个笔记或笔记本可通过它的名称或 ID 引用,也可使用代表当前所选"
|
||||
"笔记和笔记本的快捷变量 ‘$n' 和 '$b'。`$c` 可用于引用当前所选的条目。"
|
||||
"笔记和笔记本的快捷变量 ‘$n' 和 '$b'。`$c` 可用于引用当前所选的项目。"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note.tsx:413
|
||||
msgid ""
|
||||
@@ -2254,7 +2262,7 @@ msgstr "无法同步的条目"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.tsx:792
|
||||
msgid "Join us on Twitter"
|
||||
msgstr "在Twitter上加入我们"
|
||||
msgstr ""
|
||||
|
||||
#: packages/app-desktop/gui/SyncWizard/Dialog.tsx:330
|
||||
msgid ""
|
||||
@@ -2385,7 +2393,7 @@ msgstr "布局按钮序列"
|
||||
|
||||
#: packages/lib/models/Setting.ts:1704
|
||||
msgid "Leave it blank to download the language files from the default website"
|
||||
msgstr "留空可从默认网站下载语言文件"
|
||||
msgstr ""
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/leaveSharedFolder.ts:10
|
||||
msgid "Leave notebook..."
|
||||
@@ -2443,8 +2451,9 @@ msgid "Loaded"
|
||||
msgstr "已加载"
|
||||
|
||||
#: packages/app-mobile/components/voiceTyping/VoiceTypingDialog.tsx:86
|
||||
#, fuzzy
|
||||
msgid "Loading..."
|
||||
msgstr "正在加载…"
|
||||
msgstr "正在更新…"
|
||||
|
||||
#: packages/app-desktop/gui/NotePropertiesDialog.tsx:52
|
||||
msgid "Location"
|
||||
@@ -2575,9 +2584,10 @@ msgstr "最大条目数"
|
||||
|
||||
#: packages/lib/utils/joplinCloud.ts:117
|
||||
msgid "Max note or attachment size"
|
||||
msgstr "最大笔记或附件大小"
|
||||
msgstr "最大项目大小"
|
||||
|
||||
#: packages/server/src/routes/admin/users.ts:150
|
||||
#, fuzzy
|
||||
msgid "Max Total Size"
|
||||
msgstr "最大总大小"
|
||||
|
||||
@@ -2594,8 +2604,9 @@ msgid "Missing required argument: %s"
|
||||
msgstr "缺失必选参数:%s"
|
||||
|
||||
#: packages/app-cli/app/cli-utils.js:135
|
||||
#, fuzzy
|
||||
msgid "Missing required flag value: %s"
|
||||
msgstr "缺失必选标志值:%s"
|
||||
msgstr "缺失必选参数:%s"
|
||||
|
||||
#: packages/app-mobile/components/side-menu-content.tsx:452
|
||||
msgid "Mobile data - auto-sync disabled"
|
||||
@@ -2630,8 +2641,9 @@ msgid "Move to notebook..."
|
||||
msgstr "移动到笔记本..."
|
||||
|
||||
#: packages/app-cli/app/command-mv.js:14
|
||||
#, fuzzy
|
||||
msgid "Moves the given <item> to [notebook]"
|
||||
msgstr "将给定的<item>移动到[notebook]。"
|
||||
msgstr "移动符合 <note> 的笔记到 [notebook]。"
|
||||
|
||||
#: packages/app-cli/app/cli-utils.js:177
|
||||
#: packages/app-cli/app/setupCommand.ts:20
|
||||
@@ -2771,8 +2783,9 @@ msgid "Not generated"
|
||||
msgstr "未生成"
|
||||
|
||||
#: packages/app-mobile/components/biometrics/BiometricPopup.tsx:90
|
||||
#, fuzzy
|
||||
msgid "Not now"
|
||||
msgstr "不是现在"
|
||||
msgstr "立即执行"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.tsx:110
|
||||
#: packages/app-desktop/gui/NoteListControls/NoteListControls.tsx:119
|
||||
@@ -2807,6 +2820,7 @@ msgid "Note does not exist: \"%s\". Create it?"
|
||||
msgstr "笔记不存在:“%s” 。是否创建?"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/NoteEditor.tsx:83
|
||||
#, fuzzy
|
||||
msgid "Note editor"
|
||||
msgstr "笔记编辑器"
|
||||
|
||||
@@ -2925,7 +2939,7 @@ msgid ""
|
||||
"supplied the password, the encrypted items are being decrypted in the "
|
||||
"background and will be available soon."
|
||||
msgstr ""
|
||||
"一个或多个条目当前已加密,您可能需要提供主密码(master password)。若需进行此操"
|
||||
"一个或多个项目当前已加密,您可能需要提供主密码(master password)。若需进行此操"
|
||||
"作请输入 `e2ee decrypt` 。若您已提供密码,此时加密文件正在后台解密,解锁之后"
|
||||
"即可使用。"
|
||||
|
||||
@@ -3033,7 +3047,7 @@ msgstr "粘贴"
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/pasteAsText.ts:6
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:181
|
||||
msgid "Paste as text"
|
||||
msgstr "粘贴为文本"
|
||||
msgstr ""
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx:525
|
||||
msgid "Path:"
|
||||
@@ -3095,7 +3109,7 @@ msgstr ""
|
||||
|
||||
#: packages/app-mobile/components/voiceTyping/VoiceTypingDialog.tsx:87
|
||||
msgid "Please record your voice..."
|
||||
msgstr "请录下你的声音..."
|
||||
msgstr ""
|
||||
|
||||
#: packages/app-cli/app/command-ls.js:63
|
||||
msgid "Please select a notebook first."
|
||||
@@ -3208,8 +3222,9 @@ msgid "Privacy Policy"
|
||||
msgstr "隐私政策"
|
||||
|
||||
#: packages/lib/utils/joplinCloud.ts:316
|
||||
#, fuzzy
|
||||
msgid "Pro"
|
||||
msgstr "Pro"
|
||||
msgstr "配置文件"
|
||||
|
||||
#: packages/server/src/services/TaskService.ts:24
|
||||
msgid "Process failed payment subscriptions"
|
||||
@@ -3389,8 +3404,9 @@ msgid "Replace: "
|
||||
msgstr "替换: "
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/resetLayout.ts:7
|
||||
#, fuzzy
|
||||
msgid "Reset application layout"
|
||||
msgstr "重置应用布局"
|
||||
msgstr "更改应用布局"
|
||||
|
||||
#: packages/app-desktop/gui/MasterPasswordDialog/Dialog.tsx:219
|
||||
#: packages/app-desktop/gui/MasterPasswordDialog/Dialog.tsx:220
|
||||
@@ -3571,8 +3587,9 @@ msgid "Select file..."
|
||||
msgstr "选择文件..."
|
||||
|
||||
#: packages/app-mobile/components/screens/folder.js:109
|
||||
#, fuzzy
|
||||
msgid "Select parent notebook"
|
||||
msgstr "选择父笔记本"
|
||||
msgstr "新建一个笔记本"
|
||||
|
||||
#: packages/app-cli/app/command-server.js:38
|
||||
msgid "Server is already running on port %d"
|
||||
@@ -3649,6 +3666,7 @@ msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr "快捷键在 CLI 模式下不可用。"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/SearchPanel.tsx:208
|
||||
#, fuzzy
|
||||
msgid "Show advanced"
|
||||
msgstr "显示高级选项"
|
||||
|
||||
@@ -3665,6 +3683,7 @@ msgid "Show completed to-dos"
|
||||
msgstr "显示已完成待办事项"
|
||||
|
||||
#: packages/server/src/routes/admin/users.ts:200
|
||||
#, fuzzy
|
||||
msgid "Show disabled"
|
||||
msgstr "显示禁用的密钥"
|
||||
|
||||
@@ -3724,7 +3743,7 @@ msgstr "日光亮 (Solarised)"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.tsx:605
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr "某些条目无法被解密。"
|
||||
msgstr "某些项目无法被解密。"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.tsx:634
|
||||
msgid "Some items cannot be synchronised."
|
||||
@@ -3750,7 +3769,7 @@ msgstr "排序所选行"
|
||||
|
||||
#: packages/app-cli/app/command-ls.js:28
|
||||
msgid "Sorts the item by <field> (eg. title, updated_time, created_time)."
|
||||
msgstr "通过 <field> 排序条目(示例:title, updated_time, created_time)。"
|
||||
msgstr "通过 <field> 排序项目(示例:title, updated_time, created_time)。"
|
||||
|
||||
#: packages/app-cli/app/command-import.js:25
|
||||
msgid "Source format: %s"
|
||||
@@ -4022,6 +4041,7 @@ msgid "Take photo"
|
||||
msgstr "拍照"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.tsx:95
|
||||
#, fuzzy
|
||||
msgid "Task list"
|
||||
msgstr "任务列表"
|
||||
|
||||
@@ -4123,8 +4143,8 @@ msgid ""
|
||||
"item with a factor of 2 will take twice as much space as an item with a "
|
||||
"factor of 1.Restart app to see changes."
|
||||
msgstr ""
|
||||
"拉伸系数属性用于指定条目之间的容量比例。如,因子为 2 的条目所占容量是因子为 "
|
||||
"1 的条目的两倍。该更改在软件重启后生效。"
|
||||
"拉伸系数属性用于指定项目之间的容量比例。如,因子为 2 的项目所占容量是因子为 "
|
||||
"1 的项目的两倍。该更改在软件重启后生效。"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.tsx:524
|
||||
msgid "The following attachments are being watched for changes:"
|
||||
@@ -4155,8 +4175,8 @@ msgid ""
|
||||
"application does not currently have access to them. It is likely they will "
|
||||
"eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
"具有这些ID的密钥用于加密某些项目,但应用程序当前无法访问这些密钥。它们很可能"
|
||||
"最终会通过同步获取。"
|
||||
"具有这些 ID 的密钥正被用于加密您的某些项,但应用程序目前无法访问它们。项目最"
|
||||
"终会通过同步获取,但目前可能仍未被同步。"
|
||||
|
||||
#: packages/lib/components/EncryptionConfigScreen/utils.ts:217
|
||||
msgid "The master key has been upgraded successfully!"
|
||||
@@ -4239,7 +4259,7 @@ msgstr "网页剪藏未启用。"
|
||||
msgid ""
|
||||
"The WebDAV implementation of %s is incompatible with Joplin, and as such is "
|
||||
"no longer supported. Please use a different sync method."
|
||||
msgstr "%s的WebDAV工具与Joplin不兼容,因此不再受支持。请使用其他同步方法。"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/models/Setting.ts:829
|
||||
msgid "Theme"
|
||||
@@ -4444,7 +4464,7 @@ msgstr "按 Tab 键或 Shift+Tab 组合键切换面板。"
|
||||
#: packages/app-cli/app/command-status.js:44
|
||||
msgid ""
|
||||
"To retry decryption of these items. Run `e2ee decrypt --retry-failed-items`"
|
||||
msgstr "运行 `e2ee decrypt —retry-failed-items` 来尝试再次解密这些条目"
|
||||
msgstr "运行 `e2ee decrypt —retry-failed-items` 来尝试再次解密这些项目"
|
||||
|
||||
#: packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.tsx:61
|
||||
msgid ""
|
||||
@@ -4519,8 +4539,9 @@ msgid "Tools"
|
||||
msgstr "工具"
|
||||
|
||||
#: packages/server/src/routes/admin/users.ts:146
|
||||
#, fuzzy
|
||||
msgid "Total Size"
|
||||
msgstr "总大小"
|
||||
msgstr "实际大小"
|
||||
|
||||
#: packages/lib/services/ReportService.ts:284
|
||||
msgid "Total: %d/%d"
|
||||
@@ -4582,7 +4603,7 @@ msgstr "未知标记:%s"
|
||||
#: packages/lib/Synchronizer.ts:1096
|
||||
msgid ""
|
||||
"Unknown item type downloaded - please upgrade Joplin to the latest version"
|
||||
msgstr "已下载条目为未知类型,请将 Joplin 升级至最新版本"
|
||||
msgstr "已下载项目为未知类型,请将 Joplin 升级至最新版本"
|
||||
|
||||
#: packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.tsx:69
|
||||
msgid "Unordered list"
|
||||
@@ -4686,7 +4707,7 @@ msgstr "使用情况:%s"
|
||||
|
||||
#: packages/lib/models/Setting.ts:1659
|
||||
msgid "Use biometrics to secure access to the app"
|
||||
msgstr "使用生物识别技术确保对应用程序的访问安全"
|
||||
msgstr ""
|
||||
|
||||
#: packages/app-cli/app/command-ls.js:32 packages/app-cli/app/command-tag.js:18
|
||||
msgid ""
|
||||
@@ -4722,7 +4743,6 @@ msgid ""
|
||||
"Use your biometrics to secure access to your application. You can always set "
|
||||
"it up later in Settings."
|
||||
msgstr ""
|
||||
"使用您的生物特征来确保对应用程序的访问安全。您可以稍后在“设置”中进行设置。"
|
||||
|
||||
#: packages/lib/models/Setting.ts:1257
|
||||
msgid ""
|
||||
@@ -4784,12 +4804,12 @@ msgstr "Vim"
|
||||
|
||||
#: packages/lib/models/Setting.ts:1705
|
||||
msgid "Voice typing language files (URL)"
|
||||
msgstr "语音输入语言文件(URL)"
|
||||
msgstr ""
|
||||
|
||||
#: packages/app-mobile/components/screens/Note.tsx:994
|
||||
#: packages/app-mobile/components/voiceTyping/VoiceTypingDialog.tsx:119
|
||||
msgid "Voice typing..."
|
||||
msgstr "语音输入中..."
|
||||
msgstr ""
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.tsx:83
|
||||
msgid "Warning"
|
||||
@@ -4841,7 +4861,7 @@ msgstr ""
|
||||
|
||||
#: packages/lib/WelcomeUtils.ts:63
|
||||
msgid "Welcome!"
|
||||
msgstr "欢迎!"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/models/Setting.ts:1105
|
||||
msgid "When creating a new note:"
|
||||
@@ -4904,7 +4924,7 @@ msgstr ""
|
||||
|
||||
#: packages/app-cli/app/cli-utils.js:160
|
||||
msgid "Your choice: "
|
||||
msgstr "您的选择: "
|
||||
msgstr "您的选择:"
|
||||
|
||||
#: packages/lib/components/EncryptionConfigScreen/utils.ts:70
|
||||
msgid "Your data is going to be re-encrypted and synced again."
|
||||
|
||||
@@ -334,23 +334,7 @@ export function githubOauthToken() {
|
||||
// says that nothing has changed on the API, although it used to work. So since
|
||||
// we can't use /latest anymore, we need to fetch all the releases to find the
|
||||
// latest published one.
|
||||
//
|
||||
// As of July 2023 /latest seems to be working again, so switching back to this
|
||||
// method, but let's keep the old method just in case they break the API again.
|
||||
export async function gitHubLatestRelease(repoName: string): Promise<GitHubRelease> {
|
||||
const response: any = await fetch(`https://api.github.com/repos/laurent22/${repoName}/releases/latest`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Joplin Readme Updater',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Cannot fetch releases: ${response.statusText}`);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function gitHubLatestRelease_KeepInCaseMicrosoftBreaksTheApiAgain(repoName: string): Promise<GitHubRelease> {
|
||||
let pageNum = 1;
|
||||
|
||||
while (true) {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { GitHubRelease } from './tool-utils';
|
||||
import { downloadUrl, OS } from './update-readme-download';
|
||||
|
||||
describe('update-readme-download', () => {
|
||||
|
||||
it('convert download URLs', async () => {
|
||||
const createRelease = (assetName: string, browserDownloadUrl: string) => {
|
||||
const r: GitHubRelease = {
|
||||
assets: [
|
||||
{
|
||||
browser_download_url: browserDownloadUrl,
|
||||
name: assetName,
|
||||
},
|
||||
],
|
||||
tag_name: '',
|
||||
upload_url: '',
|
||||
html_url: '',
|
||||
prerelease: false,
|
||||
draft: false,
|
||||
};
|
||||
return r;
|
||||
};
|
||||
|
||||
const testCases: [GitHubRelease, OS, boolean, string][] = [
|
||||
[
|
||||
createRelease('joplin-v2.9.8.apk', 'https://github.com/laurent22/joplin-android/releases/download/android-v2.9.8/joplin-v2.9.8.apk'),
|
||||
OS.Android,
|
||||
false,
|
||||
'https://objects.joplinusercontent.com/v2.9.8/joplin-v2.9.8.apk',
|
||||
],
|
||||
[
|
||||
createRelease('Joplin-Setup-2.11.11.exe', 'https://github.com/laurent22/joplin/releases/download/v2.11.11/Joplin-Setup-2.11.11.exe'),
|
||||
OS.Windows,
|
||||
false,
|
||||
'https://objects.joplinusercontent.com/v2.11.11/Joplin-Setup-2.11.11.exe',
|
||||
],
|
||||
[
|
||||
createRelease('JoplinPortable.exe', 'https://github.com/laurent22/joplin/releases/download/v2.11.11/JoplinPortable.exe'),
|
||||
OS.Windows,
|
||||
true,
|
||||
'https://objects.joplinusercontent.com/v2.11.11/JoplinPortable.exe',
|
||||
],
|
||||
];
|
||||
|
||||
for (const [release, os, portable, expected] of testCases) {
|
||||
const actual = downloadUrl(release, os, portable);
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -11,15 +11,7 @@ async function msleep(ms: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export enum OS {
|
||||
MacOs = 'macos',
|
||||
Windows = 'windows',
|
||||
Android = 'android',
|
||||
Android32 = 'android32',
|
||||
Linux = 'linux',
|
||||
}
|
||||
|
||||
export const downloadUrl = (release: GitHubRelease, os: OS, portable = false) => {
|
||||
function downloadUrl(release: GitHubRelease, os: string, portable = false) {
|
||||
if (!release || !release.assets || !release.assets.length) return null;
|
||||
|
||||
for (let i = 0; i < release.assets.length; i++) {
|
||||
@@ -27,13 +19,13 @@ export const downloadUrl = (release: GitHubRelease, os: OS, portable = false) =>
|
||||
const name = asset.name;
|
||||
const ext = fileExtension(name);
|
||||
|
||||
const githubAndroidUrl = 'github.com/laurent22/joplin-android/releases/download/android-';
|
||||
const githubUrl = 'github.com/laurent22/joplin/releases/download/';
|
||||
const joplinDomain = 'objects.joplinusercontent.com/';
|
||||
const githubAndroidUrl = 'github.com/laurent22/joplin-android/releases/download';
|
||||
const githubUrl = 'github.com/laurent22/joplin/releases/download';
|
||||
const joplinDomain = 'objects.joplinusercontent.com';
|
||||
|
||||
if (ext === 'dmg' && os === OS.MacOs) return asset.browser_download_url.replace(githubUrl, joplinDomain);
|
||||
if (ext === 'dmg' && os === 'macos') return asset.browser_download_url.replace(githubUrl, joplinDomain);
|
||||
|
||||
if (ext === 'exe' && os === OS.Windows) {
|
||||
if (ext === 'exe' && os === 'windows') {
|
||||
if (portable) {
|
||||
if (name === 'JoplinPortable.exe') return asset.browser_download_url.replace(githubUrl, joplinDomain);
|
||||
} else {
|
||||
@@ -41,15 +33,15 @@ export const downloadUrl = (release: GitHubRelease, os: OS, portable = false) =>
|
||||
}
|
||||
}
|
||||
|
||||
if (ext === 'AppImage' && os === OS.Linux) return asset.browser_download_url.replace(githubUrl, joplinDomain);
|
||||
if (ext === 'AppImage' && os === 'linux') return asset.browser_download_url.replace(githubUrl, joplinDomain);
|
||||
|
||||
if (os === OS.Android32 && name.endsWith('32bit.apk')) return asset.browser_download_url.replace(githubAndroidUrl, joplinDomain);
|
||||
if (os === 'android32' && name.endsWith('32bit.apk')) return asset.browser_download_url.replace(githubAndroidUrl, joplinDomain);
|
||||
|
||||
if (os === OS.Android && ext === 'apk' && !name.endsWith('32bit.apk')) return asset.browser_download_url.replace(githubAndroidUrl, joplinDomain);
|
||||
if (os === 'android' && ext === 'apk' && !name.endsWith('32bit.apk')) return asset.browser_download_url.replace(githubAndroidUrl, joplinDomain);
|
||||
}
|
||||
|
||||
throw new Error(`Could not find download URL for: ${os}`);
|
||||
};
|
||||
}
|
||||
|
||||
function readmeContent() {
|
||||
if (!fs.existsSync(readmePath)) throw new Error(`Cannot find ${readmePath}`);
|
||||
@@ -81,19 +73,19 @@ async function main(argv: any) {
|
||||
|
||||
const androidRelease = await gitHubLatestRelease('joplin-android');
|
||||
|
||||
// const android32Url = downloadUrl(androidRelease, OS.Android32);
|
||||
const androidUrl = downloadUrl(androidRelease, OS.Android);
|
||||
const winUrl = downloadUrl(release, OS.Windows);
|
||||
const winPortableUrl = downloadUrl(release, OS.Windows, true);
|
||||
const macOsUrl = downloadUrl(release, OS.MacOs);
|
||||
const linuxUrl = downloadUrl(release, OS.Linux);
|
||||
const android32Url = downloadUrl(androidRelease, 'android32');
|
||||
const androidUrl = downloadUrl(androidRelease, 'android');
|
||||
const winUrl = downloadUrl(release, 'windows');
|
||||
const winPortableUrl = downloadUrl(release, 'windows', true);
|
||||
const macOsUrl = downloadUrl(release, 'macos');
|
||||
const linuxUrl = downloadUrl(release, 'linux');
|
||||
|
||||
console.info('Windows: ', winUrl);
|
||||
console.info('Windows Portable: ', winPortableUrl);
|
||||
console.info('macOS: ', macOsUrl);
|
||||
console.info('Linux: ', linuxUrl);
|
||||
console.info('Android: ', androidUrl);
|
||||
// console.info('Android 32: ', android32Url);
|
||||
console.info('Android 32: ', android32Url);
|
||||
|
||||
let content = readmeContent();
|
||||
|
||||
@@ -102,17 +94,18 @@ async function main(argv: any) {
|
||||
if (macOsUrl) content = content.replace(/(https:\/\/objects.joplinusercontent.com\/v\d+\.\d+\.\d+\/Joplin-.*?\.dmg)/, macOsUrl);
|
||||
if (linuxUrl) content = content.replace(/(https:\/\/objects.joplinusercontent.com\/v\d+\.\d+\.\d+\/Joplin-.*?\.AppImage)/, linuxUrl);
|
||||
|
||||
if (androidUrl) content = content.replace(/(https:\/\/objects.joplinusercontent.com\/v\d+\.\d+\.\d+\/joplin-v\d+\.\d+\.\d+\.apk)/, androidUrl);
|
||||
// if (android32Url) content = content.replace(/(https:\/\/objects.joplinusercontent.com\/v\d+\.\d+\.\d+\/joplin-v\d+\.\d+\.\d+-32bit\.apk)/, android32Url);
|
||||
// Disable for now due to broken /latest API end point, which returns a
|
||||
// version from 6 months ago.
|
||||
|
||||
if (androidUrl) content = content.replace(/(https:\/\/objects.joplinusercontent.com\/android-v\d+\.\d+\.\d+\/joplin-v\d+\.\d+\.\d+\.apk)/, androidUrl);
|
||||
if (android32Url) content = content.replace(/(https:\/\/objects.joplinusercontent.com\/android-v\d+\.\d+\.\d+\/joplin-v\d+\.\d+\.\d+-32bit\.apk)/, android32Url);
|
||||
|
||||
setReadmeContent(content);
|
||||
|
||||
// console.info("git pull && git add -A && git commit -m 'Update readme downloads' && git push")
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
main(process.argv).catch((error) => {
|
||||
console.error('Fatal error');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
main(process.argv).catch((error) => {
|
||||
console.error('Fatal error', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"devDependencies": {
|
||||
"browserify": "14.5.0",
|
||||
"rollup": "0.50.1",
|
||||
"standard": "17.1.0",
|
||||
"standard": "17.0.0",
|
||||
"turndown": "7.1.2",
|
||||
"turndown-attendant": "0.0.3"
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"rollup-plugin-commonjs": "8.4.1",
|
||||
"rollup-plugin-node-resolve": "3.4.0",
|
||||
"rollup-plugin-replace": "2.2.0",
|
||||
"standard": "17.1.0",
|
||||
"standard": "17.0.0",
|
||||
"turndown-attendant": "0.0.3"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
# Joplin changelog
|
||||
|
||||
## [v2.12.5](https://github.com/laurent22/joplin/releases/tag/v2.12.5) (Pre-release) - 2023-07-12T15:03:46Z
|
||||
|
||||
- Improved: Allow importing Evernote task lists ([#8440](https://github.com/laurent22/joplin/issues/8440) by Rob Moffat)
|
||||
- Improved: Prevent app from crashing when changing screens ([4c5837b](https://github.com/laurent22/joplin/commit/4c5837b))
|
||||
- Improved: Quickly synchronise immediately after changing a note ([004d2a1](https://github.com/laurent22/joplin/commit/004d2a1))
|
||||
- Improved: Updated packages standard (v17.1.0)
|
||||
- Fixed: Don't start window minimized in GNOME ([#8441](https://github.com/laurent22/joplin/issues/8441)) ([#8256](https://github.com/laurent22/joplin/issues/8256) by Henry Heino)
|
||||
- Fixed: Fix extra lines added to KaTeX source when toggling the rich text editor ([#8442](https://github.com/laurent22/joplin/issues/8442)) ([#6822](https://github.com/laurent22/joplin/issues/6822) by Henry Heino)
|
||||
- Fixed: Fixed macOS version check so that it does not return the ARM64 version ([1abfb1c](https://github.com/laurent22/joplin/commit/1abfb1c))
|
||||
|
||||
## [v2.12.4](https://github.com/laurent22/joplin/releases/tag/v2.12.4) (Pre-release) - 2023-07-07T22:36:53Z
|
||||
|
||||
|
||||
|
||||
## [v2.12.3](https://github.com/laurent22/joplin/releases/tag/v2.12.3) (Pre-release) - 2023-07-07T10:16:55Z
|
||||
|
||||
- Improved: Build macOS app as Universal binary ([#8423](https://github.com/laurent22/joplin/issues/8423)) ([#6052](https://github.com/laurent22/joplin/issues/6052))
|
||||
- Improved: Updated packages @electron/remote (v2.0.10), @rmp135/sql-ts (v1.17.0), clean-html (v2), glob (v10.2.7)
|
||||
- Fixed: Removing Markdown when using 'Paste as Text' ([#8351](https://github.com/laurent22/joplin/issues/8351)) ([#8317](https://github.com/laurent22/joplin/issues/8317) by [@pedr](https://github.com/pedr))
|
||||
|
||||
## [v2.11.11](https://github.com/laurent22/joplin/releases/tag/v2.11.11) - 2023-06-23T15:16:37Z
|
||||
|
||||
- Improved: Add support for plugin user data ([#8312](https://github.com/laurent22/joplin/issues/8312)) ([#8080](https://github.com/laurent22/joplin/issues/8080))
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
# Joplin Server Changelog
|
||||
|
||||
## [server-v2.12.1](https://github.com/laurent22/joplin/releases/tag/server-v2.12.1) - 2023-07-13T10:23:24Z
|
||||
|
||||
- New: Add yarn patches to Docker image (e91e38c)
|
||||
- Improved: Updated packages @rmp135/sql-ts (v1.17.0), clean-html (v2), dayjs (v1.11.8), glob (v10.2.7), jquery (v3.7.0), pg (v8.11.0), standard (v17.1.0)
|
||||
|
||||
## [server-v2.11.2](https://github.com/laurent22/joplin/releases/tag/server-v2.11.2) - 2023-06-20T08:40:54Z
|
||||
|
||||
- Improved: Allow giving a different version number to forks (091eff9)
|
||||
|
||||
@@ -591,30 +591,6 @@
|
||||
"created_at": "2023-07-03T13:24:27Z",
|
||||
"repoId": 79162682,
|
||||
"pullRequestNo": 8376
|
||||
},
|
||||
{
|
||||
"name": "qx100",
|
||||
"id": 25811098,
|
||||
"comment_id": 1627704105,
|
||||
"created_at": "2023-07-09T12:37:18Z",
|
||||
"repoId": 79162682,
|
||||
"pullRequestNo": 8437
|
||||
},
|
||||
{
|
||||
"name": "robmoffat",
|
||||
"id": 568673,
|
||||
"comment_id": 1628525382,
|
||||
"created_at": "2023-07-10T08:52:17Z",
|
||||
"repoId": 79162682,
|
||||
"pullRequestNo": 8440
|
||||
},
|
||||
{
|
||||
"name": "noah-nash",
|
||||
"id": 48302704,
|
||||
"comment_id": 1631642401,
|
||||
"created_at": "2023-07-11T23:36:37Z",
|
||||
"repoId": 79162682,
|
||||
"pullRequestNo": 8452
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
tweet: What's new in Joplin 2.11
|
||||
forum_url: https://discourse.joplinapp.org/t/31674
|
||||
---
|
||||
|
||||
# What's new in Joplin 2.11
|
||||
|
||||
## Desktop
|
||||
|
||||
### Add support for plugin user data
|
||||
|
||||
Developers of plugins now have the ability to associate additional information with notes, notebooks, and tags. This data is then synchronized across multiple devices. For instance, it is possible to envision attaching OCR data to an image or incorporating plugin-specific parameters to a note, which can subsequently be synchronized across all devices.
|
||||
|
||||
### Improved end-to-end encryption
|
||||
|
||||
End-to-end encryption (E2EE) plays a vital role in safeguarding your data privacy. Our aim is to make it effortlessly user-friendly, seamlessly integrated, and possibly enabled by default.
|
||||
|
||||
To achieve this objective, we have implemented several enhancements in this version. These improvements encompass more streamlined management of encryption keys and simplified setup processes for both new and existing devices utilising end-to-end encryption.
|
||||
|
||||
Moreover, we have upgraded the encryption algorithm from AES-128 to AES-256, ensuring heightened security for your data in the long run.
|
||||
|
||||
### Export a Mermaid graph
|
||||
|
||||
There is now a context menu on Mermaid graphs which you can use to export your graph as PNG, SVG, or by copying the vector data to the clipboard.
|
||||
|
||||

|
||||
|
||||
### Auto-detect language on startup
|
||||
|
||||
A small but useful change - the application language is now automatically detected startup. Of course, if needed you can later change it in the configuration screen.
|
||||
|
||||
## Android
|
||||
|
||||
As always most of these changes also apply to the mobile applications, such as the improved end-to-end encryption support, language auto-detection, and various bug fixes and improvements. But besides those, we also have several mobile-specific updates:
|
||||
|
||||
### Support for offline voice typing
|
||||
|
||||
The Android mobile app now support offline voice typing - it means your voice recording does not need to be uploaded to Google or Samsung server - it all takes place on your device for added security. To make use of it, open any note, tap on the three dots menu, then "Voice typing".
|
||||
|
||||

|
||||
|
||||
The first time, the app will need to download the language files to your device:
|
||||
|
||||

|
||||
|
||||
Once they are there, simply record your message and it will appear in the note:
|
||||
|
||||

|
||||
|
||||
### Create and move sub-notebooks
|
||||
|
||||
@jcgurango implemented support for creating sub-notebooks, and moving notebooks around in the mobile app. To do so, long press on any notebook and press "Edit". You can now choose to move it under any other notebook using the provided dropdown list.
|
||||
|
||||
## Full changelog
|
||||
|
||||
This is just an overview of the main features. The full changelog is available there:
|
||||
|
||||
- Desktop: https://joplinapp.org/changelog
|
||||
- Android: https://joplinapp.org/changelog_android
|
||||
- iOS: https://joplinapp.org/changelog_ios
|
||||
Reference in New Issue
Block a user