Compare commits
169 Commits
custom_fol
...
multi_prof
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6cd988939 | ||
|
|
a99eec7cdf | ||
|
|
7ef09dfa77 | ||
|
|
b11573a2a7 | ||
|
|
f4034b1ff0 | ||
|
|
58bf93a112 | ||
|
|
5962b0813e | ||
|
|
cffea3ea1e | ||
|
|
dfadacd7f4 | ||
|
|
ecc7b17708 | ||
|
|
ee6ab55649 | ||
|
|
b0d64e2f51 | ||
|
|
f6e21e0180 | ||
|
|
e02422070e | ||
|
|
727d64b646 | ||
|
|
23e54a60d9 | ||
|
|
0d4978223e | ||
|
|
0b32a29cce | ||
|
|
f322d40910 | ||
|
|
557cb9a6c3 | ||
|
|
d5a55c7908 | ||
|
|
7308bbd3ca | ||
|
|
c1e8f9befd | ||
|
|
a0d77d10ba | ||
|
|
bdd9c6cf35 | ||
|
|
f2bfa30e04 | ||
|
|
8077117e65 | ||
|
|
7e8927398a | ||
|
|
09dcee876c | ||
|
|
23b56f4f70 | ||
|
|
b3d09ce776 | ||
|
|
84d40b805e | ||
|
|
c097a82b7b | ||
|
|
dfa22b560e | ||
|
|
7d31a3fe90 | ||
|
|
79aabc2d06 | ||
|
|
70e82ca64f | ||
|
|
a0662412b2 | ||
|
|
220b48ef02 | ||
|
|
cb637e817b | ||
|
|
27198a16a4 | ||
|
|
1a5bff3bf4 | ||
|
|
571147acbb | ||
|
|
9d9420a35c | ||
|
|
a79bc69604 | ||
|
|
3153d3a1b6 | ||
|
|
df8c265ee4 | ||
|
|
3725b14e04 | ||
|
|
a73d822998 | ||
|
|
a62e1fba96 | ||
|
|
37d51c3b58 | ||
|
|
d5dfecc19f | ||
|
|
8f8cc12d79 | ||
|
|
8e1802409f | ||
|
|
42b2f2146c | ||
|
|
2ba1563d92 | ||
|
|
a679b21119 | ||
|
|
dd10b6ac65 | ||
|
|
32600df7ce | ||
|
|
8a1cfabfc6 | ||
|
|
1b2046f2fa | ||
|
|
a1d5168918 | ||
|
|
047c1fb1a5 | ||
|
|
e83d662555 | ||
|
|
5c3b2671bf | ||
|
|
02a2dce605 | ||
|
|
c4e158d0fb | ||
|
|
fa8a1c2122 | ||
|
|
3f732939d0 | ||
|
|
fb8886db4b | ||
|
|
eb86e9c896 | ||
|
|
85e3a44276 | ||
|
|
addcfb0129 | ||
|
|
f4ec73ab0e | ||
|
|
19ba939f0f | ||
|
|
780b58ada4 | ||
|
|
b3aed81bea | ||
|
|
74dd2d1194 | ||
|
|
3c11445db0 | ||
|
|
50890a7b2b | ||
|
|
dc60da219a | ||
|
|
bef93e1375 | ||
|
|
45e02a6b3f | ||
|
|
0c5f64207f | ||
|
|
8e88686bb1 | ||
|
|
9594508c73 | ||
|
|
de7d3b6b8a | ||
|
|
f5b6398d07 | ||
|
|
62a5cf520a | ||
|
|
90bfffed0f | ||
|
|
a5b0d594c3 | ||
|
|
1547256af4 | ||
|
|
c3e7fcb471 | ||
|
|
261302d5c4 | ||
|
|
38e65253ad | ||
|
|
eb8d0daa3d | ||
|
|
5a6c03fbac | ||
|
|
21f9dab908 | ||
|
|
6dba4730b0 | ||
|
|
9599d4ef7e | ||
|
|
252f08e828 | ||
|
|
94dc216add | ||
|
|
32de63fad3 | ||
|
|
299a14755a | ||
|
|
053dbabf74 | ||
|
|
07f128ae95 | ||
|
|
98f9ed641c | ||
|
|
d814fd6965 | ||
|
|
d44aade075 | ||
|
|
4884c9ef87 | ||
|
|
3e518ff2c7 | ||
|
|
195c84ff59 | ||
|
|
6881259501 | ||
|
|
6d7f6e29bd | ||
|
|
dc991d4c0f | ||
|
|
88ac664e37 | ||
|
|
20784b0e99 | ||
|
|
a325bf6dc6 | ||
|
|
ff79745d28 | ||
|
|
82109a3132 | ||
|
|
4e901436cc | ||
|
|
f0113c0673 | ||
|
|
0a1947a712 | ||
|
|
39aee65eee | ||
|
|
d37b820bd3 | ||
|
|
06b95bb718 | ||
|
|
8e87e64dea | ||
|
|
422a5bfa91 | ||
|
|
bfe5ee8ba3 | ||
|
|
00e0f6b97c | ||
|
|
46490113a2 | ||
|
|
cc69cabf9b | ||
|
|
da88ddb6bd | ||
|
|
554fd73054 | ||
|
|
c48a0c6e79 | ||
|
|
f8b7db40c7 | ||
|
|
051eac09ec | ||
|
|
7584b203fc | ||
|
|
440618ef71 | ||
|
|
166d8da81b | ||
|
|
6b7fd24f9e | ||
|
|
41dd93bc24 | ||
|
|
4906087649 | ||
|
|
fe95926fa2 | ||
|
|
a0fcd240b0 | ||
|
|
ae3a4a3027 | ||
|
|
5252fdc8ce | ||
|
|
fe787d1257 | ||
|
|
a70f9b1a13 | ||
|
|
ed20604ad2 | ||
|
|
105a61c1ee | ||
|
|
38fbaa9acf | ||
|
|
0d0231e82c | ||
|
|
b79270990b | ||
|
|
064891d097 | ||
|
|
9036bc4fd1 | ||
|
|
0f41a2d00a | ||
|
|
3ee0c7f440 | ||
|
|
89ada6432b | ||
|
|
445d691103 | ||
|
|
de757026d4 | ||
|
|
5e8ed8bc24 | ||
|
|
0a739e099d | ||
|
|
f3b1f07a75 | ||
|
|
1b91646d92 | ||
|
|
885f0e1557 | ||
|
|
2933b7eb2a | ||
|
|
9f252ea673 | ||
|
|
db497ee0a5 |
@@ -148,6 +148,9 @@ packages/app-desktop/checkForUpdates.js.map
|
||||
packages/app-desktop/commands/copyDevCommand.d.ts
|
||||
packages/app-desktop/commands/copyDevCommand.js
|
||||
packages/app-desktop/commands/copyDevCommand.js.map
|
||||
packages/app-desktop/commands/editProfileConfig.d.ts
|
||||
packages/app-desktop/commands/editProfileConfig.js
|
||||
packages/app-desktop/commands/editProfileConfig.js.map
|
||||
packages/app-desktop/commands/exportFolders.d.ts
|
||||
packages/app-desktop/commands/exportFolders.js
|
||||
packages/app-desktop/commands/exportFolders.js.map
|
||||
@@ -175,6 +178,18 @@ packages/app-desktop/commands/startExternalEditing.js.map
|
||||
packages/app-desktop/commands/stopExternalEditing.d.ts
|
||||
packages/app-desktop/commands/stopExternalEditing.js
|
||||
packages/app-desktop/commands/stopExternalEditing.js.map
|
||||
packages/app-desktop/commands/switchProfile.d.ts
|
||||
packages/app-desktop/commands/switchProfile.js
|
||||
packages/app-desktop/commands/switchProfile.js.map
|
||||
packages/app-desktop/commands/switchProfile1.d.ts
|
||||
packages/app-desktop/commands/switchProfile1.js
|
||||
packages/app-desktop/commands/switchProfile1.js.map
|
||||
packages/app-desktop/commands/switchProfile2.d.ts
|
||||
packages/app-desktop/commands/switchProfile2.js
|
||||
packages/app-desktop/commands/switchProfile2.js.map
|
||||
packages/app-desktop/commands/switchProfile3.d.ts
|
||||
packages/app-desktop/commands/switchProfile3.js
|
||||
packages/app-desktop/commands/switchProfile3.js.map
|
||||
packages/app-desktop/commands/toggleExternalEditing.d.ts
|
||||
packages/app-desktop/commands/toggleExternalEditing.js
|
||||
packages/app-desktop/commands/toggleExternalEditing.js.map
|
||||
@@ -235,6 +250,9 @@ packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
|
||||
packages/app-desktop/gui/ErrorBoundary.d.ts
|
||||
packages/app-desktop/gui/ErrorBoundary.js
|
||||
packages/app-desktop/gui/ErrorBoundary.js.map
|
||||
packages/app-desktop/gui/FolderIconBox.d.ts
|
||||
packages/app-desktop/gui/FolderIconBox.js
|
||||
packages/app-desktop/gui/FolderIconBox.js.map
|
||||
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.d.ts
|
||||
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js.map
|
||||
@@ -256,6 +274,9 @@ packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js.map
|
||||
packages/app-desktop/gui/MainScreen/MainScreen.d.ts
|
||||
packages/app-desktop/gui/MainScreen/MainScreen.js
|
||||
packages/app-desktop/gui/MainScreen/MainScreen.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/addProfile.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/addProfile.js
|
||||
packages/app-desktop/gui/MainScreen/commands/addProfile.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
|
||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js.map
|
||||
@@ -487,6 +508,12 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js.map
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js.map
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js.map
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js.map
|
||||
packages/app-desktop/gui/NoteEditor/utils/index.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/index.js.map
|
||||
@@ -721,6 +748,9 @@ packages/app-desktop/gui/utils/convertToScreenCoordinates.js.map
|
||||
packages/app-desktop/gui/utils/loadScript.d.ts
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/gui/utils/loadScript.js.map
|
||||
packages/app-desktop/loadResources.testEnv.d.ts
|
||||
packages/app-desktop/loadResources.testEnv.js
|
||||
packages/app-desktop/loadResources.testEnv.js.map
|
||||
packages/app-desktop/plugins/GotoAnything.d.ts
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/plugins/GotoAnything.js.map
|
||||
@@ -1147,6 +1177,9 @@ packages/lib/models/NoteTag.js.map
|
||||
packages/lib/models/Resource.d.ts
|
||||
packages/lib/models/Resource.js
|
||||
packages/lib/models/Resource.js.map
|
||||
packages/lib/models/Resource.test.d.ts
|
||||
packages/lib/models/Resource.test.js
|
||||
packages/lib/models/Resource.test.js.map
|
||||
packages/lib/models/ResourceLocalState.d.ts
|
||||
packages/lib/models/ResourceLocalState.js
|
||||
packages/lib/models/ResourceLocalState.js.map
|
||||
@@ -1231,6 +1264,9 @@ packages/lib/services/DecryptionWorker.js.map
|
||||
packages/lib/services/ExternalEditWatcher.d.ts
|
||||
packages/lib/services/ExternalEditWatcher.js
|
||||
packages/lib/services/ExternalEditWatcher.js.map
|
||||
packages/lib/services/ExternalEditWatcher/utils.d.ts
|
||||
packages/lib/services/ExternalEditWatcher/utils.js
|
||||
packages/lib/services/ExternalEditWatcher/utils.js.map
|
||||
packages/lib/services/ItemChangeUtils.d.ts
|
||||
packages/lib/services/ItemChangeUtils.js
|
||||
packages/lib/services/ItemChangeUtils.js.map
|
||||
@@ -1555,6 +1591,24 @@ packages/lib/services/plugins/utils/validatePluginVersion.js.map
|
||||
packages/lib/services/plugins/utils/validatePluginVersion.test.d.ts
|
||||
packages/lib/services/plugins/utils/validatePluginVersion.test.js
|
||||
packages/lib/services/plugins/utils/validatePluginVersion.test.js.map
|
||||
packages/lib/services/profileConfig/index.d.ts
|
||||
packages/lib/services/profileConfig/index.js
|
||||
packages/lib/services/profileConfig/index.js.map
|
||||
packages/lib/services/profileConfig/index.test.d.ts
|
||||
packages/lib/services/profileConfig/index.test.js
|
||||
packages/lib/services/profileConfig/index.test.js.map
|
||||
packages/lib/services/profileConfig/initProfile.d.ts
|
||||
packages/lib/services/profileConfig/initProfile.js
|
||||
packages/lib/services/profileConfig/initProfile.js.map
|
||||
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.d.ts
|
||||
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js
|
||||
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js.map
|
||||
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.d.ts
|
||||
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js
|
||||
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js.map
|
||||
packages/lib/services/profileConfig/types.d.ts
|
||||
packages/lib/services/profileConfig/types.js
|
||||
packages/lib/services/profileConfig/types.js.map
|
||||
packages/lib/services/rest/Api.d.ts
|
||||
packages/lib/services/rest/Api.js
|
||||
packages/lib/services/rest/Api.js.map
|
||||
@@ -1633,6 +1687,9 @@ packages/lib/services/searchengine/SearchEngineUtils.js.map
|
||||
packages/lib/services/searchengine/SearchEngineUtils.test.d.ts
|
||||
packages/lib/services/searchengine/SearchEngineUtils.test.js
|
||||
packages/lib/services/searchengine/SearchEngineUtils.test.js.map
|
||||
packages/lib/services/searchengine/SearchFilter.test.d.ts
|
||||
packages/lib/services/searchengine/SearchFilter.test.js
|
||||
packages/lib/services/searchengine/SearchFilter.test.js.map
|
||||
packages/lib/services/searchengine/filterParser.d.ts
|
||||
packages/lib/services/searchengine/filterParser.js
|
||||
packages/lib/services/searchengine/filterParser.js.map
|
||||
@@ -1960,6 +2017,9 @@ packages/tools/generate-images.js.map
|
||||
packages/tools/git-changelog.d.ts
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/git-changelog.js.map
|
||||
packages/tools/licenseChecker.d.ts
|
||||
packages/tools/licenseChecker.js
|
||||
packages/tools/licenseChecker.js.map
|
||||
packages/tools/release-android.d.ts
|
||||
packages/tools/release-android.js
|
||||
packages/tools/release-android.js.map
|
||||
@@ -2002,9 +2062,21 @@ packages/tools/website/build.js.map
|
||||
packages/tools/website/updateDownloadPage.d.ts
|
||||
packages/tools/website/updateDownloadPage.js
|
||||
packages/tools/website/updateDownloadPage.js.map
|
||||
packages/tools/website/updateNews.d.ts
|
||||
packages/tools/website/updateNews.js
|
||||
packages/tools/website/updateNews.js.map
|
||||
packages/tools/website/utils/frontMatter.d.ts
|
||||
packages/tools/website/utils/frontMatter.js
|
||||
packages/tools/website/utils/frontMatter.js.map
|
||||
packages/tools/website/utils/openGraph.d.ts
|
||||
packages/tools/website/utils/openGraph.js
|
||||
packages/tools/website/utils/openGraph.js.map
|
||||
packages/tools/website/utils/openGraph.test.d.ts
|
||||
packages/tools/website/utils/openGraph.test.js
|
||||
packages/tools/website/utils/openGraph.test.js.map
|
||||
packages/tools/website/utils/parser.d.ts
|
||||
packages/tools/website/utils/parser.js
|
||||
packages/tools/website/utils/parser.js.map
|
||||
packages/tools/website/utils/pressCarousel.d.ts
|
||||
packages/tools/website/utils/pressCarousel.js
|
||||
packages/tools/website/utils/pressCarousel.js.map
|
||||
|
||||
6
.github/scripts/run_ci.sh
vendored
@@ -38,6 +38,8 @@ echo "GITHUB_REF=$GITHUB_REF"
|
||||
echo "RUNNER_OS=$RUNNER_OS"
|
||||
echo "GIT_TAG_NAME=$GIT_TAG_NAME"
|
||||
echo "BUILD_SEQUENCIAL=$BUILD_SEQUENCIAL"
|
||||
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
|
||||
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
|
||||
|
||||
echo "IS_CONTINUOUS_INTEGRATION=$IS_CONTINUOUS_INTEGRATION"
|
||||
echo "IS_PULL_REQUEST=$IS_PULL_REQUEST"
|
||||
@@ -169,10 +171,10 @@ cd "$ROOT_DIR/packages/app-desktop"
|
||||
if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
echo "Step: Building and publishing desktop application..."
|
||||
USE_HARD_LINKS=false yarn run dist
|
||||
elif [[ $GIT_TAG_NAME = server-v* ]] && [[ $IS_LINUX = 1 ]]; then
|
||||
elif [[ $IS_LINUX = 1 ]] && [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then
|
||||
echo "Step: Building Docker Image..."
|
||||
cd "$ROOT_DIR"
|
||||
yarn run buildServerDocker --tag-name $GIT_TAG_NAME --push-images
|
||||
yarn run buildServerDocker --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
|
||||
else
|
||||
echo "Step: Building but *not* publishing desktop application..."
|
||||
USE_HARD_LINKS=false yarn run dist --publish=never
|
||||
|
||||
7
.github/workflows/github-actions-main.yml
vendored
@@ -5,7 +5,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-2016]
|
||||
# Removed windows-2016 for now - discontinued by GitHub
|
||||
os: [macos-latest, ubuntu-latest]
|
||||
steps:
|
||||
|
||||
# Silence apt-get update errors (for example when a module doesn't
|
||||
@@ -98,6 +99,8 @@ jobs:
|
||||
env:
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
SERVER_REPOSITORY: joplin/server
|
||||
SERVER_TAG_PREFIX: server
|
||||
run: |
|
||||
yarn install && cd packages/app-desktop && yarn run dist --publish=never
|
||||
|
||||
@@ -138,5 +141,5 @@ jobs:
|
||||
BUILD_SEQUENCIAL: 1
|
||||
run: |
|
||||
yarn install
|
||||
yarn run buildServerDocker --tag-name server-v0.0.0
|
||||
yarn run buildServerDocker --tag-name server-v0.0.0 --repository joplin/server
|
||||
|
||||
72
.gitignore
vendored
@@ -138,6 +138,9 @@ packages/app-desktop/checkForUpdates.js.map
|
||||
packages/app-desktop/commands/copyDevCommand.d.ts
|
||||
packages/app-desktop/commands/copyDevCommand.js
|
||||
packages/app-desktop/commands/copyDevCommand.js.map
|
||||
packages/app-desktop/commands/editProfileConfig.d.ts
|
||||
packages/app-desktop/commands/editProfileConfig.js
|
||||
packages/app-desktop/commands/editProfileConfig.js.map
|
||||
packages/app-desktop/commands/exportFolders.d.ts
|
||||
packages/app-desktop/commands/exportFolders.js
|
||||
packages/app-desktop/commands/exportFolders.js.map
|
||||
@@ -165,6 +168,18 @@ packages/app-desktop/commands/startExternalEditing.js.map
|
||||
packages/app-desktop/commands/stopExternalEditing.d.ts
|
||||
packages/app-desktop/commands/stopExternalEditing.js
|
||||
packages/app-desktop/commands/stopExternalEditing.js.map
|
||||
packages/app-desktop/commands/switchProfile.d.ts
|
||||
packages/app-desktop/commands/switchProfile.js
|
||||
packages/app-desktop/commands/switchProfile.js.map
|
||||
packages/app-desktop/commands/switchProfile1.d.ts
|
||||
packages/app-desktop/commands/switchProfile1.js
|
||||
packages/app-desktop/commands/switchProfile1.js.map
|
||||
packages/app-desktop/commands/switchProfile2.d.ts
|
||||
packages/app-desktop/commands/switchProfile2.js
|
||||
packages/app-desktop/commands/switchProfile2.js.map
|
||||
packages/app-desktop/commands/switchProfile3.d.ts
|
||||
packages/app-desktop/commands/switchProfile3.js
|
||||
packages/app-desktop/commands/switchProfile3.js.map
|
||||
packages/app-desktop/commands/toggleExternalEditing.d.ts
|
||||
packages/app-desktop/commands/toggleExternalEditing.js
|
||||
packages/app-desktop/commands/toggleExternalEditing.js.map
|
||||
@@ -225,6 +240,9 @@ packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
|
||||
packages/app-desktop/gui/ErrorBoundary.d.ts
|
||||
packages/app-desktop/gui/ErrorBoundary.js
|
||||
packages/app-desktop/gui/ErrorBoundary.js.map
|
||||
packages/app-desktop/gui/FolderIconBox.d.ts
|
||||
packages/app-desktop/gui/FolderIconBox.js
|
||||
packages/app-desktop/gui/FolderIconBox.js.map
|
||||
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.d.ts
|
||||
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js.map
|
||||
@@ -246,6 +264,9 @@ packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js.map
|
||||
packages/app-desktop/gui/MainScreen/MainScreen.d.ts
|
||||
packages/app-desktop/gui/MainScreen/MainScreen.js
|
||||
packages/app-desktop/gui/MainScreen/MainScreen.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/addProfile.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/addProfile.js
|
||||
packages/app-desktop/gui/MainScreen/commands/addProfile.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
|
||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js.map
|
||||
@@ -477,6 +498,12 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js.map
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js.map
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.test.js.map
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js.map
|
||||
packages/app-desktop/gui/NoteEditor/utils/index.d.ts
|
||||
packages/app-desktop/gui/NoteEditor/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/index.js.map
|
||||
@@ -711,6 +738,9 @@ packages/app-desktop/gui/utils/convertToScreenCoordinates.js.map
|
||||
packages/app-desktop/gui/utils/loadScript.d.ts
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/gui/utils/loadScript.js.map
|
||||
packages/app-desktop/loadResources.testEnv.d.ts
|
||||
packages/app-desktop/loadResources.testEnv.js
|
||||
packages/app-desktop/loadResources.testEnv.js.map
|
||||
packages/app-desktop/plugins/GotoAnything.d.ts
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/plugins/GotoAnything.js.map
|
||||
@@ -1137,6 +1167,9 @@ packages/lib/models/NoteTag.js.map
|
||||
packages/lib/models/Resource.d.ts
|
||||
packages/lib/models/Resource.js
|
||||
packages/lib/models/Resource.js.map
|
||||
packages/lib/models/Resource.test.d.ts
|
||||
packages/lib/models/Resource.test.js
|
||||
packages/lib/models/Resource.test.js.map
|
||||
packages/lib/models/ResourceLocalState.d.ts
|
||||
packages/lib/models/ResourceLocalState.js
|
||||
packages/lib/models/ResourceLocalState.js.map
|
||||
@@ -1221,6 +1254,9 @@ packages/lib/services/DecryptionWorker.js.map
|
||||
packages/lib/services/ExternalEditWatcher.d.ts
|
||||
packages/lib/services/ExternalEditWatcher.js
|
||||
packages/lib/services/ExternalEditWatcher.js.map
|
||||
packages/lib/services/ExternalEditWatcher/utils.d.ts
|
||||
packages/lib/services/ExternalEditWatcher/utils.js
|
||||
packages/lib/services/ExternalEditWatcher/utils.js.map
|
||||
packages/lib/services/ItemChangeUtils.d.ts
|
||||
packages/lib/services/ItemChangeUtils.js
|
||||
packages/lib/services/ItemChangeUtils.js.map
|
||||
@@ -1545,6 +1581,24 @@ packages/lib/services/plugins/utils/validatePluginVersion.js.map
|
||||
packages/lib/services/plugins/utils/validatePluginVersion.test.d.ts
|
||||
packages/lib/services/plugins/utils/validatePluginVersion.test.js
|
||||
packages/lib/services/plugins/utils/validatePluginVersion.test.js.map
|
||||
packages/lib/services/profileConfig/index.d.ts
|
||||
packages/lib/services/profileConfig/index.js
|
||||
packages/lib/services/profileConfig/index.js.map
|
||||
packages/lib/services/profileConfig/index.test.d.ts
|
||||
packages/lib/services/profileConfig/index.test.js
|
||||
packages/lib/services/profileConfig/index.test.js.map
|
||||
packages/lib/services/profileConfig/initProfile.d.ts
|
||||
packages/lib/services/profileConfig/initProfile.js
|
||||
packages/lib/services/profileConfig/initProfile.js.map
|
||||
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.d.ts
|
||||
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js
|
||||
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js.map
|
||||
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.d.ts
|
||||
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js
|
||||
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js.map
|
||||
packages/lib/services/profileConfig/types.d.ts
|
||||
packages/lib/services/profileConfig/types.js
|
||||
packages/lib/services/profileConfig/types.js.map
|
||||
packages/lib/services/rest/Api.d.ts
|
||||
packages/lib/services/rest/Api.js
|
||||
packages/lib/services/rest/Api.js.map
|
||||
@@ -1623,6 +1677,9 @@ packages/lib/services/searchengine/SearchEngineUtils.js.map
|
||||
packages/lib/services/searchengine/SearchEngineUtils.test.d.ts
|
||||
packages/lib/services/searchengine/SearchEngineUtils.test.js
|
||||
packages/lib/services/searchengine/SearchEngineUtils.test.js.map
|
||||
packages/lib/services/searchengine/SearchFilter.test.d.ts
|
||||
packages/lib/services/searchengine/SearchFilter.test.js
|
||||
packages/lib/services/searchengine/SearchFilter.test.js.map
|
||||
packages/lib/services/searchengine/filterParser.d.ts
|
||||
packages/lib/services/searchengine/filterParser.js
|
||||
packages/lib/services/searchengine/filterParser.js.map
|
||||
@@ -1950,6 +2007,9 @@ packages/tools/generate-images.js.map
|
||||
packages/tools/git-changelog.d.ts
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/git-changelog.js.map
|
||||
packages/tools/licenseChecker.d.ts
|
||||
packages/tools/licenseChecker.js
|
||||
packages/tools/licenseChecker.js.map
|
||||
packages/tools/release-android.d.ts
|
||||
packages/tools/release-android.js
|
||||
packages/tools/release-android.js.map
|
||||
@@ -1992,9 +2052,21 @@ packages/tools/website/build.js.map
|
||||
packages/tools/website/updateDownloadPage.d.ts
|
||||
packages/tools/website/updateDownloadPage.js
|
||||
packages/tools/website/updateDownloadPage.js.map
|
||||
packages/tools/website/updateNews.d.ts
|
||||
packages/tools/website/updateNews.js
|
||||
packages/tools/website/updateNews.js.map
|
||||
packages/tools/website/utils/frontMatter.d.ts
|
||||
packages/tools/website/utils/frontMatter.js
|
||||
packages/tools/website/utils/frontMatter.js.map
|
||||
packages/tools/website/utils/openGraph.d.ts
|
||||
packages/tools/website/utils/openGraph.js
|
||||
packages/tools/website/utils/openGraph.js.map
|
||||
packages/tools/website/utils/openGraph.test.d.ts
|
||||
packages/tools/website/utils/openGraph.test.js
|
||||
packages/tools/website/utils/openGraph.test.js.map
|
||||
packages/tools/website/utils/parser.d.ts
|
||||
packages/tools/website/utils/parser.js
|
||||
packages/tools/website/utils/parser.js.map
|
||||
packages/tools/website/utils/pressCarousel.d.ts
|
||||
packages/tools/website/utils/pressCarousel.js
|
||||
packages/tools/website/utils/pressCarousel.js.map
|
||||
|
||||
@@ -307,7 +307,7 @@ p,
|
||||
div.navbar-mobile-content a.sponsor-button {
|
||||
padding: 4px 12px;
|
||||
font-size: 0.9em;
|
||||
margin-right: 1em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
#nav-section.white-bg a {
|
||||
@@ -670,8 +670,8 @@ footer .bottom-links-row p {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.news-page img,
|
||||
.news-item-page img {
|
||||
.news-page .main-content img,
|
||||
.news-item-page .main-content img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -740,7 +740,7 @@ footer .bottom-links-row p {
|
||||
}
|
||||
|
||||
#nav-section .button-link {
|
||||
padding: 4px 12px;
|
||||
padding: 4px 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
@@ -937,6 +937,25 @@ footer .bottom-links-row p {
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
VERY NARROW VIEW
|
||||
eg for Galaxy Fold
|
||||
*****************************************************************/
|
||||
|
||||
@media (max-width: 350px) {
|
||||
|
||||
#nav-section .navbar-mobile-content a.sponsor-button {
|
||||
background-color: transparent;
|
||||
color: #0557ba !important;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#nav-section .navbar-mobile-content a.sponsor-button .sponsor-button-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
PLANS PAGE
|
||||
*****************************************************************/
|
||||
@@ -1054,6 +1073,10 @@ footer .bottom-links-row p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.joplin-cloud-feature-list table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price-row .plan-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
BIN
Assets/WebsiteAssets/images/news/20220224-edit-dialog.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
Assets/WebsiteAssets/images/news/20220224-edit-notebook.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
Assets/WebsiteAssets/images/news/20220224-notebook-icon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
Assets/WebsiteAssets/images/news/20220308-gsoc-banner.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
@@ -10,8 +10,8 @@
|
||||
<link rel="icon" href="{{imageBaseUrl}}/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Joplin, the open source note-taking application" />
|
||||
<link rel="stylesheet" href="{{{assetUrls.css.fontawesome}}}">
|
||||
{{> openGraphTags}}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{cssBaseUrl}}/bootstrap5.0.2.min.css"
|
||||
|
||||
@@ -23,7 +23,7 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
|
||||
<link rel="icon" href="{{imageBaseUrl}}/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Joplin, the open source note-taking application" />
|
||||
{{> openGraphTags}}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{cssBaseUrl}}/bootstrap5.0.2.min.css"
|
||||
@@ -65,11 +65,6 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
|
||||
{{{contentHtml}}}
|
||||
{{#showBottomLinks}}
|
||||
<div class="bottom-links">
|
||||
{{#discussOnForumLink}}
|
||||
<a class="bottom-link" href="{{{discussOnForumLink}}}">
|
||||
<i class="fab fa-discourse"></i></i> Discuss on the forum
|
||||
</a>
|
||||
{{/discussOnForumLink}}
|
||||
{{#showImproveThisDoc}}
|
||||
<a class="bottom-link" href="https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}">
|
||||
<i class="fab fa-github"></i> Improve this doc
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-9 text-right d-none d-md-block">
|
||||
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500"><i class="fab fa-twitter"></i></a>
|
||||
{{> twitterLink}}
|
||||
<a href="{{baseUrl}}/news/" class="fw500">News</a>
|
||||
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
|
||||
<a href="{{forumUrl}}" class="fw500">Forum</a>
|
||||
@@ -22,6 +22,7 @@
|
||||
{{> supportButton}}
|
||||
</div>
|
||||
<div class="col-9 text-right d-block d-md-none navbar-mobile-content">
|
||||
{{> twitterLink}}
|
||||
{{> supportButton}}
|
||||
|
||||
<span class="pointer"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{{#openGraph}}
|
||||
<meta name="description" content="{{openGraph.description}}" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@joplinapp" />
|
||||
<meta property="og:url" content="{{{openGraph.url}}}" />
|
||||
<meta property="og:title" content="{{openGraph.title}}" />
|
||||
<meta property="twitter:title" content="{{openGraph.title}}" />
|
||||
<meta property="og:description" content="{{openGraph.description}}" />
|
||||
<meta property="twitter:description" content="{{openGraph.description}}" />
|
||||
{{#openGraph.image}}
|
||||
<meta property="og:image" content="{{{openGraph.image}}}" />
|
||||
<meta property="twitter:image" content="{{{openGraph.image}}}" />
|
||||
{{/openGraph.image}}
|
||||
{{/openGraph}}
|
||||
@@ -6,11 +6,11 @@
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-monthly">
|
||||
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month"> /month</sub>
|
||||
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month"> /month{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-yearly">
|
||||
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month"> /month</sub>
|
||||
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month"> /month{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,17 +20,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#featuresOn}}
|
||||
{{#featureLabelsOn}}
|
||||
<p><i class="fas fa-check feature feature-on"></i>{{.}}</p>
|
||||
{{/featuresOn}}
|
||||
{{/featureLabelsOn}}
|
||||
|
||||
{{#featuresOff}}
|
||||
{{#featureLabelsOff}}
|
||||
<p class="unchecked-text"><i class="fas fa-times feature feature-off"></i>{{.}}</p>
|
||||
{{/featuresOff}}
|
||||
{{/featureLabelsOff}}
|
||||
|
||||
<p class="text-center subscribe-wrapper">
|
||||
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton">{{cfaLabel}}</a>
|
||||
</p>
|
||||
|
||||
{{#footnote}}<sub>(*) {{.}}</sub>{{/footnote}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<a class="button-link btn-blue sponsor-button" href="{{baseUrl}}/donate">
|
||||
<i class="fas fa-heart heart-full"></i><i class="far fa-heart heart-line"></i> Support us
|
||||
<i class="fas fa-heart heart-full"></i><i class="far fa-heart heart-line"></i><span class="sponsor-button-label"> Support us</span>
|
||||
</a>
|
||||
@@ -0,0 +1 @@
|
||||
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500"><i class="fab fa-twitter"></i></a>
|
||||
@@ -42,13 +42,23 @@
|
||||
{{> plan}}
|
||||
{{/plans.pro}}
|
||||
|
||||
{{#plans.business}}
|
||||
{{#plans.teams}}
|
||||
{{> plan}}
|
||||
{{/plans.business}}
|
||||
{{/plans.teams}}
|
||||
|
||||
<p class="joplin-cloud-login-info">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<h1>Feature comparison</h1>
|
||||
<div class="joplin-cloud-feature-list">
|
||||
{{{featureListHtml}}}
|
||||
</div>
|
||||
<p> </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{{{faqHtml}}}
|
||||
</div>
|
||||
|
||||
2
BUILD.md
@@ -25,6 +25,8 @@ There are also a few forks of existing packages under the "fork-*" name.
|
||||
|
||||
## Building
|
||||
|
||||
Make sure the path to the project directory does not contain spaces or the build may fail.
|
||||
|
||||
Before doing anything else, from the root of the project, run:
|
||||
|
||||
yarn install
|
||||
|
||||
@@ -38,7 +38,8 @@ If you want to start contributing to the project's code, please follow these gui
|
||||
- **Changes that will consist of more than 50 lines of code should be discussed on the [Joplin Forum](https://discourse.joplinapp.org/)**, so that you don't spend too much time implementing something that might not be accepted.
|
||||
- All the applications share the same backend (database, synchronisation, settings, models, business logic, etc.) so if you change something in the backend in one app, make sure it still works in the other apps. Usually it does, but keep this in mind.
|
||||
- Pull requests that make many changes using an automated tool, like for spell fixing, styling, etc. will not be accepted. An exception would be if the changes have been discussed in the forum and someone has agreed to review **and test** the pull request.
|
||||
- Pull requests that make address multiple issues will most likely stall and eventually be closed. This is because we might be fine with one of the changes but not with others and untangling that kind of pull request is too much hassle both for maintainers and the person who submitted it. So most of the time someone gives up and the PR gets closed. So please keep the pull request focused on one issue.
|
||||
- Pull requests that address multiple issues will most likely stall and eventually be closed. This is because we might be fine with one of the changes but not with others and untangling that kind of pull request is too much hassle both for maintainers and the person who submitted it. So most of the time someone gives up and the PR gets closed. So please keep the pull request focused on one issue.
|
||||
- **Do not mark your reviewer's comments as "resolved"**. If you do that, the comments will be hidden and the reviewer will not know what are the pending issues in the pull request. Only the reviewer should resolve the comments.
|
||||
|
||||
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/dev/BUILD.md) for more details.
|
||||
|
||||
@@ -52,37 +53,57 @@ For changes made to the Desktop client that affect the user interface, refer to
|
||||
|
||||
## Automated tests
|
||||
|
||||
When submitting a pull request for a new feature or a bug fix, please add automated tests for your code whenever possible. Tests in Joplin are divided into **unit tests** and **feature tests**.
|
||||
When submitting a pull request for a new feature or a bug fix, please add automated tests. We use [Jest](https://jestjs.io/) as a testing framework so you will need to be familiar with it or go through their documentation.
|
||||
|
||||
* **Unit tests** are used to test models, services or utility classes - they are relatively low level. Unit tests should be prefixed with the type of class that is being tested - for example "models_Folder" or "services_SearchEngine".
|
||||
### Running the tests
|
||||
|
||||
* **Feature tests** on the other hand are to test higher level functionalities such as interactions with the GUI and how they affect the underlying model. Often these tests would dispatch Redux actions, and inspect how the application state has been changed. The feature tests should be prefixed with "feature_", for example "feature_TagList". There's a good explanation on what qualifies as a feature test in [this post](https://github.com/laurent22/joplin/pull/2819#issuecomment-603502230).
|
||||
|
||||
The tests are under packages/app-cli/tests. To get them running, you first need to build the CLI app:
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
cd packages/app-cli
|
||||
```
|
||||
|
||||
To run all the test units:
|
||||
To run all the test units, run from the root:
|
||||
|
||||
```sh
|
||||
yarn test
|
||||
```
|
||||
|
||||
Or you can go inside a package folder, and run the tests from there. For example to run all the library tests, go in `packages/lib` and run `yarn test`
|
||||
|
||||
To run just one particular file:
|
||||
|
||||
```sh
|
||||
yarn test --filter=markdownUtils # Don't add the .js extension
|
||||
# Run all the tests in markdownUtils.test.ts
|
||||
yarn test markdownUtils
|
||||
```
|
||||
|
||||
To filter tests. For example, to run all the test units that contain "should handle conflict" in their description:
|
||||
To run only a particular test in a file:
|
||||
|
||||
```sh
|
||||
yarn test --filter="should handle conflict"
|
||||
# Run only the test described as "should handle conflict"
|
||||
# inside markdownUtils.test.ts:
|
||||
yarn test markdownUtils --filter="should handle conflict"
|
||||
```
|
||||
|
||||
### Adding a new test file
|
||||
|
||||
To add a test, simply create a new file with an extension `.test.ts` in the same directory. For example if you are working on the file `example.ts`, create a file `example.test.ts` for the unit tests. If this file already exist, simply add your tests directly to it.
|
||||
|
||||
### Setting the testing environment
|
||||
|
||||
Many utility functions are available under the package `@joplin/lib/testing/test-utils`. Have a look for example at [Note.test.ts](https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Note.test.ts) to see how to setup test units with database support and synchroniser support. Note that this is not needed for all tests - if you just have a simple functions to test you won't need that extra setup.
|
||||
|
||||
### Testing React Hooks
|
||||
|
||||
To test React Hooks please use the package `@testing-library/react-hooks`. See [useLayoutItemSizes.test.ts](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/gui/ResizableLayout/utils/useLayoutItemSizes.test.ts) for an example.
|
||||
|
||||
### If it is not possible to add tests
|
||||
|
||||
More often than not, it is actually possible to add tests - just go back to your code and see if it can be refactored and certain functionalities moved to simple functions (with no dependencies). Once you have a simple function, you can easily add unit tests for it.
|
||||
|
||||
Additionally, if the unit tests are not sufficient, please provide a **manual testing plan**, which should include detailed steps on:
|
||||
|
||||
- How to test that your feature is working. Include at least 5 tests. Try to think of the possible inputs - if it's a list, how does it work with 0 elements, or 1, or 10, or 100,000. If it's a text input, how does it work with an empty string, or a very large string, etc. Basically don't just put one test that check the best case scenario.
|
||||
|
||||
- How to verify that related parts of the applications are not broken. For example if you changed the note loading logic, check that the toolbar is still working as expected (and not modifying the previously loaded note for example), check that switching from one note to another still works. Look at the note list and verify that the note title is updated there too, etc.
|
||||
|
||||
A reviewer should be able to run the app with your changes, then do the above steps to verify that everything's working as expected.
|
||||
|
||||
## About abandoned pull requests
|
||||
|
||||
It happens that a pull request is started but not finished and despite our attempts to contact the contributor, we don't hear from them again.
|
||||
|
||||
@@ -6,13 +6,9 @@ FROM node:16-bullseye AS builder
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
python \
|
||||
python tini \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Download the init tool Tini and make it executable for use in the final image
|
||||
ADD https://github.com/krallin/tini/releases/download/v0.19.0/tini-static /tini
|
||||
RUN chmod u+x /tini
|
||||
|
||||
# Enables Yarn
|
||||
RUN corepack enable
|
||||
|
||||
@@ -63,7 +59,7 @@ RUN useradd --create-home --shell /bin/bash $user
|
||||
USER $user
|
||||
|
||||
COPY --chown=$user:$user --from=builder /build/packages /home/$user/packages
|
||||
COPY --chown=$user:$user --from=builder /tini /usr/local/bin/tini
|
||||
COPY --chown=$user:$user --from=builder /usr/bin/tini /usr/local/bin/tini
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV RUNNING_IN_DOCKER=1
|
||||
|
||||
86
README.md
@@ -4,13 +4,19 @@
|
||||
|
||||
* * *
|
||||
|
||||
<img width="64" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png" align="left" /> **Joplin**® is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](#markdown).
|
||||
🌞 Joplin participates in **Google Summer of Code 2022**! More info on [the announcement post](https://github.com/laurent22/joplin/blob/dev/readme/news/20220308-gsoc2022-start.md). 🌞
|
||||
|
||||
Notes exported from Evernote via .enex files [can be imported](#importing) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
|
||||
* * *
|
||||
|
||||
The notes can be [synchronised](#synchronisation) with various cloud services including [Nextcloud](https://nextcloud.com/), Dropbox, OneDrive, WebDAV or the file system (for example with a network directory). When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
|
||||
<img width="64" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png" align="left" /> **Joplin** is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](#markdown).
|
||||
|
||||
The application is available for Windows, Linux, macOS, Android and iOS (the terminal app also works on FreeBSD). A [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
|
||||
Notes exported from Evernote [can be imported](#importing) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
|
||||
|
||||
The notes can be securely [synchronised](#synchronisation) using [end-to-end encryption](#encryption) with various cloud services including Nextcloud, Dropbox, OneDrive and [Joplin Cloud](https://joplinapp.org/plans/).
|
||||
|
||||
Full text search is available on all platforms to quickly find the information you need. The app can be customised using plugins and themes, and you can also easily create your own.
|
||||
|
||||
The application is available for Windows, Linux, macOS, Android and iOS. A [Web Clipper](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md), to save web pages and screenshots from your browser, is also available for [Firefox](https://addons.mozilla.org/firefox/addon/joplin-web-clipper/) and [Chrome](https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB).
|
||||
|
||||
<div class="top-screenshot"><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/home-top-img.png" style="max-width: 100%; max-height: 35em;"></div>
|
||||
|
||||
@@ -22,11 +28,11 @@ Three types of applications are available: for **desktop** (Windows, macOS and L
|
||||
|
||||
Operating System | Download
|
||||
---|---
|
||||
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v2.6.10/Joplin-Setup-2.6.10.exe'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v2.6.10/Joplin-2.6.10.dmg'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v2.6.10/Joplin-2.6.10.AppImage'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
|
||||
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v2.7.15/Joplin-Setup-2.7.15.exe'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v2.7.15/Joplin-2.7.15.dmg'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v2.7.15/Joplin-2.7.15.AppImage'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
|
||||
|
||||
**On Windows**, you may also use the <a href='https://github.com/laurent22/joplin/releases/download/v2.6.10/JoplinPortable.exe'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
|
||||
**On Windows**, you may also use the <a href='https://github.com/laurent22/joplin/releases/download/v2.7.15/JoplinPortable.exe'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
|
||||
|
||||
**On Linux**, the recommended way is to use the following installation script as it will handle the desktop icon too:
|
||||
|
||||
@@ -36,7 +42,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/v2.6.10/J
|
||||
|
||||
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: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.6.9/joplin-v2.6.9.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.6.9/joplin-v2.6.9-32bit.apk)
|
||||
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://github.com/laurent22/joplin-android/releases/download/android-v2.7.2/joplin-v2.7.2.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.7.2/joplin-v2.7.2-32bit.apk)
|
||||
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
|
||||
@@ -49,19 +55,18 @@ To start it, type `joplin`.
|
||||
|
||||
For usage information, please refer to the full [Joplin Terminal Application Documentation](https://joplinapp.org/terminal/).
|
||||
|
||||
### Unsupported methods
|
||||
|
||||
There are other ways to install the terminal application. However, they are not supported and problems must be reported to the upstream projects.
|
||||
|
||||
Operating system | Method
|
||||
-----------------|----------------
|
||||
Arch Linux | An Arch Linux package is available [here](https://aur.archlinux.org/packages/joplin/). To install it, use an AUR wrapper such as yay: `yay -S joplin`. Both the CLI tool (type `joplin`) and desktop app (type `joplin-desktop`) are packaged. You can also install a compiled version with the [chaotic-aur](https://wiki.archlinux.org/index.php/Unofficial_user_repositories#chaotic-aur) repository. For support, please go to the [GitHub repo](https://github.com/masterkorp/joplin-pkgbuild). If you are interested in [pre-release](https://joplinapp.org/prereleases/) you have [joplin-beta](https://aur.archlinux.org/packages/joplin-beta).
|
||||
Flatpak | A Flatpak is available on [Flathub](https://flathub.org/apps/details/net.cozic.joplin_desktop). To install it, run `flatpak install net.cozic.joplin_desktop` after [setting up Flathub](https://flatpak.org/setup/). GUI software managers on most distros support Flatpak installation.
|
||||
|
||||
## Web Clipper
|
||||
|
||||
The Web Clipper is a browser extension that allows you to save web pages and screenshots from your browser. For more information on how to install and use it, see the [Web Clipper Help Page](https://github.com/laurent22/joplin/blob/dev/readme/clipper.md).
|
||||
|
||||
## Unofficial Alternative Distributions
|
||||
|
||||
There are a number of unofficial alternative Joplin distributions. If you do not want to or cannot use appimages or any of the other officially supported releases then you may wish to consider these.
|
||||
|
||||
However these come with a caveat in that they are not officially supported so certain issues may not be supportable by the main project. Rather support requests, bug reports and general advice would need to go to the maintainers of these distributions.
|
||||
|
||||
A community maintained list of these distributions can be found here: [Unofficial Joplin distributions](https://discourse.joplinapp.org/t/unofficial-alternative-joplin-distributions/23703)
|
||||
|
||||
# Sponsors
|
||||
|
||||
<!-- SPONSORS-ORG -->
|
||||
@@ -75,11 +80,11 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/3061769?s=96&v=4"/></br>[c-nagy](https://github.com/c-nagy) | <img width="50" src="https://avatars2.githubusercontent.com/u/70780798?s=96&v=4"/></br>[cabottech](https://github.com/cabottech) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/4862947?s=96&v=4"/></br>[chrootlogin](https://github.com/chrootlogin) | <img width="50" src="https://avatars2.githubusercontent.com/u/82579431?s=96&v=4"/></br>[clmntsl](https://github.com/clmntsl) | <img width="50" src="https://avatars2.githubusercontent.com/u/808091?s=96&v=4"/></br>[cuongtransc](https://github.com/cuongtransc) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[fbloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/38898566?s=96&v=4"/></br>[h4sh5](https://github.com/h4sh5) | <img width="50" src="https://avatars2.githubusercontent.com/u/3266447?s=96&v=4"/></br>[iamwillbar](https://github.com/iamwillbar) | <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) |
|
||||
| | | | |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[fbloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/49439044?s=96&v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars2.githubusercontent.com/u/38898566?s=96&v=4"/></br>[h4sh5](https://github.com/h4sh5) | <img width="50" src="https://avatars2.githubusercontent.com/u/3266447?s=96&v=4"/></br>[iamwillbar](https://github.com/iamwillbar) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) | <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) | | | |
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
|
||||
<!-- TOC -->
|
||||
@@ -128,6 +133,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
- [How to build the apps](https://github.com/laurent22/joplin/blob/dev/BUILD.md)
|
||||
- [Writing a technical spec](https://github.com/laurent22/joplin/blob/dev/readme/technical_spec.md)
|
||||
- [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/e2ee.md)
|
||||
- [Desktop application styling](https://github.com/laurent22/joplin/blob/dev/readme/spec/desktop_styling.md)
|
||||
- [Note History spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/history.md)
|
||||
- [Sync Lock spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_lock.md)
|
||||
- [Synchronous Scroll spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_scroll.md)
|
||||
@@ -137,11 +143,11 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
- [Server: Delta Sync](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_delta_sync.md)
|
||||
- [Server: Sharing](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_sharing.md)
|
||||
|
||||
- Google Summer of Code 2021
|
||||
- Google Summer of Code 2022
|
||||
|
||||
- [Google Summer of Code 2021](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2021/index.md)
|
||||
- [How to submit a GSoC pull request](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2021/pull_request_guidelines.md)
|
||||
- [Project Ideas](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2021/ideas.md)
|
||||
- [Google Summer of Code 2022](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2022/index.md)
|
||||
- [How to submit a GSoC pull request](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2022/pull_request_guidelines.md)
|
||||
- [Project Ideas](https://github.com/laurent22/joplin/blob/dev/readme/gsoc2022/ideas.md)
|
||||
|
||||
- About
|
||||
|
||||
@@ -514,44 +520,44 @@ Current translations:
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Po File | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 91%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 26%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 65%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 51%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 87%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Benjamin Weis](mailto:benjamin.weis@gmx.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [MrKanister](mailto:pthrp_bnsrv@aleeas.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 50%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Mora](mailto:francisco.m.collao@gmail.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Mora](mailto:francisco.m.collao@gmail.com) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 29%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 91%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | miucci | 92%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 100%
|
||||
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 33%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 90%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 87%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 76%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 83%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | Alexander Dawson | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 63%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 62%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 82%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Felipe Viggiano](mailto:felipeviggiano@gmail.com) | 91%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 82%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 58%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 57%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 91%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 41%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 81%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 85%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 91%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 74%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [horaceyoung](mailto:yonghaoharry@gmail.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 73%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [horaceyoung](mailto:yonghaoharry@gmail.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [SiderealArt](mailto:nelson22768384@gmail.com) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 87%
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
# This is a sample docker-compose file that can be used to run Joplin Server
|
||||
# along with a PostgreSQL server.
|
||||
#
|
||||
# All environment variables are optional. If you don't set them, you will get a
|
||||
# warning from docker-compose, however the app should use working defaults.
|
||||
# Update the following fields in the stanza below:
|
||||
#
|
||||
# POSTGRES_USER
|
||||
# POSTGRES_PASSWORD
|
||||
# APP_BASE_URL
|
||||
#
|
||||
# APP_BASE_URL: This is the base public URL where the service will be running.
|
||||
# - If Joplin Server needs to be accessible over the internet, configure APP_BASE_URL as follows: https://example.com/joplin.
|
||||
# - If Joplin Server does not need to be accessible over the internet, set the the APP_BASE_URL to your server's hostname.
|
||||
# For Example: http://[hostname]:22300. The base URL can include the port.
|
||||
# APP_PORT: The local port on which the Docker container will listen.
|
||||
# - This would typically be mapped to port to 443 (TLS) with a reverse proxy.
|
||||
# - If Joplin Server does not need to be accessible over the internet, the port can be mapped to 22300.
|
||||
|
||||
version: '3'
|
||||
|
||||
|
||||
1
fastlane/metadata/android/de/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
eine Notiz- und Aufgaben-App mit Sync zwischen Linux, macOS, Windows
|
||||
9
fastlane/metadata/android/en-US/full_description.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
<strong>Joplin</strong> is a note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in <a href="#markdown">Markdown format</a>.
|
||||
|
||||
Notes exported from Evernote and other applications <a href="https://joplinapp.org/help/#importing">can be imported</a> into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
|
||||
|
||||
The notes can be securely <a href="https://joplinapp.org/help/#synchronisation">synchronised</a> using <a href="https://joplinapp.org/help/#encryption">end-to-end encryption</a> with various cloud services including Nextcloud, Dropbox, OneDrive and <a href="https://joplinapp.org/plans/">Joplin Cloud</a>.
|
||||
|
||||
Full text search is available on all platforms to quickly find the information you need. The app can be customised using plugins and themes, and you can also easily create your own.
|
||||
|
||||
The application is available for Windows, Linux, macOS, Android and iOS. A <a href="https://joplinapp.org/clipper/">Web Clipper</a>, to save web pages and screenshots from your browser, is also available for <a href="https://addons.mozilla.org/firefox/addon/joplin-web-clipper/">Firefox</a> and <a href="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek?hl=en-GB">Chrome</a>.
|
||||
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/01.png
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/02.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
a note taking and to-do app with sync between Linux, macOS, Windows, and mobile
|
||||
@@ -18,6 +18,7 @@
|
||||
"buildCommandIndex": "gulp buildCommandIndex",
|
||||
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../joplin-website/docs/api/references/plugin_api packages/lib/services/plugins/api/",
|
||||
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
|
||||
"updateNews": "node ./packages/tools/website/updateNews",
|
||||
"buildSettingJsonSchema": "yarn workspace joplin start settingschema ../../../joplin-website/docs/schema/settings.json",
|
||||
"buildTranslations": "node packages/tools/build-translation.js",
|
||||
"buildWebsite": "node ./packages/tools/website/build.js && yarn run buildPluginDoc && yarn run buildSettingJsonSchema",
|
||||
@@ -31,7 +32,7 @@
|
||||
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"postinstall": "gulp build",
|
||||
"publishAll": "git pull && yarn run buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
"releaseAndroid": "yarn run buildParallel && export PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" && node packages/tools/release-android.js",
|
||||
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" node packages/tools/release-android.js",
|
||||
"releaseAndroidClean": "node packages/tools/release-android.js",
|
||||
"releaseCli": "node packages/tools/release-cli.js",
|
||||
"releaseClipper": "node packages/tools/release-clipper.js",
|
||||
|
||||
@@ -313,6 +313,14 @@ async function fetchAllNotes() {
|
||||
lines.push('');
|
||||
lines.push('\tcurl -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my resource title"}\' http://localhost:41184/resources');
|
||||
lines.push('');
|
||||
lines.push('To **update** the resource content, you can make a PUT request with the same arguments:');
|
||||
lines.push('');
|
||||
lines.push('\tcurl -X PUT -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my modified title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696');
|
||||
lines.push('');
|
||||
lines.push('Or if you only need to update the resource properties (title, etc.), without changing the content, you can make a regular PUT request:');
|
||||
lines.push('');
|
||||
lines.push('\tcurl -X PUT --data \'{"title": "My new title"}\' http://localhost:41184/resources/8fe1417d7b184324bf6b0122b76c4696');
|
||||
lines.push('');
|
||||
lines.push('The "data" field is required, while the "props" one is not. If not specified, default values will be used.');
|
||||
lines.push('');
|
||||
lines.push('**From a plugin** the syntax to create a resource is also a bit special:');
|
||||
@@ -368,6 +376,11 @@ async function fetchAllNotes() {
|
||||
lines.push(`Sets the properties of the ${singular} with ID :id`);
|
||||
lines.push('');
|
||||
|
||||
if (model.type === BaseModel.TYPE_RESOURCE) {
|
||||
lines.push('You may also update the file data by specifying a file (See `POST /resources` example).');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push(`## DELETE /${tableName}/:id`);
|
||||
lines.push('');
|
||||
lines.push(`Deletes the ${singular} with ID :id`);
|
||||
|
||||
@@ -242,9 +242,9 @@ describe('MdToHtml', function() {
|
||||
{
|
||||
const input = '# Head\nFruits\n- Apple\n';
|
||||
const result = await mdToHtml.render(input, null, { bodyOnly: true, mapsToLine: true });
|
||||
expect(result.html.trim()).toBe('<h1 id="head" class="maps-to-line" source-line="0">Head</h1>\n' +
|
||||
'<p class="maps-to-line" source-line="1">Fruits</p>\n' +
|
||||
'<ul>\n<li class="maps-to-line" source-line="2">Apple</li>\n</ul>'
|
||||
expect(result.html.trim()).toBe('<h1 id="head" class="maps-to-line" source-line="0" source-line-end="1">Head</h1>\n' +
|
||||
'<p class="maps-to-line" source-line="1" source-line-end="2">Fruits</p>\n' +
|
||||
'<ul>\n<li class="maps-to-line" source-line="2" source-line-end="3">Apple</li>\n</ul>'
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -63,13 +63,15 @@ class AppComponent extends Component {
|
||||
contentScriptLoaded: false,
|
||||
selectedTags: [],
|
||||
contentScriptError: '',
|
||||
newNoteId: null,
|
||||
});
|
||||
|
||||
this.confirm_click = () => {
|
||||
this.confirm_click = async () => {
|
||||
const content = Object.assign({}, this.props.clippedContent);
|
||||
content.tags = this.state.selectedTags.join(',');
|
||||
content.parent_id = this.props.selectedFolderId;
|
||||
bridge().sendContentToJoplin(content);
|
||||
const response = await bridge().sendContentToJoplin(content);
|
||||
this.setState({ newNoteId: response.id });
|
||||
};
|
||||
|
||||
this.contentTitle_change = (event) => {
|
||||
@@ -402,6 +404,24 @@ class AppComponent extends Component {
|
||||
);
|
||||
};
|
||||
|
||||
const openNewNoteButton = () => {
|
||||
|
||||
if (!this.state.newNoteId) { return null; } else {
|
||||
return (
|
||||
// The jopin:// link must be opened in a new tab. When it's opened for the first time, a system dialog will ask for the user's permission.
|
||||
// The system dialog is too big to fit into the popup so the user will not be able to see the dialog buttons and get stuck.
|
||||
<a
|
||||
className="Button"
|
||||
href={`joplin://x-callback-url/openNote?id=${encodeURIComponent(this.state.newNoteId)}`}
|
||||
target="_blank"
|
||||
onClick={() => this.setState({ newNoteId: null })}
|
||||
>
|
||||
Open newly created note
|
||||
</a>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const tagDataListOptions = [];
|
||||
for (let i = 0; i < this.props.tags.length; i++) {
|
||||
const tag = this.props.tags[i];
|
||||
@@ -437,6 +457,7 @@ class AppComponent extends Component {
|
||||
</div>
|
||||
{ warningComponent }
|
||||
{ previewComponent }
|
||||
{ openNewNoteButton() }
|
||||
{ clipperStatusComp() }
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -463,9 +463,11 @@ class Bridge {
|
||||
// This is the perfect Heisenbug - it happens always when opening the popup the first time EXCEPT
|
||||
// when the debugger is open. Then everything is working fine and the bug NEVER EVER happens,
|
||||
// so it's impossible to understand what's going on.
|
||||
await this.clipperApiExec('POST', 'notes', { nounce: this.nounce_++ }, content);
|
||||
const response = await this.clipperApiExec('POST', 'notes', { nounce: this.nounce_++ }, content);
|
||||
|
||||
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.message === '{"error":"Duplicate Nounce"}') {
|
||||
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });
|
||||
|
||||
@@ -9,6 +9,13 @@ interface LastSelectedPath {
|
||||
directory: string;
|
||||
}
|
||||
|
||||
interface OpenDialogOptions {
|
||||
properties?: string[];
|
||||
defaultPath?: string;
|
||||
createDirectory?: boolean;
|
||||
filters?: any[];
|
||||
}
|
||||
|
||||
export class Bridge {
|
||||
|
||||
private electronWrapper_: ElectronAppWrapper;
|
||||
@@ -155,14 +162,14 @@ export class Bridge {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async showOpenDialog(options: any = null) {
|
||||
async showOpenDialog(options: OpenDialogOptions = null) {
|
||||
const { dialog } = require('electron');
|
||||
if (!options) options = {};
|
||||
let fileType = 'file';
|
||||
if (options.properties && options.properties.includes('openDirectory')) fileType = 'directory';
|
||||
if (!('defaultPath' in options) && (this.lastSelectedPaths_ as any)[fileType]) options.defaultPath = (this.lastSelectedPaths_ as any)[fileType];
|
||||
if (!('createDirectory' in options)) options.createDirectory = true;
|
||||
const { filePaths } = await dialog.showOpenDialog(this.window(), options);
|
||||
const { filePaths } = await dialog.showOpenDialog(this.window(), options as any);
|
||||
if (filePaths && filePaths.length) {
|
||||
(this.lastSelectedPaths_ as any)[fileType] = dirname(filePaths[0]);
|
||||
}
|
||||
|
||||
19
packages/app-desktop/commands/editProfileConfig.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import Setting from '../../lib/models/Setting';
|
||||
import { openFileWithExternalEditor } from '../../lib/services/ExternalEditWatcher/utils';
|
||||
import bridge from '../services/bridge';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'editProfileConfig',
|
||||
label: () => _('Edit profile configuration...'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
await openFileWithExternalEditor(`${Setting.value('rootProfileDir')}/profiles.json`, bridge());
|
||||
},
|
||||
enabledCondition: 'hasMultiProfiles',
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
import * as copyDevCommand from './copyDevCommand';
|
||||
import * as editProfileConfig from './editProfileConfig';
|
||||
import * as exportFolders from './exportFolders';
|
||||
import * as exportNotes from './exportNotes';
|
||||
import * as focusElement from './focusElement';
|
||||
@@ -8,11 +9,16 @@ import * as replaceMisspelling from './replaceMisspelling';
|
||||
import * as restoreNoteRevision from './restoreNoteRevision';
|
||||
import * as startExternalEditing from './startExternalEditing';
|
||||
import * as stopExternalEditing from './stopExternalEditing';
|
||||
import * as switchProfile from './switchProfile';
|
||||
import * as switchProfile1 from './switchProfile1';
|
||||
import * as switchProfile2 from './switchProfile2';
|
||||
import * as switchProfile3 from './switchProfile3';
|
||||
import * as toggleExternalEditing from './toggleExternalEditing';
|
||||
import * as toggleSafeMode from './toggleSafeMode';
|
||||
|
||||
const index:any[] = [
|
||||
copyDevCommand,
|
||||
editProfileConfig,
|
||||
exportFolders,
|
||||
exportNotes,
|
||||
focusElement,
|
||||
@@ -21,6 +27,10 @@ const index:any[] = [
|
||||
restoreNoteRevision,
|
||||
startExternalEditing,
|
||||
stopExternalEditing,
|
||||
switchProfile,
|
||||
switchProfile1,
|
||||
switchProfile2,
|
||||
switchProfile3,
|
||||
toggleExternalEditing,
|
||||
toggleSafeMode,
|
||||
];
|
||||
|
||||
26
packages/app-desktop/commands/switchProfile.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { saveProfileConfig } from '@joplin/lib/services/profileConfig';
|
||||
import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
|
||||
import bridge from '../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'switchProfile',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, profileIndex: number) => {
|
||||
const currentConfig = context.state.profileConfig;
|
||||
if (currentConfig.currentProfile === profileIndex) return;
|
||||
|
||||
const newConfig: ProfileConfig = {
|
||||
...currentConfig,
|
||||
currentProfile: profileIndex,
|
||||
};
|
||||
|
||||
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newConfig);
|
||||
bridge().restart();
|
||||
},
|
||||
};
|
||||
};
|
||||
15
packages/app-desktop/commands/switchProfile1.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'switchProfile1',
|
||||
label: () => _('Switch to profile %d', 1),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
await CommandService.instance().execute('switchProfile', 0);
|
||||
},
|
||||
};
|
||||
};
|
||||
15
packages/app-desktop/commands/switchProfile2.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'switchProfile2',
|
||||
label: () => _('Switch to profile %d', 2),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
await CommandService.instance().execute('switchProfile', 1);
|
||||
},
|
||||
};
|
||||
};
|
||||
15
packages/app-desktop/commands/switchProfile3.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'switchProfile3',
|
||||
label: () => _('Switch to profile %d', 3),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
await CommandService.instance().execute('switchProfile', 2);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -8,9 +8,11 @@ import StyledInput from '../style/StyledInput';
|
||||
import { IconSelector, ChangeEvent } from './IconSelector';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { FolderEntity, FolderIcon } from '@joplin/lib/services/database/types';
|
||||
import { FolderEntity, FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
|
||||
import Button from '../Button/Button';
|
||||
import bridge from '../../services/bridge';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import FolderIconBox from '../FolderIconBox';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -93,6 +95,34 @@ export default function(props: Props) {
|
||||
setFolderIcon(null);
|
||||
}, []);
|
||||
|
||||
const onBrowseClick = useCallback(async () => {
|
||||
const filePaths = await bridge().showOpenDialog({
|
||||
filters: [
|
||||
{
|
||||
name: _('Images'),
|
||||
extensions: ['jpg', 'jpeg', 'png'],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (filePaths.length !== 1) return;
|
||||
const filePath = filePaths[0];
|
||||
|
||||
try {
|
||||
const dataUrl = await shim.imageToDataUrl(filePath, 256);
|
||||
setFolderIcon(icon => {
|
||||
return {
|
||||
...icon,
|
||||
emoji: '',
|
||||
name: '',
|
||||
type: FolderIconType.DataUrl,
|
||||
dataUrl,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
await bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function renderForm() {
|
||||
return (
|
||||
<div>
|
||||
@@ -105,11 +135,14 @@ export default function(props: Props) {
|
||||
<div className="form-input-group">
|
||||
<label>{_('Icon')}</label>
|
||||
<div className="icon-selector-row">
|
||||
{ folderIcon && <div className="foldericon"><FolderIconBox folderIcon={folderIcon} /></div> }
|
||||
<IconSelector
|
||||
title={_('Select emoji...')}
|
||||
icon={folderIcon}
|
||||
onChange={onFolderIconChange}
|
||||
/>
|
||||
<Button ml={1} title={_('Clear')} onClick={onClearClick}/>
|
||||
<Button ml={1} title={_('Select file...')} onClick={onBrowseClick}/>
|
||||
{ folderIcon && <Button ml={1} title={_('Clear')} onClick={onClearClick}/> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { loadScript } from '../utils/loadScript';
|
||||
import Button from '../Button/Button';
|
||||
import { FolderIcon } from '@joplin/lib/services/database/types';
|
||||
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
|
||||
import bridge from '../../services/bridge';
|
||||
|
||||
export interface ChangeEvent {
|
||||
@@ -15,6 +15,7 @@ type ChangeHandler = (event: ChangeEvent)=> void;
|
||||
interface Props {
|
||||
onChange: ChangeHandler;
|
||||
icon: FolderIcon | null;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const IconSelector = (props: Props) => {
|
||||
@@ -62,7 +63,7 @@ export const IconSelector = (props: Props) => {
|
||||
});
|
||||
|
||||
const onEmoji = (selection: FolderIcon) => {
|
||||
props.onChange({ value: selection });
|
||||
props.onChange({ value: { ...selection, type: FolderIconType.Emoji } });
|
||||
};
|
||||
|
||||
p.on('emoji', onEmoji);
|
||||
@@ -78,16 +79,25 @@ export const IconSelector = (props: Props) => {
|
||||
picker.togglePicker(buttonRef.current);
|
||||
}, [picker]);
|
||||
|
||||
const buttonText = props.icon ? props.icon.emoji : '...';
|
||||
// const buttonText = props.icon ? props.icon.emoji : '...';
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={!picker}
|
||||
ref={buttonRef}
|
||||
onClick={onClick}
|
||||
title={buttonText}
|
||||
isSquare={true}
|
||||
fontSize={20}
|
||||
title={props.title}
|
||||
/>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <Button
|
||||
// disabled={!picker}
|
||||
// ref={buttonRef}
|
||||
// onClick={onClick}
|
||||
// title={buttonText}
|
||||
// isSquare={true}
|
||||
// fontSize={20}
|
||||
// />
|
||||
// );
|
||||
};
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
.icon-selector-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon-selector-row > .foldericon {
|
||||
margin-right: 5px;
|
||||
display: flex;
|
||||
border: 1px solid var(--joplin-divider-color);
|
||||
padding: 5px;
|
||||
background-color: var(--joplin-background-color);
|
||||
}
|
||||
17
packages/app-desktop/gui/FolderIconBox.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
|
||||
|
||||
interface Props {
|
||||
folderIcon: FolderIcon;
|
||||
}
|
||||
|
||||
export default function(props: Props) {
|
||||
const folderIcon = props.folderIcon;
|
||||
|
||||
if (folderIcon.type === FolderIconType.Emoji) {
|
||||
return <span style={{ fontSize: 20 }}>{folderIcon.emoji}</span>;
|
||||
} else if (folderIcon.type === FolderIconType.DataUrl) {
|
||||
return <img style={{ width: 20, height: 20 }} src={folderIcon.dataUrl} />;
|
||||
} else {
|
||||
throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
|
||||
}
|
||||
}
|
||||
34
packages/app-desktop/gui/MainScreen/commands/addProfile.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { createNewProfile, saveProfileConfig } from '@joplin/lib/services/profileConfig';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'addProfile',
|
||||
label: () => _('Create new profile...'),
|
||||
};
|
||||
|
||||
export const runtime = (comp: any): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext) => {
|
||||
comp.setState({
|
||||
promptOptions: {
|
||||
label: _('Profile name:'),
|
||||
buttons: ['create', 'cancel'],
|
||||
value: '',
|
||||
onClose: async (answer: string) => {
|
||||
if (answer) {
|
||||
const newConfig = await createNewProfile(context.state.profileConfig, answer);
|
||||
newConfig.currentProfile = newConfig.profiles.length - 1;
|
||||
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newConfig);
|
||||
bridge().restart();
|
||||
}
|
||||
|
||||
comp.setState({ promptOptions: null });
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||
import * as addProfile from './addProfile';
|
||||
import * as commandPalette from './commandPalette';
|
||||
import * as editAlarm from './editAlarm';
|
||||
import * as exportPdf from './exportPdf';
|
||||
@@ -38,6 +39,7 @@ import * as toggleSideBar from './toggleSideBar';
|
||||
import * as toggleVisiblePanes from './toggleVisiblePanes';
|
||||
|
||||
const index:any[] = [
|
||||
addProfile,
|
||||
commandPalette,
|
||||
editAlarm,
|
||||
exportPdf,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||
import { AppState } from '../app.reducer';
|
||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
@@ -18,9 +18,9 @@ import menuCommandNames from './menuCommandNames';
|
||||
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
|
||||
import bridge from '../services/bridge';
|
||||
import checkForUpdates from '../checkForUpdates';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import { ProfileConfig } from '../../lib/services/profileConfig/types';
|
||||
const packageInfo = require('../packageInfo.js');
|
||||
const { clipboard } = require('electron');
|
||||
const Menu = bridge().Menu;
|
||||
@@ -39,7 +39,7 @@ function pluginMenuItemsCommandNames(menuItems: MenuItem[]): string[] {
|
||||
return output;
|
||||
}
|
||||
|
||||
function pluginCommandNames(plugins: PluginStates): string[] {
|
||||
function getPluginCommandNames(plugins: PluginStates): string[] {
|
||||
let output: string[] = [];
|
||||
|
||||
for (const view of pluginUtils.viewsByType(plugins, 'menu')) {
|
||||
@@ -70,6 +70,42 @@ function createPluginMenuTree(label: string, menuItems: MenuItem[], onMenuItemCl
|
||||
return output;
|
||||
}
|
||||
|
||||
const useSwitchProfileMenuItems = (profileConfig: ProfileConfig, menuItemDic: any) => {
|
||||
return useMemo(() => {
|
||||
const switchProfileMenuItems: any[] = [];
|
||||
|
||||
for (let i = 0; i < profileConfig.profiles.length; i++) {
|
||||
const profile = profileConfig.profiles[i];
|
||||
|
||||
let menuItem: any = {};
|
||||
const profileNum = i + 1;
|
||||
|
||||
if (menuItemDic[`switchProfile${profileNum}`]) {
|
||||
menuItem = { ...menuItemDic[`switchProfile${profileNum}`] };
|
||||
} else {
|
||||
menuItem = {
|
||||
label: profile.name,
|
||||
click: () => {
|
||||
void CommandService.instance().execute('switchProfile', i);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
menuItem.label = profile.name;
|
||||
menuItem.type = 'checkbox';
|
||||
menuItem.checked = profileConfig.currentProfile === i;
|
||||
|
||||
switchProfileMenuItems.push(menuItem);
|
||||
}
|
||||
|
||||
switchProfileMenuItems.push({ type: 'separator' });
|
||||
switchProfileMenuItems.push(menuItemDic.addProfile);
|
||||
switchProfileMenuItems.push(menuItemDic.editProfileConfig);
|
||||
|
||||
return switchProfileMenuItems;
|
||||
}, [profileConfig, menuItemDic]);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
dispatch: Function;
|
||||
menuItemProps: any;
|
||||
@@ -90,6 +126,7 @@ interface Props {
|
||||
plugins: PluginStates;
|
||||
customCss: string;
|
||||
locale: string;
|
||||
profileConfig: ProfileConfig;
|
||||
}
|
||||
|
||||
const commandNames: string[] = menuCommandNames();
|
||||
@@ -241,6 +278,18 @@ function useMenu(props: Props) {
|
||||
const onImportModuleClickRef = useRef(null);
|
||||
onImportModuleClickRef.current = onImportModuleClick;
|
||||
|
||||
const pluginCommandNames = useMemo(() => props.pluginMenuItems.map((view: any) => view.commandName), [props.pluginMenuItems]);
|
||||
|
||||
const menuItemDic = useMemo(() => {
|
||||
return menuUtils.commandsToMenuItems(
|
||||
commandNames.concat(pluginCommandNames),
|
||||
(commandName: string) => onMenuItemClickRef.current(commandName),
|
||||
props.locale
|
||||
);
|
||||
}, [commandNames, pluginCommandNames, props.locale]);
|
||||
|
||||
const switchProfileMenuItems: any[] = useSwitchProfileMenuItems(props.profileConfig, menuItemDic);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: any = null;
|
||||
|
||||
@@ -249,13 +298,6 @@ function useMenu(props: Props) {
|
||||
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
const pluginCommandNames = props.pluginMenuItems.map((view: any) => view.commandName);
|
||||
const menuItemDic = menuUtils.commandsToMenuItems(
|
||||
commandNames.concat(pluginCommandNames),
|
||||
(commandName: string) => onMenuItemClickRef.current(commandName),
|
||||
props.locale
|
||||
);
|
||||
|
||||
const quitMenuItem = {
|
||||
label: _('Quit'),
|
||||
accelerator: keymapService.getAccelerator('quit'),
|
||||
@@ -347,6 +389,12 @@ function useMenu(props: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
importItems.push({ type: 'separator' });
|
||||
importItems.push({
|
||||
label: _('Other applications...'),
|
||||
click: () => { void bridge().openExternal('https://discourse.joplinapp.org/t/importing-notes-from-other-notebook-applications/22425'); },
|
||||
});
|
||||
|
||||
exportItems.push(
|
||||
menuItemDic.exportPdf
|
||||
);
|
||||
@@ -379,6 +427,10 @@ function useMenu(props: Props) {
|
||||
const newFolderItem = menuItemDic.newFolder;
|
||||
const newSubFolderItem = menuItemDic.newSubFolder;
|
||||
const printItem = menuItemDic.print;
|
||||
const switchProfileItem = {
|
||||
label: _('Switch profile'),
|
||||
submenu: switchProfileMenuItems,
|
||||
};
|
||||
|
||||
let toolsItems: any[] = [];
|
||||
|
||||
@@ -493,6 +545,8 @@ function useMenu(props: Props) {
|
||||
platforms: ['darwin'],
|
||||
},
|
||||
|
||||
shim.isMac() ? noItem : switchProfileItem,
|
||||
|
||||
shim.isMac() ? {
|
||||
label: _('Hide %s', 'Joplin'),
|
||||
platforms: ['darwin'],
|
||||
@@ -539,6 +593,7 @@ function useMenu(props: Props) {
|
||||
type: 'separator',
|
||||
},
|
||||
printItem,
|
||||
switchProfileItem,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -566,8 +621,15 @@ function useMenu(props: Props) {
|
||||
menuItemDic.textPaste,
|
||||
menuItemDic.textSelectAll,
|
||||
separator(),
|
||||
menuItemDic['editor.undo'],
|
||||
menuItemDic['editor.redo'],
|
||||
// Using the generic "undo"/"redo" roles mean the menu
|
||||
// item will work in every text fields, whether it's the
|
||||
// editor or a regular text field.
|
||||
{
|
||||
role: 'undo',
|
||||
},
|
||||
{
|
||||
role: 'redo',
|
||||
},
|
||||
separator(),
|
||||
menuItemDic.textBold,
|
||||
menuItemDic.textItalic,
|
||||
@@ -835,7 +897,21 @@ function useMenu(props: Props) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
};
|
||||
}, [props.routeName, props.pluginMenuItems, props.pluginMenus, keymapLastChangeTime, modulesLastChangeTime, props['spellChecker.language'], props['spellChecker.enabled'], props.plugins, props.customCss, props.locale]);
|
||||
}, [
|
||||
props.routeName,
|
||||
props.pluginMenuItems,
|
||||
props.pluginMenus,
|
||||
keymapLastChangeTime,
|
||||
modulesLastChangeTime,
|
||||
props['spellChecker.language'],
|
||||
props['spellChecker.enabled'],
|
||||
props.plugins,
|
||||
props.customCss,
|
||||
props.locale,
|
||||
props.profileConfig,
|
||||
switchProfileMenuItems,
|
||||
menuItemDic,
|
||||
]);
|
||||
|
||||
useMenuStates(menu, props);
|
||||
|
||||
@@ -876,7 +952,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
|
||||
return {
|
||||
menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(pluginCommandNames(state.pluginService.plugins)), whenClauseContext),
|
||||
menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(getPluginCommandNames(state.pluginService.plugins)), whenClauseContext),
|
||||
locale: state.settings.locale,
|
||||
routeName: state.route.routeName,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
@@ -894,6 +970,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customCss,
|
||||
profileConfig: state.profileConfig,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -515,8 +515,9 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
color: ${theme.searchMarkerColor} !important;
|
||||
}
|
||||
|
||||
/* We need !important because the search marker is overridden by CodeMirror's own text selection marker */
|
||||
.cm-search-marker-selected {
|
||||
background: ${theme.selectedColor2};
|
||||
background: ${theme.selectedColor2} !important;
|
||||
color: ${theme.color2} !important;
|
||||
}
|
||||
|
||||
@@ -648,6 +649,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
// undefined. Maybe due to the error boundary that unmount components.
|
||||
// Since we can't do much about it we just print an error.
|
||||
if (webviewRef.current && webviewRef.current.wrappedInstance) {
|
||||
// To keep consistency among CodeMirror's editing and scroll percents
|
||||
// of Editor and Viewer.
|
||||
const percent = getLineScrollPercent();
|
||||
setEditorPercentScroll(percent);
|
||||
options.percent = percent;
|
||||
webviewRef.current.wrappedInstance.send('setHtml', renderedBody.html, options);
|
||||
} else {
|
||||
console.error('Trying to set HTML on an undefined webview ref');
|
||||
@@ -731,15 +737,19 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
// It might be buggy, refer to the below issue
|
||||
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
|
||||
useEffect(() => {
|
||||
function pointerInsideEditor(x: number, y: number) {
|
||||
function pointerInsideEditor(params: any) {
|
||||
const x = params.x, y = params.y, isEditable = params.isEditable, inputFieldType = params.inputFieldType;
|
||||
const elements = document.getElementsByClassName('codeMirrorEditor');
|
||||
if (!elements.length) return null;
|
||||
|
||||
// inputFieldType: The input field type of CodeMirror is "textarea" so the inputFieldType = "none",
|
||||
// and any single-line input above codeMirror has inputFieldType value according to the type of input e.g.(text = plainText, password = password, ...).
|
||||
if (!elements.length || !isEditable || inputFieldType !== 'none') return null;
|
||||
const rect = convertToScreenCoordinates(Setting.value('windowContentZoomFactor'), elements[0].getBoundingClientRect());
|
||||
return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y;
|
||||
}
|
||||
|
||||
async function onContextMenu(_event: any, params: any) {
|
||||
if (!pointerInsideEditor(params.x, params.y)) return;
|
||||
if (!pointerInsideEditor(params)) return;
|
||||
|
||||
const menu = new Menu();
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ export interface EditorProps {
|
||||
function Editor(props: EditorProps, ref: any) {
|
||||
const [editor, setEditor] = useState(null);
|
||||
const editorParent = useRef(null);
|
||||
const lastEditTime = useRef(NaN);
|
||||
|
||||
// Codemirror plugins add new commands to codemirror (or change it's behavior)
|
||||
// This command adds the smartListIndent function which will be bound to tab
|
||||
@@ -120,6 +121,7 @@ function Editor(props: EditorProps, ref: any) {
|
||||
const editor_change = useCallback((cm: any, change: any) => {
|
||||
if (props.onChange && change.origin !== 'setValue') {
|
||||
props.onChange(cm.getValue());
|
||||
lastEditTime.current = Date.now();
|
||||
}
|
||||
}, [props.onChange]);
|
||||
|
||||
@@ -154,7 +156,8 @@ function Editor(props: EditorProps, ref: any) {
|
||||
}, [props.onResize]);
|
||||
|
||||
const editor_update = useCallback((cm: any) => {
|
||||
props.onUpdate(cm);
|
||||
const edited = Date.now() - lastEditTime.current <= 100;
|
||||
props.onUpdate(cm, edited);
|
||||
}, [props.onUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
const now = Date.now();
|
||||
if (now >= ignoreNextEditorScrollTime_.current) ignoreNextEditorScrollEventCount_.current = 0;
|
||||
if (ignoreNextEditorScrollEventCount_.current < 10) { // for safety
|
||||
ignoreNextEditorScrollTime_.current = now + 200;
|
||||
ignoreNextEditorScrollTime_.current = now + 1000;
|
||||
ignoreNextEditorScrollEventCount_.current += 1;
|
||||
}
|
||||
};
|
||||
@@ -157,8 +157,9 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
// When heights of lines are updated in CodeMirror, 'update' events are raised.
|
||||
// If such an update event is raised, scroll position should be restored.
|
||||
// See https://github.com/laurent22/joplin/issues/5981
|
||||
const editor_update = useCallback((cm) => {
|
||||
const editor_update = useCallback((cm: any, edited: boolean) => {
|
||||
if (isCodeMirrorReady(cm)) {
|
||||
if (edited) return;
|
||||
const linesHeight = cm.heightAtLine(cm.lineCount()) - cm.heightAtLine(0);
|
||||
if (lastLinesHeight_.current !== linesHeight) {
|
||||
// To avoid cancelling intentional scroll position changes,
|
||||
|
||||
@@ -3,7 +3,8 @@ import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||
import { useEffect } from 'react';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenu';
|
||||
import { ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenuUtils';
|
||||
import { menuItems } from '../../../utils/contextMenu';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import convertToScreenCoordinates from '../../../../utils/convertToScreenCoordinates';
|
||||
@@ -67,6 +68,8 @@ export default function(editor: any, plugins: PluginStates, dispatch: Function)
|
||||
contextMenuActionOptions.current = {
|
||||
itemType,
|
||||
resourceId,
|
||||
filename: null,
|
||||
mime: null,
|
||||
linkToCopy,
|
||||
textToCopy: null,
|
||||
htmlToCopy: editor.selection ? editor.selection.getContent() : '',
|
||||
|
||||
@@ -93,11 +93,11 @@ const declarations: CommandDeclaration[] = [
|
||||
},
|
||||
{
|
||||
name: 'editor.undo',
|
||||
label: () => _('Undo'),
|
||||
label: () => _('Editor: %s', _('Undo')),
|
||||
},
|
||||
{
|
||||
name: 'editor.redo',
|
||||
label: () => _('Redo'),
|
||||
label: () => _('Editor: %s', _('Redo')),
|
||||
},
|
||||
{
|
||||
name: 'editor.indentLess',
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/** @jest-environment ./loadResources.testEnv */
|
||||
// eslint-disable-next-line strict, lines-around-directive
|
||||
'use strict';
|
||||
// use strict is necessary here so that typescript doesn't place "use strict" above the jest docblock
|
||||
// https://github.com/microsoft/TypeScript/issues/15819#issuecomment-782235619
|
||||
|
||||
import { textToDataUri, svgUriToPng } from './contextMenuUtils';
|
||||
|
||||
jest.mock('@joplin/lib/models/Resource');
|
||||
|
||||
describe('contextMenu', () => {
|
||||
it('should provide proper copy path', async () => {
|
||||
const testCase = [
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">test</svg>',
|
||||
'image/svg+xml',
|
||||
];
|
||||
const expectedText = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIj50ZXN0PC9zdmc+';
|
||||
expect(textToDataUri(testCase[0], testCase[1])).toBe(expectedText);
|
||||
});
|
||||
|
||||
it('should convert to png binary', async () => {
|
||||
const testCase = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiIGhlaWdodD0iMTAwcHgiIHdpZHRoPSIxMDBweCI+CjxnPgoJPHBhdGggZD0iTTI4LjEsMzYuNmM0LjYsMS45LDEyLjIsMS42LDIwLjksMS4xYzguOS0wLjQsMTktMC45LDI4LjksMC45YzYuMywxLjIsMTEuOSwzLjEsMTYuOCw2Yy0xLjUtMTIuMi03LjktMjMuNy0xOC42LTMxLjMgICBjLTQuOS0wLjItOS45LDAuMy0xNC44LDEuNEM0Ny44LDE3LjksMzYuMiwyNS42LDI4LjEsMzYuNnoiLz4KCTxwYXRoIGQ9Ik03MC4zLDkuOEM1Ny41LDMuNCw0Mi44LDMuNiwzMC41LDkuNWMtMyw2LTguNCwxOS42LTUuMywyNC45YzguNi0xMS43LDIwLjktMTkuOCwzNS4yLTIzLjFDNjMuNywxMC41LDY3LDEwLDcwLjMsOS44eiIvPgoJPHBhdGggZD0iTTE2LjUsNTEuM2MwLjYtMS43LDEuMi0zLjQsMi01LjFjLTMuOC0zLjQtNy41LTctMTEtMTAuOGMtMi4xLDYuMS0yLjgsMTIuNS0yLjMsMTguN0M5LjYsNTEuMSwxMy40LDUwLjIsMTYuNSw1MS4zeiIvPgoJPHBhdGggZD0iTTksMzEuNmMzLjUsMy45LDcuMiw3LjYsMTEuMSwxMS4xYzAuOC0xLjYsMS43LTMuMSwyLjYtNC42YzAuMS0wLjIsMC4zLTAuNCwwLjQtMC42Yy0yLjktMy4zLTMuMS05LjItMC42LTE3LjYgICBjMC44LTIuNywxLjgtNS4zLDIuNy03LjRjLTUuMiwzLjQtOS44LDgtMTMuMywxMy43QzEwLjgsMjcuOSw5LjgsMjkuNyw5LDMxLjZ6Ii8+Cgk8cGF0aCBkPSJNMTUuNCw1NC43Yy0yLjYtMS02LjEsMC43LTkuNywzLjRjMS4yLDYuNiwzLjksMTMsOCwxOC41QzEzLDY5LjMsMTMuNSw2MS44LDE1LjQsNTQuN3oiLz4KCTxwYXRoIGQ9Ik0zOS44LDU3LjZDNTQuMyw2Ni43LDcwLDczLDg2LjUsNzYuNGMwLjYtMC44LDEuMS0xLjYsMS43LTIuNWM0LjgtNy43LDctMTYuMyw2LjgtMjQuOGMtMTMuOC05LjMtMzEuMy04LjQtNDUuOC03LjcgICBjLTkuNSwwLjUtMTcuOCwwLjktMjMuMi0xLjdjLTAuMSwwLjEtMC4yLDAuMy0wLjMsMC40Yy0xLDEuNy0yLDMuNC0yLjksNS4xQzI4LjIsNDkuNywzMy44LDUzLjksMzkuOCw1Ny42eiIvPgoJPHBhdGggZD0iTTI2LjIsODguMmMzLjMsMiw2LjcsMy42LDEwLjIsNC43Yy0zLjUtNi4yLTYuMy0xMi42LTguOC0xOC41Yy0zLjEtNy4yLTUuOC0xMy41LTktMTcuMmMtMS45LDgtMiwxNi40LTAuMywyNC43ICAgQzIwLjYsODQuMiwyMy4yLDg2LjMsMjYuMiw4OC4yeiIvPgoJPHBhdGggZD0iTTMwLjksNzNjMi45LDYuOCw2LjEsMTQuNCwxMC41LDIxLjJjMTUuNiwzLDMyLTIuMyw0Mi42LTE0LjZDNjcuNyw3Niw1Mi4yLDY5LjYsMzcuOSw2MC43QzMyLDU3LDI2LjUsNTMsMjEuMyw0OC42ICAgYy0wLjYsMS41LTEuMiwzLTEuNyw0LjZDMjQuMSw1Ny4xLDI3LjMsNjQuNSwzMC45LDczeiIvPgo8L2c+Cjwvc3ZnPg==';
|
||||
const png = await svgUriToPng(document, testCase);
|
||||
expect(png).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('should throw error on invalid svg uri', async () => {
|
||||
// We are mocking console.error since jsdom throws errors to console when we try to load an invalid img
|
||||
// https://github.com/facebook/jest/pull/5267#issuecomment-356605468
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
const testCases: Array<string> = [
|
||||
'data:image/svg+xml;base64,error',
|
||||
'invalid',
|
||||
];
|
||||
for (const testCase of testCases) {
|
||||
await expect(svgUriToPng(document, testCase)).rejects.toBeInstanceOf(Error);
|
||||
}
|
||||
console.error = consoleError;
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index'
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { copyHtmlToClipboard } from './clipboardUtils';
|
||||
import bridge from '../../../services/bridge';
|
||||
import { ContextMenuItemType, ContextMenuOptions, ContextMenuItems, resourceInfo, textToDataUri, svgUriToPng } from './contextMenuUtils';
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
@@ -10,43 +11,10 @@ import BaseModel from '@joplin/lib/BaseModel';
|
||||
import { processPastedHtml } from './resourceHandling';
|
||||
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
const fs = require('fs-extra');
|
||||
const { writeFile } = require('fs-extra');
|
||||
const { clipboard } = require('electron');
|
||||
const { toSystemSlashes } = require('@joplin/lib/path-utils');
|
||||
|
||||
export enum ContextMenuItemType {
|
||||
None = '',
|
||||
Image = 'image',
|
||||
Resource = 'resource',
|
||||
Text = 'text',
|
||||
Link = 'link',
|
||||
}
|
||||
|
||||
export interface ContextMenuOptions {
|
||||
itemType: ContextMenuItemType;
|
||||
resourceId: string;
|
||||
linkToCopy: string;
|
||||
textToCopy: string;
|
||||
htmlToCopy: string;
|
||||
insertContent: Function;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
interface ContextMenuItem {
|
||||
label: string;
|
||||
onAction: Function;
|
||||
isActive: Function;
|
||||
}
|
||||
|
||||
interface ContextMenuItems {
|
||||
[key: string]: ContextMenuItem;
|
||||
}
|
||||
|
||||
async function resourceInfo(options: ContextMenuOptions): Promise<any> {
|
||||
const resource = options.resourceId ? await Resource.load(options.resourceId) : null;
|
||||
const resourcePath = resource ? Resource.fullPath(resource) : '';
|
||||
return { resource, resourcePath };
|
||||
}
|
||||
|
||||
function handleCopyToClipboard(options: ContextMenuOptions) {
|
||||
if (options.textToCopy) {
|
||||
clipboard.writeText(options.textToCopy);
|
||||
@@ -55,6 +23,12 @@ function handleCopyToClipboard(options: ContextMenuOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFileData(data: any, filename: string) {
|
||||
const newFilePath = await bridge().showSaveDialog({ defaultPath: filename });
|
||||
if (!newFilePath) return;
|
||||
await writeFile(newFilePath, data);
|
||||
}
|
||||
|
||||
export async function openItemById(itemId: string, dispatch: Function, hash: string = '') {
|
||||
|
||||
const item = await BaseItem.loadItemById(itemId);
|
||||
@@ -100,7 +74,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
await openItemById(options.resourceId, dispatch);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && (itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource),
|
||||
},
|
||||
saveAs: {
|
||||
label: _('Save as...'),
|
||||
@@ -112,7 +86,32 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
if (!filePath) return;
|
||||
await fs.copy(resourcePath, filePath);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
||||
// We handle images received as text seperately as it can be saved in multiple formats
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && (itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource),
|
||||
},
|
||||
saveAsSvg: {
|
||||
label: _('Save as SVG'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
await saveFileData(options.textToCopy, options.filename);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy && itemType === ContextMenuItemType.Image && options.mime?.startsWith('image/svg'),
|
||||
},
|
||||
saveAsPng: {
|
||||
label: _('Save as PNG'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
// First convert it to png then save
|
||||
if (options.mime != 'image/svg+xml') {
|
||||
throw new Error(`Unsupported image type: ${options.mime}`);
|
||||
}
|
||||
if (!options.filename) {
|
||||
throw new Error('Filename is needed to save as png');
|
||||
}
|
||||
const dataUri = textToDataUri(options.textToCopy, options.mime);
|
||||
const png = await svgUriToPng(document, dataUri);
|
||||
const filename = options.filename.replace('.svg', '.png');
|
||||
await saveFileData(png, filename);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy && itemType === ContextMenuItemType.Image && options.mime?.startsWith('image/svg'),
|
||||
},
|
||||
revealInFolder: {
|
||||
label: _('Reveal file in folder'),
|
||||
@@ -120,13 +119,20 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
const { resourcePath } = await resourceInfo(options);
|
||||
bridge().showItemInFolder(resourcePath);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
||||
},
|
||||
copyPathToClipboard: {
|
||||
label: _('Copy path to clipboard'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
const { resourcePath } = await resourceInfo(options);
|
||||
clipboard.writeText(toSystemSlashes(resourcePath));
|
||||
let path = '';
|
||||
if (options.textToCopy && options.mime) {
|
||||
path = textToDataUri(options.textToCopy, options.mime);
|
||||
} else {
|
||||
const { resourcePath } = await resourceInfo(options);
|
||||
if (resourcePath) path = toSystemSlashes(resourcePath);
|
||||
}
|
||||
if (!path) return;
|
||||
clipboard.writeText(path);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
||||
},
|
||||
@@ -137,7 +143,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
const image = bridge().createImageFromPath(resourcePath);
|
||||
clipboard.writeImage(image);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image,
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && itemType === ContextMenuItemType.Image,
|
||||
},
|
||||
cut: {
|
||||
label: _('Cut'),
|
||||
@@ -145,14 +151,14 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
handleCopyToClipboard(options);
|
||||
options.insertContent('');
|
||||
},
|
||||
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy),
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType != ContextMenuItemType.Image && (!options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy)),
|
||||
},
|
||||
copy: {
|
||||
label: _('Copy'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
handleCopyToClipboard(options);
|
||||
},
|
||||
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy || !!options.htmlToCopy,
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType != ContextMenuItemType.Image && (!!options.textToCopy || !!options.htmlToCopy),
|
||||
},
|
||||
paste: {
|
||||
label: _('Paste'),
|
||||
@@ -184,7 +190,6 @@ export default async function contextMenu(options: ContextMenuOptions, dispatch:
|
||||
const items = menuItems(dispatch);
|
||||
|
||||
if (!('readyOnly' in options)) options.isReadOnly = true;
|
||||
|
||||
for (const itemKey in items) {
|
||||
const item = items[itemKey];
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
|
||||
export enum ContextMenuItemType {
|
||||
None = '',
|
||||
Image = 'image',
|
||||
Resource = 'resource',
|
||||
Text = 'text',
|
||||
Link = 'link',
|
||||
}
|
||||
|
||||
export interface ContextMenuOptions {
|
||||
itemType: ContextMenuItemType;
|
||||
resourceId: string;
|
||||
mime: string;
|
||||
filename: string;
|
||||
linkToCopy: string;
|
||||
textToCopy: string;
|
||||
htmlToCopy: string;
|
||||
insertContent: Function;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface ContextMenuItem {
|
||||
label: string;
|
||||
onAction: Function;
|
||||
isActive: Function;
|
||||
}
|
||||
|
||||
export interface ContextMenuItems {
|
||||
[key: string]: ContextMenuItem;
|
||||
}
|
||||
|
||||
export async function resourceInfo(options: ContextMenuOptions): Promise<any> {
|
||||
const resource = options.resourceId ? await Resource.load(options.resourceId) : null;
|
||||
const filePath = resource ? Resource.fullPath(resource) : null;
|
||||
const filename = resource ? (resource.filename ? resource.filename : resource.title) : options.filename ? options.filename : '';
|
||||
return { resource, filePath, filename };
|
||||
}
|
||||
|
||||
export function textToDataUri(text: string, mime: string): string {
|
||||
return `data:${mime};base64,${Buffer.from(text).toString('base64')}`;
|
||||
}
|
||||
|
||||
export const svgUriToPng = (document: Document, svg: string) => {
|
||||
return new Promise<Uint8Array>((resolve, reject) => {
|
||||
let canvas: HTMLCanvasElement;
|
||||
let img: HTMLImageElement;
|
||||
|
||||
const cleanUpAndReject = (e: Error) => {
|
||||
if (canvas) canvas.remove();
|
||||
if (img) img.remove();
|
||||
return reject(e);
|
||||
};
|
||||
|
||||
try {
|
||||
img = document.createElement('img');
|
||||
if (!img) throw new Error('Failed to create img element');
|
||||
} catch (e) {
|
||||
return cleanUpAndReject(e);
|
||||
}
|
||||
|
||||
img.onload = function() {
|
||||
try {
|
||||
canvas = document.createElement('canvas');
|
||||
if (!canvas) throw new Error('Failed to create canvas element');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to get context');
|
||||
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
|
||||
const pngUri = canvas.toDataURL('image/png');
|
||||
if (!pngUri) throw new Error('Failed to generate png uri');
|
||||
const pngBase64 = pngUri.split(',')[1];
|
||||
const byteString = atob(pngBase64);
|
||||
// write the bytes of the string to a typed array
|
||||
const buff = new Uint8Array(byteString.length);
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
buff[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
canvas.remove();
|
||||
img.remove();
|
||||
resolve(buff);
|
||||
} catch (err) {
|
||||
cleanUpAndReject(err);
|
||||
}
|
||||
};
|
||||
img.onerror = function(e) {
|
||||
cleanUpAndReject(new Error(e.toString()));
|
||||
};
|
||||
img.src = svg;
|
||||
});
|
||||
};
|
||||
@@ -35,6 +35,8 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
|
||||
const menu = await contextMenu({
|
||||
itemType: arg0 && arg0.type,
|
||||
resourceId: arg0.resourceId,
|
||||
filename: arg0.filename,
|
||||
mime: arg0.mime,
|
||||
textToCopy: arg0.textToCopy,
|
||||
linkToCopy: arg0.linkToCopy || null,
|
||||
htmlToCopy: '',
|
||||
|
||||
@@ -231,6 +231,13 @@ class PromptDialog extends React.Component {
|
||||
}
|
||||
|
||||
const buttonComps = [];
|
||||
if (buttonTypes.indexOf('create') >= 0) {
|
||||
buttonComps.push(
|
||||
<button key="create" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'create')}>
|
||||
{_('Create')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
if (buttonTypes.indexOf('ok') >= 0) {
|
||||
buttonComps.push(
|
||||
<button key="ok" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'ok')}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
@@ -15,6 +15,7 @@ import Button from './Button/Button';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { getEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import SyncTargetRegistry from '../../lib/SyncTargetRegistry';
|
||||
const { clipboard } = require('electron');
|
||||
|
||||
interface Props {
|
||||
@@ -22,6 +23,7 @@ interface Props {
|
||||
noteIds: Array<string>;
|
||||
onClose: Function;
|
||||
shares: StateShare[];
|
||||
syncTargetId: number;
|
||||
}
|
||||
|
||||
function styles_(props: Props) {
|
||||
@@ -69,9 +71,10 @@ export function ShareNoteDialog(props: Props) {
|
||||
console.info('Render ShareNoteDialog');
|
||||
|
||||
const [notes, setNotes] = useState<NoteEntity[]>([]);
|
||||
const [recursiveShare, setRecursiveShare] = useState<boolean>(false);
|
||||
const [sharesState, setSharesState] = useState<string>('unknown');
|
||||
// const [shares, setShares] = useState<SharesMap>({});
|
||||
|
||||
const syncTargetInfo = useMemo(() => SyncTargetRegistry.infoById(props.syncTargetId), [props.syncTargetId]);
|
||||
const noteCount = notes.length;
|
||||
const theme = themeStyle(props.themeId);
|
||||
const styles = styles_(props);
|
||||
@@ -102,7 +105,7 @@ export function ShareNoteDialog(props: Props) {
|
||||
clipboard.writeText(links.join('\n'));
|
||||
};
|
||||
|
||||
const shareLinkButton_click = async () => {
|
||||
const shareLinkButton_click = useCallback(async () => {
|
||||
const service = ShareService.instance();
|
||||
|
||||
let hasSynced = false;
|
||||
@@ -121,7 +124,7 @@ export function ShareNoteDialog(props: Props) {
|
||||
const newShares: StateShare[] = [];
|
||||
|
||||
for (const note of notes) {
|
||||
const share = await service.shareNote(note.id);
|
||||
const share = await service.shareNote(note.id, recursiveShare);
|
||||
newShares.push(share);
|
||||
}
|
||||
|
||||
@@ -149,17 +152,7 @@ export function ShareNoteDialog(props: Props) {
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// const removeNoteButton_click = (event: any) => {
|
||||
// const newNotes = [];
|
||||
// for (let i = 0; i < notes.length; i++) {
|
||||
// const n = notes[i];
|
||||
// if (n.id === event.noteId) continue;
|
||||
// newNotes.push(n);
|
||||
// }
|
||||
// setNotes(newNotes);
|
||||
// };
|
||||
}, [recursiveShare, notes]);
|
||||
|
||||
const unshareNoteButton_click = async (event: any) => {
|
||||
await ShareService.instance().unshareNote(event.noteId);
|
||||
@@ -171,22 +164,6 @@ export function ShareNoteDialog(props: Props) {
|
||||
<Button tooltip={_('Unpublish note')} iconName="fas fa-share-alt" onClick={() => unshareNoteButton_click({ noteId: note.id })}/>
|
||||
);
|
||||
|
||||
// const removeButton = notes.length <= 1 ? null : (
|
||||
// <Button iconName="fa fa-times" onClick={() => removeNoteButton_click({ noteId: note.id })}/>
|
||||
// );
|
||||
|
||||
// const unshareButton = !shares[note.id] ? null : (
|
||||
// <button onClick={() => unshareNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
|
||||
// <i style={styles.noteRemoveButtonIcon} className={'fas fa-share-alt'}></i>
|
||||
// </button>
|
||||
// );
|
||||
|
||||
// const removeButton = notes.length <= 1 ? null : (
|
||||
// <button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
|
||||
// <i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
|
||||
// </button>
|
||||
// );
|
||||
|
||||
return (
|
||||
<div key={note.id} style={styles.note}>
|
||||
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
|
||||
@@ -214,11 +191,26 @@ export function ShareNoteDialog(props: Props) {
|
||||
return <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}<hr/></div>;
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
const onRecursiveShareChange = useCallback(() => {
|
||||
setRecursiveShare(v => !v);
|
||||
}, []);
|
||||
|
||||
const renderRecursiveShareCheckbox = () => {
|
||||
if (!syncTargetInfo.supportsRecursiveLinkedNotes) return null;
|
||||
|
||||
return (
|
||||
<div style={styles.root}>
|
||||
<div className="form-input-group form-input-group-checkbox">
|
||||
<input id="recursiveShare" name="recursiveShare" type="checkbox" checked={!!recursiveShare} onChange={onRecursiveShareChange} /> <label htmlFor="recursiveShare">{_('Also publish linked notes')}</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<div style={styles.root} className="form">
|
||||
<DialogTitle title={_('Publish Notes')}/>
|
||||
{renderNoteList(notes)}
|
||||
{renderRecursiveShareCheckbox()}
|
||||
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
|
||||
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
|
||||
{renderEncryptionWarningMessage()}
|
||||
@@ -230,7 +222,7 @@ export function ShareNoteDialog(props: Props) {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog renderContent={renderContent}/>
|
||||
@@ -240,6 +232,7 @@ export function ShareNoteDialog(props: Props) {
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
shares: state.shareService.shares.filter(s => !!s.note_id),
|
||||
syncTargetId: state.settings['sync.target'],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -17,11 +17,12 @@ import Folder from '@joplin/lib/models/Folder';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import { FolderEntity, FolderIcon } from '@joplin/lib/services/database/types';
|
||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||
import { store } from '@joplin/lib/reducer';
|
||||
import PerFolderSortOrderService from '../../services/sortOrder/PerFolderSortOrderService';
|
||||
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import FolderIconBox from '../FolderIconBox';
|
||||
const { connect } = require('react-redux');
|
||||
const shared = require('@joplin/lib/components/shared/side-menu-shared.js');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
@@ -77,6 +78,12 @@ function ExpandLink(props: any) {
|
||||
);
|
||||
}
|
||||
|
||||
const renderFolderIcon = (folderIcon: FolderIcon) => {
|
||||
if (!folderIcon) return null;
|
||||
|
||||
return <div style={{ marginRight: 5, display: 'flex' }}><FolderIconBox folderIcon={folderIcon}/></div>;
|
||||
};
|
||||
|
||||
function FolderItem(props: any) {
|
||||
const { hasChildren, isExpanded, parentId, depth, selected, folderId, folderTitle, folderIcon, anchorRef, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
|
||||
|
||||
@@ -84,8 +91,6 @@ function FolderItem(props: any) {
|
||||
|
||||
const shareIcon = shareId && !parentId ? <StyledShareIcon className="fas fa-share-alt"></StyledShareIcon> : null;
|
||||
|
||||
const icon = folderIcon ? <span style={{ fontSize: 20, marginRight: 5 }}>{folderIcon.emoji}</span> : null;
|
||||
|
||||
return (
|
||||
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth} ${selected ? 'selected' : ''}`} onDragStart={onFolderDragStart_} onDragOver={onFolderDragOver_} onDrop={onFolderDrop_} draggable={true} data-folder-id={folderId}>
|
||||
<ExpandLink themeId={props.themeId} hasChildren={hasChildren} folderId={folderId} onClick={onFolderToggleClick_} isExpanded={isExpanded}/>
|
||||
@@ -105,7 +110,7 @@ function FolderItem(props: any) {
|
||||
}}
|
||||
onDoubleClick={onFolderToggleClick_}
|
||||
>
|
||||
{icon}<span className="title" style={{ lineHeight: 0 }}>{folderTitle}</span>
|
||||
{renderFolderIcon(folderIcon)}<span className="title" style={{ lineHeight: 0 }}>{folderTitle}</span>
|
||||
{shareIcon} {noteCountComp}
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
|
||||
@@ -193,34 +193,36 @@ export default function(props: Props) {
|
||||
const onJoplinCloudLoginClick = useCallback(async () => {
|
||||
setJoplinCloudLoginInProgress(true);
|
||||
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
const result = await SyncTargetJoplinCloud.checkConfig({
|
||||
result = await SyncTargetJoplinCloud.checkConfig({
|
||||
password: () => joplinCloudPassword,
|
||||
path: () => Setting.value('sync.10.path'),
|
||||
userContentPath: () => Setting.value('sync.10.userContentPath'),
|
||||
username: () => joplinCloudEmail,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
Setting.setValue('sync.target', 10);
|
||||
Setting.setValue('sync.10.username', joplinCloudEmail);
|
||||
Setting.setValue('sync.10.password', joplinCloudPassword);
|
||||
await Setting.saveAll();
|
||||
|
||||
alert(_('Thank you! Your Joplin Cloud account is now setup and ready to use.'));
|
||||
|
||||
closeDialog(props.dispatch);
|
||||
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Main',
|
||||
});
|
||||
} else {
|
||||
alert(_('There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s', result.errorMessage));
|
||||
}
|
||||
} finally {
|
||||
setJoplinCloudLoginInProgress(false);
|
||||
}
|
||||
|
||||
if (result.ok) {
|
||||
Setting.setValue('sync.target', 10);
|
||||
Setting.setValue('sync.10.username', joplinCloudEmail);
|
||||
Setting.setValue('sync.10.password', joplinCloudPassword);
|
||||
await Setting.saveAll();
|
||||
|
||||
alert(_('Thank you! Your Joplin Cloud account is now setup and ready to use.'));
|
||||
|
||||
closeDialog(props.dispatch);
|
||||
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Main',
|
||||
});
|
||||
} else {
|
||||
alert(_('There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s', result.errorMessage));
|
||||
}
|
||||
}, [joplinCloudEmail, joplinCloudPassword, props.dispatch]);
|
||||
|
||||
const onJoplinCloudCreateAccountClick = useCallback(() => {
|
||||
@@ -230,10 +232,10 @@ export default function(props: Props) {
|
||||
function renderJoplinCloudLoginForm() {
|
||||
return (
|
||||
<JoplinCloudLoginForm>
|
||||
<div>{_('Login below.')} <CreateAccountLink href="#" onClick={onJoplinCloudCreateAccountClick}>{_('Or create an account.')}</CreateAccountLink></div>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<div style={{ fontSize: '16px' }}>{_('Login below.')} <CreateAccountLink href="#" onClick={onJoplinCloudCreateAccountClick}>{_('Or create an account.')}</CreateAccountLink></div>
|
||||
<FormLabel>{_('Email')}</FormLabel>
|
||||
<StyledInput type="email" onChange={onJoplinCloudEmailChange}/>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>{_('Password')}</FormLabel>
|
||||
<StyledInput type="password" onChange={onJoplinCloudPasswordChange}/>
|
||||
<SelectButton mt="1.3em" disabled={joplinCloudLoginInProgress} level={ButtonLevel.Primary} title={_('Login')} onClick={onJoplinCloudLoginClick}/>
|
||||
</JoplinCloudLoginForm>
|
||||
|
||||
@@ -40,8 +40,13 @@ export default function() {
|
||||
'toggleVisiblePanes',
|
||||
'editor.deleteLine',
|
||||
'editor.duplicateLine',
|
||||
'editor.undo',
|
||||
'editor.redo',
|
||||
// We cannot put the undo/redo commands in the menu because they are
|
||||
// editor-specific commands. If we put them there it will break the
|
||||
// undo/redo in regular text fields.
|
||||
// https://github.com/laurent22/joplin/issues/6214
|
||||
|
||||
// 'editor.undo',
|
||||
// 'editor.redo',
|
||||
'editor.indentLess',
|
||||
'editor.indentMore',
|
||||
'editor.toggleComment',
|
||||
@@ -55,5 +60,10 @@ export default function() {
|
||||
'gotoAnything',
|
||||
'commandPalette',
|
||||
'openMasterPasswordDialog',
|
||||
'addProfile',
|
||||
'editProfileConfig',
|
||||
'switchProfile1',
|
||||
'switchProfile2',
|
||||
'switchProfile3',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
const now = Date.now();
|
||||
if (now >= ignoreNextScrollTime_) ignoreNextScrollEventCount_ = 0;
|
||||
if (ignoreNextScrollEventCount_ < 10) { // for safety
|
||||
ignoreNextScrollTime_ = now + 200;
|
||||
ignoreNextScrollTime_ = now + 1000;
|
||||
ignoreNextScrollEventCount_ += 1;
|
||||
}
|
||||
};
|
||||
@@ -293,7 +293,7 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!heightChanged) return;
|
||||
if (!heightChanged && cause !== 'dom-changed') return;
|
||||
const restoreAndRefresh = () => {
|
||||
scrollmap.refresh();
|
||||
restorePercentScroll();
|
||||
@@ -337,7 +337,11 @@
|
||||
contentElement.innerHTML = html;
|
||||
|
||||
scrollmap.create(event.options.markupLineCount);
|
||||
restorePercentScroll(); // First, a quick treatment is applied.
|
||||
if (typeof event.options.percent !== 'number') {
|
||||
restorePercentScroll(); // First, a quick treatment is applied.
|
||||
} else {
|
||||
setPercentScroll(event.options.percent);
|
||||
}
|
||||
|
||||
addPluginAssets(event.options.pluginAssets);
|
||||
|
||||
@@ -586,9 +590,24 @@
|
||||
}));
|
||||
|
||||
document.addEventListener('contextmenu', webviewLib.logEnabledEventHandler(event => {
|
||||
let element = event.target;
|
||||
|
||||
// To handle right clicks on resource icons
|
||||
let element = event.target;
|
||||
|
||||
// Mermaid svgs are wrapped inside a <pre> with class "mermaid"
|
||||
let mermaidElement = element.closest(".mermaid")?.children[0];
|
||||
if (mermaidElement) {
|
||||
const svgString = new XMLSerializer().serializeToString(mermaidElement);
|
||||
if (!!svgString) {
|
||||
ipcProxySendToHost('contextMenu', {
|
||||
type: 'image',
|
||||
textToCopy: svgString,
|
||||
mime: 'image/svg+xml',
|
||||
filename: mermaidElement.id + '.svg',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (element && !element.getAttribute('data-resource-id')) element = element.parentElement;
|
||||
|
||||
if (element && element.getAttribute('data-resource-id')) {
|
||||
|
||||
@@ -45,7 +45,8 @@ scrollmap.get_ = () => {
|
||||
// Each map entry is total-ordered.
|
||||
let last = 0;
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
const top = elems[i].getBoundingClientRect().top - offset;
|
||||
const rect = elems[i].getBoundingClientRect();
|
||||
const top = rect.top - offset;
|
||||
const line = Number(elems[i].getAttribute('source-line'));
|
||||
const percent = Math.max(0, Math.min(1, top / height));
|
||||
if (map.line[last] < line && map.percent[last] < percent) {
|
||||
@@ -53,12 +54,20 @@ scrollmap.get_ = () => {
|
||||
map.percent.push(percent);
|
||||
last += 1;
|
||||
}
|
||||
const bottom = rect.bottom - offset;
|
||||
const lineEnd = Number(elems[i].getAttribute('source-line-end'));
|
||||
const percentEnd = Math.max(0, Math.min(1, bottom / height));
|
||||
if (map.line[last] < lineEnd && map.percent[last] < percentEnd) {
|
||||
map.line.push(lineEnd);
|
||||
map.percent.push(percentEnd);
|
||||
last += 1;
|
||||
}
|
||||
}
|
||||
const lineCount = scrollmap.lineCount_;
|
||||
if (lineCount) {
|
||||
map.lineCount = lineCount;
|
||||
} else {
|
||||
if (map.lineCount <= map.line[last]) map.lineCount = map.line[last] + 1;
|
||||
if (map.lineCount < map.line[last]) map.lineCount = map.line[last];
|
||||
}
|
||||
if (map.percent[last] < 1) {
|
||||
map.line.push(lineCount || 1e10);
|
||||
|
||||
20
packages/app-desktop/loadResources.testEnv.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* A Jest custom test Environment to load the resources for the tests.
|
||||
* Use this test envirenment when you work with resources like images, files.
|
||||
* See gui/NoteEditor/utils/contextMenu.test.ts for an example.
|
||||
*/
|
||||
|
||||
const JSDOMEnvironment = require('jest-environment-jsdom');
|
||||
import type { EnvironmentContext } from '@jest/environment';
|
||||
import type { Config } from '@jest/types';
|
||||
|
||||
|
||||
export default class CustomEnvironment extends JSDOMEnvironment {
|
||||
constructor(config: Config.ProjectConfig, context?: EnvironmentContext) {
|
||||
// Resources is set to 'usable' to enable fetching of resources like images and fonts while testing
|
||||
// Which does not happen by default in jest
|
||||
// https://stackoverflow.com/a/49482563
|
||||
config.testEnvironmentOptions.resources = 'usable';
|
||||
super(config, context);
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,15 @@ document.addEventListener('auxclick', event => event.preventDefault());
|
||||
// Each link (rendered as a button or list item) has its own custom click event
|
||||
// so disable the default. In particular this will disable Ctrl+Clicking a link
|
||||
// which would open a new browser window.
|
||||
document.addEventListener('click', (event) => event.preventDefault());
|
||||
document.addEventListener('click', (event) => {
|
||||
// We don't apply this to labels and inputs because it would break
|
||||
// checkboxes. Such a global event handler is probably not a good idea
|
||||
// anyway but keeping it for now, as it doesn't seem to break anything else.
|
||||
// https://github.com/facebook/react/issues/13477#issuecomment-489274045
|
||||
if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return;
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
app().start(bridge().processArgv()).then((result) => {
|
||||
if (!result || !result.action) {
|
||||
|
||||
@@ -180,6 +180,22 @@ h2 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form > .form-input-group-checkbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form > .form-input-group-checkbox > input {
|
||||
display: flex;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.form > .form-input-group-checkbox > label {
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.7.8",
|
||||
"version": "2.7.14",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -67,7 +67,7 @@
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"allowToChangeInstallationDirectory": false,
|
||||
"differentialPackage": false
|
||||
},
|
||||
"portable": {
|
||||
@@ -116,6 +116,7 @@
|
||||
"app-builder-bin": "^1.9.11",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"canvas": "^2.9.0",
|
||||
"electron": "14.1.0",
|
||||
"electron-builder": "^22.11.7",
|
||||
"electron-notarize": "^1.0.0",
|
||||
|
||||
@@ -146,8 +146,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097665
|
||||
versionName "2.7.0"
|
||||
versionCode 2097667
|
||||
versionName "2.7.2"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ class CameraView extends Component {
|
||||
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'flex-end' }}>
|
||||
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginBottom: 20 }}>
|
||||
{ reverseCameraButton }
|
||||
<TouchableOpacity onPress={this.photo_onPress}>
|
||||
<TouchableOpacity onPress={this.photo_onPress} disabled={this.state.snapping}>
|
||||
<View style={{ flexDirection: 'row', borderRadius: 90, width: 90, height: 90, backgroundColor: '#ffffffaa', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Icon
|
||||
name={photoIcon}
|
||||
|
||||
@@ -38,10 +38,9 @@ class Dropdown extends React.Component {
|
||||
const listTop = Math.min(maxListTop, this.state.headerSize.y + this.state.headerSize.height);
|
||||
|
||||
const wrapperStyle = {
|
||||
width: this.state.headerSize.width,
|
||||
height: listHeight + 2, // +2 for the border (otherwise it makes the scrollbar appear)
|
||||
marginTop: listTop,
|
||||
marginLeft: this.state.headerSize.x,
|
||||
alignSelf: 'center',
|
||||
};
|
||||
|
||||
const itemListStyle = Object.assign({}, this.props.itemListStyle ? this.props.itemListStyle : {}, {
|
||||
@@ -87,6 +86,7 @@ class Dropdown extends React.Component {
|
||||
if (this.props.labelTransform && this.props.labelTransform === 'trim') headerLabel = headerLabel.trim();
|
||||
|
||||
const closeList = () => {
|
||||
if (this.props.onClose()) this.props.onClose();
|
||||
this.setState({ listVisible: false });
|
||||
};
|
||||
|
||||
@@ -116,6 +116,7 @@ class Dropdown extends React.Component {
|
||||
onPress={() => {
|
||||
this.updateHeaderCoordinates();
|
||||
this.setState({ listVisible: true });
|
||||
if (this.props.onOpen()) this.props.onOpen();
|
||||
}}
|
||||
>
|
||||
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>
|
||||
|
||||
@@ -19,6 +19,8 @@ interface CodeMirrorResult {
|
||||
editor: EditorView;
|
||||
undo: Function;
|
||||
redo: Function;
|
||||
select: (anchor: number, head: number)=> void;
|
||||
insertText: (text: string)=> void;
|
||||
}
|
||||
|
||||
function postMessage(name: string, data: any) {
|
||||
@@ -36,25 +38,53 @@ function logMessage(...msg: any[]) {
|
||||
//
|
||||
// https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
|
||||
//
|
||||
// For a tutorial, see:
|
||||
//
|
||||
// https://codemirror.net/6/examples/styling/#themes
|
||||
//
|
||||
// Use Safari developer tools to view the content of the CodeMirror iframe while
|
||||
// the app is running. It seems that what appears as ".ͼ1" in the CSS is the
|
||||
// equivalent of "&" in the theme object. So to target ".ͼ1.cm-focused", you'd
|
||||
// use '&.cm-focused' in the theme.
|
||||
const createTheme = (theme: any): Extension => {
|
||||
const isDarkTheme = theme.appearance === 'dark';
|
||||
|
||||
const baseGlobalStyle: Record<string, string> = {
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: `${theme.fontSize}px`,
|
||||
};
|
||||
const baseCursorStyle: Record<string, string> = { };
|
||||
const baseContentStyle: Record<string, string> = { };
|
||||
const baseSelectionStyle: Record<string, string> = { };
|
||||
|
||||
// If we're in dark mode, the caret and selection are difficult to see.
|
||||
// Adjust them appropriately
|
||||
if (isDarkTheme) {
|
||||
// Styling the caret requires styling both the caret itself
|
||||
// and the CodeMirror caret.
|
||||
// See https://codemirror.net/6/examples/styling/#themes
|
||||
baseContentStyle.caretColor = 'white';
|
||||
baseCursorStyle.borderLeftColor = 'white';
|
||||
|
||||
baseSelectionStyle.backgroundColor = '#6b6b6b';
|
||||
}
|
||||
|
||||
const baseTheme = EditorView.baseTheme({
|
||||
'&': {
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: `${theme.fontSize}px`,
|
||||
},
|
||||
'&': baseGlobalStyle,
|
||||
|
||||
// These must be !important or more specific than CodeMirror's built-ins
|
||||
'.cm-content': baseContentStyle,
|
||||
'&.cm-focused .cm-cursor': baseCursorStyle,
|
||||
'&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle,
|
||||
|
||||
'&.cm-focused': {
|
||||
outline: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
const appearanceTheme = EditorView.theme({}, { dark: theme.appearance === 'dark' });
|
||||
const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme });
|
||||
|
||||
const baseHeadingStyle = {
|
||||
fontWeight: 'bold',
|
||||
@@ -152,6 +182,13 @@ export function initCodeMirror(parentElement: any, initialText: string, theme: a
|
||||
postMessage('onChange', { value: editor.state.doc.toString() });
|
||||
schedulePostUndoRedoDepthChange(editor);
|
||||
}
|
||||
|
||||
if (!viewUpdate.state.selection.eq(viewUpdate.startState.selection)) {
|
||||
const mainRange = viewUpdate.state.selection.main;
|
||||
const selStart = mainRange.from;
|
||||
const selEnd = mainRange.to;
|
||||
postMessage('onSelectionChange', { selection: { start: selStart, end: selEnd } });
|
||||
}
|
||||
}),
|
||||
],
|
||||
doc: initialText,
|
||||
@@ -169,5 +206,14 @@ export function initCodeMirror(parentElement: any, initialText: string, theme: a
|
||||
redo(editor);
|
||||
schedulePostUndoRedoDepthChange(editor, true);
|
||||
},
|
||||
select: (anchor: number, head: number) => {
|
||||
editor.dispatch(editor.state.update({
|
||||
selection: { anchor, head },
|
||||
scrollIntoView: true,
|
||||
}));
|
||||
},
|
||||
insertText: (text: string) => {
|
||||
editor.dispatch(editor.state.replaceSelection(text));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
const React = require('react');
|
||||
const { forwardRef, useImperativeHandle, useEffect, useState, useCallback, useRef } = require('react');
|
||||
const { forwardRef, useImperativeHandle, useEffect, useMemo, useState, useCallback, useRef } = require('react');
|
||||
const { WebView } = require('react-native-webview');
|
||||
const { editorFont } = require('../global-style');
|
||||
|
||||
@@ -15,14 +15,27 @@ export interface UndoRedoDepthChangeEvent {
|
||||
redoDepth: number;
|
||||
}
|
||||
|
||||
export interface Selection {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface SelectionChangeEvent {
|
||||
selection: Selection;
|
||||
}
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
type SelectionChangeEventHandler = (event: SelectionChangeEvent)=> void;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
initialText: string;
|
||||
initialSelection?: Selection;
|
||||
style: any;
|
||||
|
||||
onChange: ChangeEventHandler;
|
||||
onSelectionChange: SelectionChangeEventHandler;
|
||||
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
|
||||
}
|
||||
|
||||
@@ -31,6 +44,7 @@ function fontFamilyFromSettings() {
|
||||
return [f, 'sans-serif'].join(', ');
|
||||
}
|
||||
|
||||
// Obsolete with CodeMirror 6. See ./CodeMirror.ts for styling.
|
||||
// function useCss(themeId:number):string {
|
||||
// const [css, setCss] = useState('');
|
||||
|
||||
@@ -169,6 +183,17 @@ function fontFamilyFromSettings() {
|
||||
// return css;
|
||||
// }
|
||||
|
||||
function useCss(themeId: number): string {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return `
|
||||
:root {
|
||||
background-color: ${theme.backgroundColor};
|
||||
}
|
||||
`;
|
||||
}, [themeId]);
|
||||
}
|
||||
|
||||
function useHtml(css: string): string {
|
||||
const [html, setHtml] = useState('');
|
||||
|
||||
@@ -211,11 +236,15 @@ function NoteEditor(props: Props, ref: any) {
|
||||
const [source, setSource] = useState(undefined);
|
||||
const webviewRef = useRef(null);
|
||||
|
||||
const setInitialSelectionJS = props.initialSelection ? `
|
||||
cm.select(${props.initialSelection.start}, ${props.initialSelection.end});
|
||||
` : '';
|
||||
|
||||
const injectedJavaScript = `
|
||||
function postMessage(name, data) {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||
data,
|
||||
name,
|
||||
name,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -226,7 +255,7 @@ function NoteEditor(props: Props, ref: any) {
|
||||
// This variable is not used within this script
|
||||
// but is called using "injectJavaScript" from
|
||||
// the wrapper component.
|
||||
let cm = null;
|
||||
window.cm = null;
|
||||
|
||||
try {
|
||||
${shim.injectedJs('codeMirrorBundle')};
|
||||
@@ -236,6 +265,7 @@ function NoteEditor(props: Props, ref: any) {
|
||||
const initialText = ${JSON.stringify(props.initialText)};
|
||||
|
||||
cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, theme);
|
||||
${setInitialSelectionJS}
|
||||
} catch (e) {
|
||||
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
|
||||
} finally {
|
||||
@@ -243,8 +273,8 @@ function NoteEditor(props: Props, ref: any) {
|
||||
}
|
||||
`;
|
||||
|
||||
// const css = useCss(props.themeId);
|
||||
const html = useHtml('');
|
||||
const css = useCss(props.themeId);
|
||||
const html = useHtml(css);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
@@ -254,6 +284,14 @@ function NoteEditor(props: Props, ref: any) {
|
||||
redo: function() {
|
||||
webviewRef.current.injectJavaScript('cm.redo(); true;');
|
||||
},
|
||||
select: (anchor: number, head: number) => {
|
||||
webviewRef.current.injectJavaScript(
|
||||
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)}); true;`
|
||||
);
|
||||
},
|
||||
insertText: (text: string) => {
|
||||
webviewRef.current.injectJavaScript(`cm.insertText(${JSON.stringify(text)}); true;`);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -300,6 +338,10 @@ function NoteEditor(props: Props, ref: any) {
|
||||
console.info('onUndoRedoDepthChange', event);
|
||||
props.onUndoRedoDepthChange(event);
|
||||
},
|
||||
|
||||
onSelectionChange: (event: SelectionChangeEvent) => {
|
||||
props.onSelectionChange(event);
|
||||
},
|
||||
};
|
||||
|
||||
if (handlers[msg.name]) {
|
||||
|
||||
@@ -103,7 +103,7 @@ export default class SelectDateTimeDialog extends React.PureComponent<any, any>
|
||||
return (
|
||||
<View style={{ flex: 0, margin: 20, alignItems: 'center' }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
{ this.state.date && <Text style={{ ...theme.normalText, marginRight: 10 }}>{time.formatDateToLocal(this.state.date)}</Text> }
|
||||
{ this.state.date && <Text style={{ ...theme.normalText,color: theme.color, marginRight: 10 }}>{time.formatDateToLocal(this.state.date)}</Text> }
|
||||
<Button title="Set date" onPress={this.onSetDate} />
|
||||
</View>
|
||||
<DateTimePickerModal
|
||||
|
||||
@@ -29,8 +29,10 @@ class ScreenHeaderComponent extends React.PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.styles_ = {};
|
||||
this.state = { showUndoRedoButtons: true };
|
||||
}
|
||||
|
||||
|
||||
styles() {
|
||||
const themeId = Setting.value('theme');
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
@@ -256,7 +258,7 @@ class ScreenHeaderComponent extends React.PureComponent {
|
||||
}
|
||||
|
||||
const renderTopButton = (options) => {
|
||||
if (!options.visible) return null;
|
||||
if (!options.visible || !this.state.showUndoRedoButtons) return null;
|
||||
|
||||
const icon = <Icon name={options.iconName} style={this.styles().topIcon} />;
|
||||
const viewStyle = options.disabled ? this.styles().iconButtonDisabled : this.styles().iconButton;
|
||||
@@ -422,6 +424,16 @@ class ScreenHeaderComponent extends React.PureComponent {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
onOpen={() => {
|
||||
this.setState({
|
||||
showUndoRedoButtons: false,
|
||||
});
|
||||
}}
|
||||
onClose={() => {
|
||||
this.setState({
|
||||
showUndoRedoButtons: true,
|
||||
});
|
||||
}}
|
||||
onValueChange={async (folderId, itemIndex) => {
|
||||
// If onValueChange is specified, use this as a callback, otherwise do the default
|
||||
// which is to take the selectedNoteIds from the state and move them to the
|
||||
|
||||
@@ -530,9 +530,9 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
if (this.state.profileExportStatus === 'prompt') {
|
||||
const profileExportPrompt = (
|
||||
<View style={this.styles().settingContainer} key="profileExport">
|
||||
<Text style={this.styles().settingText}>Path:</Text>
|
||||
<TextInput style={{ ...this.styles().textInput, paddingRight: 20 }} onChange={(event: any) => this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard" keyboardAppearance={theme.keyboardAppearance}></TextInput>
|
||||
<Button title="OK" onPress={this.exportProfileButtonPress2_}></Button>
|
||||
<Text style={{ ...this.styles().settingText, flex: 0 }}>Path:</Text>
|
||||
<TextInput style={{ ...this.styles().textInput, paddingRight: 20, width: '75%', marginRight: 'auto' }} onChange={(event: any) => this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard" keyboardAppearance={theme.keyboardAppearance} />
|
||||
<Button title="OK" onPress={this.exportProfileButtonPress2_} />
|
||||
</View>
|
||||
);
|
||||
|
||||
|
||||
@@ -93,9 +93,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
this.doFocusUpdate_ = false;
|
||||
|
||||
// iOS doesn't support multiline text fields properly so disable it
|
||||
this.enableMultilineTitle_ = Platform.OS !== 'ios';
|
||||
|
||||
this.saveButtonHasBeenShown_ = false;
|
||||
|
||||
this.styles_ = {};
|
||||
@@ -231,7 +228,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.onAlarmDialogAccept = this.onAlarmDialogAccept.bind(this);
|
||||
this.onAlarmDialogReject = this.onAlarmDialogReject.bind(this);
|
||||
this.todoCheckbox_change = this.todoCheckbox_change.bind(this);
|
||||
this.titleTextInput_contentSizeChange = this.titleTextInput_contentSizeChange.bind(this);
|
||||
this.title_changeText = this.title_changeText.bind(this);
|
||||
this.undoRedoService_stackChange = this.undoRedoService_stackChange.bind(this);
|
||||
this.screenHeader_undoButtonPress = this.screenHeader_undoButtonPress.bind(this);
|
||||
@@ -389,7 +385,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
paddingBottom: 10, // Added for iOS (Not needed for Android??)
|
||||
};
|
||||
|
||||
if (this.enableMultilineTitle_) styles.titleTextInput.height = this.state.titleTextInputHeight;
|
||||
if (this.state.HACK_webviewLoadingState === 1) styles.titleTextInput.marginTop = 1;
|
||||
|
||||
this.styles_[cacheKey] = StyleSheet.create(styles);
|
||||
@@ -493,7 +488,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
body_selectionChange(event: any) {
|
||||
this.selection = event.nativeEvent.selection;
|
||||
if (this.useEditorBeta()) {
|
||||
this.selection = event.selection;
|
||||
} else {
|
||||
this.selection = event.nativeEvent.selection;
|
||||
}
|
||||
}
|
||||
|
||||
makeSaveAction() {
|
||||
@@ -713,9 +712,17 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
const newNote = Object.assign({}, this.state.note);
|
||||
|
||||
if (this.state.mode == 'edit' && !!this.selection) {
|
||||
const newText = `\n${resourceTag}\n`;
|
||||
|
||||
const prefix = newNote.body.substring(0, this.selection.start);
|
||||
const suffix = newNote.body.substring(this.selection.end);
|
||||
newNote.body = `${prefix}\n${resourceTag}\n${suffix}`;
|
||||
newNote.body = `${prefix}${newText}${suffix}`;
|
||||
|
||||
if (this.useEditorBeta()) {
|
||||
// The beta editor needs to be explicitly informed of changes
|
||||
// to the note's body
|
||||
this.editorRef.current.insertText(newText);
|
||||
}
|
||||
} else {
|
||||
newNote.body += `\n${resourceTag}`;
|
||||
}
|
||||
@@ -884,11 +891,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
output.push({
|
||||
title: _('Attach...'),
|
||||
onPress: async () => {
|
||||
if (this.state.mode === 'edit' && this.useEditorBeta()) {
|
||||
alert('Attaching files from the beta editor is not yet supported. You may do so from the viewer mode instead.');
|
||||
return;
|
||||
}
|
||||
|
||||
const buttons = [];
|
||||
|
||||
// On iOS, it will show "local files", which means certain files saved from the browser
|
||||
@@ -971,13 +973,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
await this.saveOneProperty('todo_completed', checked ? time.unixMs() : 0);
|
||||
}
|
||||
|
||||
titleTextInput_contentSizeChange(event: any) {
|
||||
if (!this.enableMultilineTitle_) return;
|
||||
|
||||
const height = event.nativeEvent.contentSize.height;
|
||||
this.setState({ titleTextInputHeight: height });
|
||||
}
|
||||
|
||||
scheduleFocusUpdate() {
|
||||
if (this.focusUpdateIID_) shim.clearTimeout(this.focusUpdateIID_);
|
||||
|
||||
@@ -1137,7 +1132,9 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
ref={this.editorRef}
|
||||
themeId={this.props.themeId}
|
||||
initialText={note.body}
|
||||
initialSelection={this.selection}
|
||||
onChange={this.onBodyChange}
|
||||
onSelectionChange={this.body_selectionChange}
|
||||
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
|
||||
style={this.styles().bodyTextInput}
|
||||
/>;
|
||||
@@ -1178,8 +1175,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
<View style={titleContainerStyle}>
|
||||
{isTodo && <Checkbox style={this.styles().checkbox} checked={!!Number(note.todo_completed)} onChange={this.todoCheckbox_change} />}
|
||||
<TextInput
|
||||
onContentSizeChange={this.titleTextInput_contentSizeChange}
|
||||
multiline={this.enableMultilineTitle_}
|
||||
ref="titleTextField"
|
||||
underlineColorAndroid="#ffffff00"
|
||||
autoCapitalize="sentences"
|
||||
|
||||
@@ -21,6 +21,7 @@ class NoteTagsDialogComponent extends React.Component {
|
||||
tagListData: [],
|
||||
newTags: '',
|
||||
savingTags: false,
|
||||
tagFilter: '',
|
||||
};
|
||||
|
||||
const noteHasTag = tagId => {
|
||||
@@ -88,6 +89,10 @@ class NoteTagsDialogComponent extends React.Component {
|
||||
this.cancelButton_press = () => {
|
||||
if (this.props.onCloseRequested) this.props.onCloseRequested();
|
||||
};
|
||||
|
||||
this.filterTags = (allTags) => {
|
||||
return allTags.filter((tag) => tag.title.includes(this.state.tagFilter.toLowerCase()), allTags);
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
@@ -140,16 +145,16 @@ class NoteTagsDialogComponent extends React.Component {
|
||||
fontSize: 20,
|
||||
color: theme.color,
|
||||
},
|
||||
newTagBox: {
|
||||
tagBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
},
|
||||
newTagBoxLabel: Object.assign({}, theme.normalText, { marginRight: 8 }),
|
||||
newTagBoxInput: Object.assign({}, theme.lineInput, { flex: 1 }),
|
||||
tagBoxInput: Object.assign({}, theme.lineInput, { flex: 1 }),
|
||||
};
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
@@ -161,7 +166,7 @@ class NoteTagsDialogComponent extends React.Component {
|
||||
|
||||
const dialogContent = (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={this.styles().newTagBox}>
|
||||
<View style={this.styles().tagBox}>
|
||||
<Text style={this.styles().newTagBoxLabel}>{_('New tags:')}</Text>
|
||||
<TextInput
|
||||
selectionColor={theme.textSelectionColor}
|
||||
@@ -170,10 +175,23 @@ class NoteTagsDialogComponent extends React.Component {
|
||||
onChangeText={value => {
|
||||
this.setState({ newTags: value });
|
||||
}}
|
||||
style={this.styles().newTagBoxInput}
|
||||
style={this.styles().tagBoxInput}
|
||||
placeholder={_('tag1,tag2,...')}
|
||||
/>
|
||||
</View>
|
||||
<FlatList data={this.state.tagListData} renderItem={this.renderTag} keyExtractor={this.tagKeyExtractor} />
|
||||
<View style={this.styles().tagBox}>
|
||||
<TextInput
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
value={this.state.tagFilter}
|
||||
onChangeText={value => {
|
||||
this.setState({ tagFilter: value });
|
||||
}}
|
||||
placeholder={_('Filter tags')}
|
||||
style={this.styles().tagBoxInput}
|
||||
/>
|
||||
</View>
|
||||
<FlatList data={this.filterTags(this.state.tagListData)} renderItem={this.renderTag} keyExtractor={this.tagKeyExtractor} />
|
||||
</View>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Alert } = require('react-native');
|
||||
const { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Alert, Image } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const Folder = require('@joplin/lib/models/Folder').default;
|
||||
@@ -74,7 +74,7 @@ class SideMenuContentComponent extends Component {
|
||||
|
||||
styles.folderButton = Object.assign({}, styles.button);
|
||||
styles.folderButton.paddingLeft = 0;
|
||||
styles.folderButtonText = Object.assign({}, styles.buttonText);
|
||||
styles.folderButtonText = Object.assign({}, styles.buttonText, { paddingLeft: 0 });
|
||||
styles.folderButtonSelected = Object.assign({}, styles.folderButton);
|
||||
styles.folderButtonSelected.backgroundColor = theme.selectedColor;
|
||||
styles.folderIcon = Object.assign({}, theme.icon);
|
||||
@@ -219,6 +219,18 @@ class SideMenuContentComponent extends Component {
|
||||
if (actionDone === 'auth') this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
}
|
||||
|
||||
renderFolderIcon(theme, folderIcon) {
|
||||
if (!folderIcon) return null;
|
||||
|
||||
if (folderIcon.type === 1) { // FolderIconType.Emoji
|
||||
return <Text style={{ fontSize: theme.fontSize, marginRight: 4 }}>{folderIcon.emoji}</Text>;
|
||||
} else if (folderIcon.type === 2) { // FolderIconType.DataUrl
|
||||
return <Image style={{ width: 20, height: 20, marginRight: 4, resizeMode: 'contain' }} source={{ uri: folderIcon.dataUrl }}/>;
|
||||
} else {
|
||||
throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
renderFolderItem(folder, selected, hasChildren, depth) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
@@ -228,6 +240,7 @@ class SideMenuContentComponent extends Component {
|
||||
height: 36,
|
||||
alignItems: 'center',
|
||||
paddingRight: theme.marginRight,
|
||||
paddingLeft: 10,
|
||||
};
|
||||
if (selected) folderButtonStyle.backgroundColor = theme.selectedColor;
|
||||
folderButtonStyle.paddingLeft = depth * 10 + theme.marginLeft;
|
||||
@@ -253,7 +266,6 @@ class SideMenuContentComponent extends Component {
|
||||
);
|
||||
|
||||
const folderIcon = Folder.unserializeIcon(folder.icon);
|
||||
const icon = folderIcon ? `${folderIcon.emoji} ` : '';
|
||||
|
||||
return (
|
||||
<View key={folder.id} style={{ flex: 1, flexDirection: 'row' }}>
|
||||
@@ -267,8 +279,9 @@ class SideMenuContentComponent extends Component {
|
||||
}}
|
||||
>
|
||||
<View style={folderButtonStyle}>
|
||||
{this.renderFolderIcon(theme, folderIcon)}
|
||||
<Text numberOfLines={1} style={this.styles().folderButtonText}>
|
||||
{icon + Folder.displayTitle(folder)}
|
||||
{Folder.displayTitle(folder)}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -492,13 +492,13 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 79;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.7.0;
|
||||
MARKETING_VERSION = 12.7.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -521,12 +521,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 79;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.7.0;
|
||||
MARKETING_VERSION = 12.7.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -667,14 +667,14 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 79;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.7.0;
|
||||
MARKETING_VERSION = 12.7.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
@@ -698,14 +698,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 79;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.7.0;
|
||||
MARKETING_VERSION = 12.7.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -43,6 +43,7 @@ module.exports = {
|
||||
'@joplin/renderer': path.resolve(__dirname, '../renderer/'),
|
||||
'@joplin/tools': path.resolve(__dirname, '../tools/'),
|
||||
'@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
|
||||
'@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
|
||||
},
|
||||
{
|
||||
get: (target, name) => {
|
||||
@@ -60,5 +61,6 @@ module.exports = {
|
||||
path.resolve(__dirname, '../renderer'),
|
||||
path.resolve(__dirname, '../tools'),
|
||||
path.resolve(__dirname, '../fork-htmlparser2'),
|
||||
path.resolve(__dirname, '../fork-uslug'),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -550,6 +550,7 @@ async function initialize(dispatch: Function) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
BaseItem.encryptionService_ = EncryptionService.instance();
|
||||
BaseItem.shareService_ = ShareService.instance();
|
||||
Resource.shareService_ = ShareService.instance();
|
||||
DecryptionWorker.instance().dispatch = dispatch;
|
||||
DecryptionWorker.instance().setLogger(mainLogger);
|
||||
DecryptionWorker.instance().setKvStore(KvStore.instance());
|
||||
|
||||
@@ -8,5 +8,6 @@
|
||||
"author": "<%= pluginAuthor %>",
|
||||
"homepage_url": "<%= pluginHomepageUrl %>",
|
||||
"repository_url": "<%= pluginRepositoryUrl %>",
|
||||
"keywords": []
|
||||
"keywords": [],
|
||||
"categories": []
|
||||
}
|
||||
@@ -29,6 +29,7 @@ const userConfig = Object.assign({}, {
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const allPossibleCategories = ['appearance', 'developer tools', 'productivity', 'themes', 'integrations', 'viewer', 'search', 'tags', 'editor', 'files', 'personal knowledge management'];
|
||||
const manifest = readManifest(manifestPath);
|
||||
const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
|
||||
const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`);
|
||||
@@ -67,10 +68,19 @@ function currentGitInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
function validateCategories(categories) {
|
||||
if (!categories) return null;
|
||||
if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed');
|
||||
categories.forEach(category => {
|
||||
if (!allPossibleCategories.includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid Categories are: \n${allPossibleCategories}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
function readManifest(manifestPath) {
|
||||
const content = fs.readFileSync(manifestPath, 'utf8');
|
||||
const output = JSON.parse(content);
|
||||
if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`);
|
||||
validateCategories(output.categories);
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ import { loadMasterKeysFromSettings, migrateMasterPassword } from './services/e2
|
||||
import SyncTargetNone from './SyncTargetNone';
|
||||
import { setRSA } from './services/e2ee/ppk';
|
||||
import RSA from './services/e2ee/RSA.node';
|
||||
import Resource from './models/Resource';
|
||||
import { ProfileConfig } from './services/profileConfig/types';
|
||||
import initProfile from './services/profileConfig/initProfile';
|
||||
|
||||
const appLogger: LoggerWrapper = Logger.create('App');
|
||||
|
||||
@@ -69,6 +72,7 @@ export default class BaseApplication {
|
||||
private eventEmitter_: any;
|
||||
private scheduleAutoAddResourcesIID_: any = null;
|
||||
private database_: any = null;
|
||||
private profileConfig_: ProfileConfig = null;
|
||||
|
||||
protected showStackTraces_: boolean = false;
|
||||
protected showPromptString_: boolean = false;
|
||||
@@ -645,6 +649,12 @@ export default class BaseApplication {
|
||||
public initRedux() {
|
||||
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn() as any));
|
||||
setStore(this.store_);
|
||||
|
||||
this.store_.dispatch({
|
||||
type: 'PROFILE_CONFIG_SET',
|
||||
value: this.profileConfig_,
|
||||
});
|
||||
|
||||
BaseModel.dispatch = this.store().dispatch;
|
||||
FoldersScreenUtils.dispatch = this.store().dispatch;
|
||||
// reg.dispatch = this.store().dispatch;
|
||||
@@ -713,14 +723,16 @@ export default class BaseApplication {
|
||||
// https://immerjs.github.io/immer/docs/freezing
|
||||
setAutoFreeze(initArgs.env === 'dev');
|
||||
|
||||
const profileDir = this.determineProfileDir(initArgs);
|
||||
const rootProfileDir = this.determineProfileDir(initArgs);
|
||||
const { profileDir, profileConfig, isSubProfile } = await initProfile(rootProfileDir);
|
||||
this.profileConfig_ = profileConfig;
|
||||
|
||||
const resourceDirName = 'resources';
|
||||
const resourceDir = `${profileDir}/${resourceDirName}`;
|
||||
const tempDir = `${profileDir}/tmp`;
|
||||
const cacheDir = `${profileDir}/cache`;
|
||||
|
||||
Setting.setConstant('env', initArgs.env);
|
||||
Setting.setConstant('profileDir', profileDir);
|
||||
Setting.setConstant('resourceDirName', resourceDirName);
|
||||
Setting.setConstant('resourceDir', resourceDir);
|
||||
Setting.setConstant('tempDir', tempDir);
|
||||
@@ -777,6 +789,7 @@ export default class BaseApplication {
|
||||
|
||||
|
||||
appLogger.info(`Profile directory: ${profileDir}`);
|
||||
appLogger.info(`Root profile directory: ${rootProfileDir}`);
|
||||
|
||||
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
|
||||
this.database_.setLogExcludedQueryTypes(['SELECT']);
|
||||
@@ -819,7 +832,7 @@ export default class BaseApplication {
|
||||
// Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
|
||||
// Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
|
||||
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
|
||||
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
|
||||
Setting.setValue('sync.10.userContentPath', 'http://joplincloud.local:22300');
|
||||
}
|
||||
|
||||
// For now always disable fuzzy search due to performance issues:
|
||||
@@ -837,6 +850,7 @@ export default class BaseApplication {
|
||||
}
|
||||
|
||||
if ('welcomeDisabled' in initArgs) Setting.setValue('welcome.enabled', !initArgs.welcomeDisabled);
|
||||
if (isSubProfile) Setting.setValue('welcome.enabled', false);
|
||||
|
||||
if (!Setting.value('api.token')) {
|
||||
void EncryptionService.instance()
|
||||
@@ -855,6 +869,7 @@ export default class BaseApplication {
|
||||
|
||||
BaseItem.encryptionService_ = EncryptionService.instance();
|
||||
BaseItem.shareService_ = ShareService.instance();
|
||||
Resource.shareService_ = ShareService.instance();
|
||||
DecryptionWorker.instance().setLogger(globalLogger);
|
||||
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
|
||||
DecryptionWorker.instance().setKvStore(KvStore.instance());
|
||||
|
||||
@@ -33,6 +33,10 @@ export default class BaseSyncTarget {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static supportsRecursiveLinkedNotes(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public option(name: string, defaultValue: any = null) {
|
||||
return this.options_ && name in this.options_ ? this.options_[name] : defaultValue;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static supportsRecursiveLinkedNotes(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4,33 +4,16 @@ export interface SyncTargetInfo {
|
||||
label: string;
|
||||
supportsSelfHosted: boolean;
|
||||
supportsConfigCheck: boolean;
|
||||
supportsRecursiveLinkedNotes: boolean;
|
||||
description: string;
|
||||
classRef: any;
|
||||
}
|
||||
|
||||
// const syncTargetOrder = [
|
||||
// 'joplinCloud',
|
||||
// 'dropbox',
|
||||
// 'onedrive',
|
||||
// ];
|
||||
|
||||
export default class SyncTargetRegistry {
|
||||
|
||||
private static reg_: Record<number, SyncTargetInfo> = {};
|
||||
|
||||
private static get reg() {
|
||||
// if (!this.reg_[0]) {
|
||||
// this.reg_[0] = {
|
||||
// id: 0,
|
||||
// name: SyncTargetNone.targetName(),
|
||||
// label: SyncTargetNone.label(),
|
||||
// classRef: SyncTargetNone,
|
||||
// description: SyncTargetNone.description(),
|
||||
// supportsSelfHosted: false,
|
||||
// supportsConfigCheck: false,
|
||||
// };
|
||||
// }
|
||||
|
||||
return this.reg_;
|
||||
}
|
||||
|
||||
@@ -47,6 +30,10 @@ export default class SyncTargetRegistry {
|
||||
throw new Error(`Unknown name: ${name}`);
|
||||
}
|
||||
|
||||
public static infoById(id: number): SyncTargetInfo {
|
||||
return this.infoByName(this.idToName(id));
|
||||
}
|
||||
|
||||
public static addClass(SyncTargetClass: any) {
|
||||
this.reg[SyncTargetClass.id()] = {
|
||||
id: SyncTargetClass.id(),
|
||||
@@ -56,6 +43,7 @@ export default class SyncTargetRegistry {
|
||||
description: SyncTargetClass.description(),
|
||||
supportsSelfHosted: SyncTargetClass.supportsSelfHosted(),
|
||||
supportsConfigCheck: SyncTargetClass.supportsConfigCheck(),
|
||||
supportsRecursiveLinkedNotes: SyncTargetClass.supportsRecursiveLinkedNotes(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
"Completed": "Abgeschlossen",
|
||||
"Completed decryption.": "Entschlüsselung abgeschlossen.",
|
||||
"Completed: %s (%s)": "Abgeschlossen: %s (%s)",
|
||||
"Compress old changes": "Komprimiere alte Änderungen",
|
||||
"Configuration": "Konfiguration",
|
||||
"Confirm password cannot be empty": "Bestätigungs-Passwort darf nicht leer sein",
|
||||
"Confirm password:": "Passwort bestätigen:",
|
||||
@@ -198,6 +199,8 @@
|
||||
"Default: %s": "Standard: %s",
|
||||
"Delete": "Löschen",
|
||||
"Delete attachment \"%s\"?": "Anhang „%s“ löschen?",
|
||||
"Delete expired sessions": "Lösche abgelaufene Sitzungen",
|
||||
"Delete expired tokens": "Lösche abgelaufene Tokens",
|
||||
"Delete line": "Zeile löschen",
|
||||
"Delete local data and re-download from sync target": "Lösche lokale Daten und lade Daten erneut vom Synchronisierungsziel",
|
||||
"Delete note \"%s\"?": "Notiz „%s“ löschen?",
|
||||
@@ -262,6 +265,8 @@
|
||||
"Editor monospace font family": "Nichtproportionale Schriftfamilie im Editor",
|
||||
"Either \"text\" or \"json\"": "Entweder „text“ oder „json“",
|
||||
"Emacs": "Emacs",
|
||||
"Email": "E-Mail",
|
||||
"Emails": "E-Mails",
|
||||
"emphasised text": "hervorgehobener Text",
|
||||
"Enable": "Aktivieren",
|
||||
"Enable ++insert++ syntax": "Syntax ++insert++ aktivieren",
|
||||
@@ -588,6 +593,9 @@
|
||||
"Previous versions of this note": "Vorherige Version dieser Notiz",
|
||||
"Print": "Drucken",
|
||||
"Privacy Policy": "Datenschutzrichtlinie",
|
||||
"Process failed payment subscriptions": "Verarbeite fehlgeschlagene Zahlungsabonnements",
|
||||
"Process oversized accounts": "Verarbeite zu große Konten",
|
||||
"Process user deletions": "Verarbeite Benutzerlöschungen",
|
||||
"Profile": "Profil",
|
||||
"Profile Version: %s": "Profil-Version: %s",
|
||||
"Properties": "Eigenschaften",
|
||||
@@ -842,6 +850,7 @@
|
||||
"Untitled": "Unbenannt",
|
||||
"Update": "Aktualisieren",
|
||||
"Update profile": "Profil aktualisieren",
|
||||
"Update total sizes": "Aktualisiere Gesamtgrößen",
|
||||
"Updated": "Aktualisiert",
|
||||
"updated date": "Aktualisierungsdatum",
|
||||
"Updated local items: %d.": "Lokale Elemente aktualisiert: %d.",
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"Aritim Dark": "Aritim Dark",
|
||||
"Attach file": "Liitä tiedosto",
|
||||
"Attach photo": "Liitä valokuva",
|
||||
"Attach...": "Liittää...",
|
||||
"Attach...": "Liitä...",
|
||||
"Attaches the given file to the note.": "Liittää annetun tiedoston muistiinpanoon.",
|
||||
"attachment": "liite",
|
||||
"Attachment conflict: \"%s\"": "Liitteen ristiriita: \"%s\"",
|
||||
@@ -78,6 +78,7 @@
|
||||
"Authorisation token:": "Valtuutuksen tunnus:",
|
||||
"Auto": "Automaattinen",
|
||||
"Auto-pair braces, parenthesis, quotations, etc.": "Yhdistä sulut, sulkeet, lainaukset jne.",
|
||||
"Automatically check for updates": "Tarkista päivitykset automaattisesti",
|
||||
"Automatically switch theme to match system theme": "Vaihda teema automaattisesti vastaamaan järjestelmän teemaa",
|
||||
"Back": "Takaisin",
|
||||
"Bold": "Lihavoitu",
|
||||
@@ -142,6 +143,7 @@
|
||||
"Convert to todo": "Muunna tehtäväksi",
|
||||
"Copy": "Kopioi",
|
||||
"Copy dev mode command to clipboard": "Kopioi kehitystila komento leikepöydälle",
|
||||
"Copy image": "Kopioi kuva",
|
||||
"Copy Link Address": "Kopioi linkin osoite",
|
||||
"Copy Markdown link": "Kopioi Merkinnän linkki",
|
||||
"Copy path to clipboard": "Kopioi polku leikepöydälle",
|
||||
@@ -153,6 +155,8 @@
|
||||
"Could not install plugin: %s": "Laajennuksen asentaminen epäonnistui: %s",
|
||||
"Could not upgrade master key: %s": "Pääavainta ei voitu päivittää: %s",
|
||||
"Create a notebook": "Luo muistikirja",
|
||||
"Create notebook": "Luo muistikirja",
|
||||
"Create user": "Luo käyttäjä",
|
||||
"Created": "Luotu",
|
||||
"created date": "luotu päivämäärä",
|
||||
"Created local items: %d.": "Luodut paikalliset kohteet: %d.",
|
||||
@@ -248,6 +252,8 @@
|
||||
"Editor monospace font family": "Editorin monospace fonttiperhe",
|
||||
"Either \"text\" or \"json\"": "Joko \"text\" tai \"json\"",
|
||||
"Emacs": "Emacs",
|
||||
"Email": "Sähköposti",
|
||||
"Emails": "Sähköpostit",
|
||||
"emphasised text": "korostettu teksti",
|
||||
"Enable": "Ota käyttöön",
|
||||
"Enable ++insert++ syntax": "Ota käyttöön ++insert++ syntax",
|
||||
@@ -279,6 +285,8 @@
|
||||
"Encryption": "Salaus",
|
||||
"Encryption Config": "Salauksen määritys",
|
||||
"Encryption is: %s": "Salaus on: %s",
|
||||
"Encryption keys": "Salausavaimet",
|
||||
"Encryption:": "Salaus:",
|
||||
"Enter code here": "Syötä koodi tähän",
|
||||
"Enter master password:": "Syötä pääsalasana:",
|
||||
"Enter notebook title": "Anna muistikirjan otsikko",
|
||||
@@ -346,6 +354,7 @@
|
||||
"Idle": "Käyttämättömänä",
|
||||
"Ignore": "Ohita",
|
||||
"Ignore TLS certificate errors": "Ohita TLS varmenteen virheet",
|
||||
"Images": "Kuvat",
|
||||
"Import": "Tuo",
|
||||
"Importing from \"%s\" as \"%s\" format. Please wait...": "Tuodaan kohteesta \"%s\" as \"%s\" muodossa. Odota...",
|
||||
"Importing notes...": "Muistiinpanojen tuominen...",
|
||||
@@ -374,6 +383,7 @@
|
||||
"Invalid answer: %s": "Virheellinen vastaus: %s",
|
||||
"Invalid command: \"%s\"": "Virheellinen komento: \"%s\"",
|
||||
"Invalid option value: \"%s\". Possible values are: %s.": "Virheellinen asetusarvo: \"%s\". Mahdolliset arvot ovat: %s.",
|
||||
"Invalid password": "Virheellinen salasana",
|
||||
"Italic": "Kursiivi",
|
||||
"Item \"%s\" could not be downloaded: %s": "Kohdetta \"%s\" ei voitu ladata: %s",
|
||||
"Items that cannot be decrypted": "Kohteet, joita ei voi purkaa",
|
||||
@@ -419,6 +429,8 @@
|
||||
"Login with Dropbox": "Kirjaudu sisään Dropbox",
|
||||
"Login with OneDrive": "Kirjaudu sisään OneDrive",
|
||||
"Make a donation": "Tee lahjoitus",
|
||||
"Manage master password": "Pääsalasanan hallinta",
|
||||
"Manage master password...": "Pääsalasanan hallinta...",
|
||||
"Manage your plugins": "Hallitse laajennuksia",
|
||||
"Manual": "Manuaalinen",
|
||||
"Markdown": "Markdown",
|
||||
@@ -426,6 +438,8 @@
|
||||
"Marks a to-do as non-completed.": "Merkitsee tehtävän keskeneräiseksi.",
|
||||
"Markup": "Merkintä",
|
||||
"Master Key %s": "Pääavain %s",
|
||||
"Master password": "Pääsalasana",
|
||||
"Master password:": "Pääsalasana:",
|
||||
"Max concurrent connections": "Samanaikaiset yhteydet enintään",
|
||||
"Missing Master Keys": "Puuttuvat pääavaimet",
|
||||
"Missing required argument: %s": "Vaadittu argumentti puuttuu: %s",
|
||||
@@ -475,7 +489,7 @@
|
||||
"Note body": "Muistiinpanon kappale",
|
||||
"Note does not exist: \"%s\". Create it?": "Huomautusta ei ole: \"%s\". Luodaanko se?",
|
||||
"Note has been saved.": "Huomautus on tallennettu.",
|
||||
"Note History": "Muistiinpano historia",
|
||||
"Note History": "Muistiinpanohistoria",
|
||||
"Note is not a to-do: \"%s\"": "Huomautus ei ole tehtävä: \"%s\"",
|
||||
"Note list": "Muistiinpanot",
|
||||
"Note list growth factor": "Huomautus luettelon kasvutekijä",
|
||||
@@ -484,10 +498,12 @@
|
||||
"Note&book": "&Muistikirjat",
|
||||
"Note: Does not work in all desktop environments.": "Huomautus: Ei toimi kaikissa työpöytäympäristöissä.",
|
||||
"Note: When a note is shared, it will no longer be encrypted on the server.": "Huomautus: Kun muistiinpano on jaettu, sitä ei enää salata palvelimella.",
|
||||
"Notebook": "Muistikirja",
|
||||
"Notebook list growth factor": "Muistikirjaluettelon kasvutekijä",
|
||||
"Notebook: %s": "Muistikirja: %s",
|
||||
"Notebooks": "Muistikirjat",
|
||||
"Notebooks cannot be named \"%s\", which is a reserved title.": "Muistikirjoja ei voi nimetä \"%s\", joka on varattu otsikko.",
|
||||
"Notes": "Muistiinpanot",
|
||||
"Notes and settings are stored in: %s": "Muistiinpanot ja oletusasetukset tallennetaan: %s",
|
||||
"Notes can only be created within a notebook.": "Muistiinpanoja voi luoda vain muistikirjaan.",
|
||||
"Numbered List": "Numeroitu luettelo",
|
||||
@@ -601,6 +617,7 @@
|
||||
"See the pre-release page for more details: %s": "Lisätietoja on ennakkojulkaisusivulla: %s",
|
||||
"Select": "Valitse",
|
||||
"Select all": "Valitse kaikki",
|
||||
"Select file...": "Valitse tiedosto...",
|
||||
"Server is already running on port %d": "Palvelin on jo käynnissä portissa %d",
|
||||
"Server is not running.": "Palvelin ei ole käynnissä.",
|
||||
"Server is running on port %d": "Palvelin on käynnissä portissa %d",
|
||||
@@ -797,6 +814,7 @@
|
||||
"Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.": "Tämän avulla voit muodostaa hakuindeksin uudelleen, jos haussa on ongelma. Se voi kestää kauan muistiinpanojen määrästä riippuen.",
|
||||
"Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.": "Käytetään useimmissa teksteissä markdown editorissa. Jos sitä ei löydy, käytetään yleistä suhteellista fonttia (vaihtelevaa leveyttä).",
|
||||
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": "Käytetään, kun tekstiin tarvitaan kiinteäleveyksinen fontti (esim. taulukot, valintaruudut, koodi). Jos sitä ei löydy, käytetään yleistä monospace fonttia (kiinteäleveyksinen).",
|
||||
"Users": "Käyttäjät",
|
||||
"Valid": "Kelvollinen",
|
||||
"View": "Näytä",
|
||||
"View on map": "Näytä kartalla",
|
||||
|
||||
@@ -79,8 +79,9 @@
|
||||
"Authentication was not completed (did not receive an authentication token).": "Impossible d'autoriser le logiciel (jeton d'identification non‑reçu).",
|
||||
"Authorisation token:": "Code d'authentification :",
|
||||
"Auto": "Auto",
|
||||
"Auto-add disabled accounts for deletion": "Supprimer automatiquement les comptes désactivés",
|
||||
"Auto-pair braces, parenthesis, quotations, etc.": "Auto‑compléter les paires de parenthèses, guillemets, etc.",
|
||||
"Automatically check for updates": "Vérifier automatiquement les mises à jour…",
|
||||
"Automatically check for updates": "Vérifier automatiquement les mises à jour",
|
||||
"Automatically switch theme to match system theme": "Changer le thème automatiquement pour correspondre au thème système",
|
||||
"Back": "Retour",
|
||||
"Bold": "Gras",
|
||||
@@ -135,6 +136,7 @@
|
||||
"Completed": "Terminé",
|
||||
"Completed decryption.": "Déchiffrement complété.",
|
||||
"Completed: %s (%s)": "Terminé : %s (%s)",
|
||||
"Compress old changes": "Compresser les vieux changements",
|
||||
"Configuration": "Configuration",
|
||||
"Confirm password cannot be empty": "Le mot de passe de confirmation ne peut être vide",
|
||||
"Confirm password:": "Confirmer le mot de passe :",
|
||||
@@ -198,6 +200,8 @@
|
||||
"Default: %s": "Défaut : %s",
|
||||
"Delete": "Supprimer",
|
||||
"Delete attachment \"%s\"?": "Supprimer fichier joint \"%s\" ?",
|
||||
"Delete expired sessions": "Supprimer les sessions expirées",
|
||||
"Delete expired tokens": "Supprimer les tokens expirés",
|
||||
"Delete line": "Supprimer la ligne",
|
||||
"Delete local data and re-download from sync target": "Supprimer les données locales et re‑télécharger depuis la cible de synchronisation",
|
||||
"Delete note \"%s\"?": "Supprimer note \"%s\" ?",
|
||||
@@ -262,6 +266,8 @@
|
||||
"Editor monospace font family": "Police monospace de l'éditeur",
|
||||
"Either \"text\" or \"json\"": "Soit \"text\" soit \"json\"",
|
||||
"Emacs": "Emacs",
|
||||
"Email": "Email",
|
||||
"Emails": "Emails",
|
||||
"emphasised text": "texte en italique",
|
||||
"Enable": "Activer",
|
||||
"Enable ++insert++ syntax": "Activer la syntaxe ++insertion++",
|
||||
@@ -368,6 +374,7 @@
|
||||
"Idle": "Arrêté",
|
||||
"Ignore": "Ignorer",
|
||||
"Ignore TLS certificate errors": "Ignorer les erreurs de certificats TLS",
|
||||
"Images": "Images",
|
||||
"Import": "Importer",
|
||||
"Importing from \"%s\" as \"%s\" format. Please wait...": "Importer depuis \"%s\" au format \"%s\". Veuillez patienter…",
|
||||
"Importing notes...": "Importation des notes…",
|
||||
@@ -547,6 +554,7 @@
|
||||
"Operation cancelled": "Opération annulée",
|
||||
"Options": "Options",
|
||||
"Or create an account.": "Ou créer un compte.",
|
||||
"Other applications...": "Autres applications...",
|
||||
"Output format: %s": "Format de la sortie : %s",
|
||||
"Page orientation for PDF export": "Orientation de page pour l'export PDF",
|
||||
"Page size for PDF export": "Taille de page pour l'export PDF",
|
||||
@@ -588,6 +596,9 @@
|
||||
"Previous versions of this note": "Versions précédentes de cette note",
|
||||
"Print": "Imprimer",
|
||||
"Privacy Policy": "Politique de confidentialité",
|
||||
"Process failed payment subscriptions": "Traiter les inscriptions avec échec de paiement",
|
||||
"Process oversized accounts": "Traiter les comptes ayant excédé leur limite",
|
||||
"Process user deletions": "Traiter la suppression d'utilisateurs",
|
||||
"Profile": "Profil",
|
||||
"Profile Version: %s": "Version du profil : %s",
|
||||
"Properties": "Propriétés",
|
||||
@@ -651,6 +662,8 @@
|
||||
"See the pre-release page for more details: %s": "Voir la page des pré‑release pour plus de détails : %s",
|
||||
"Select": "Sélectionner",
|
||||
"Select all": "Sélectionner tout",
|
||||
"Select emoji...": "Sélectionner émoji...",
|
||||
"Select file...": "Sélectionner fichier...",
|
||||
"Server is already running on port %d": "Le serveur tourne déjà sur le port %d",
|
||||
"Server is not running.": "Le serveur est arrêté.",
|
||||
"Server is running on port %d": "Le serveur tourne sur le port %d",
|
||||
@@ -842,6 +855,7 @@
|
||||
"Untitled": "Sans titre",
|
||||
"Update": "Mettre à jour",
|
||||
"Update profile": "Mettre à jour le profil",
|
||||
"Update total sizes": "Mettre à jour les tailles totales",
|
||||
"Updated": "Mis à jour",
|
||||
"updated date": "date de modification",
|
||||
"Updated local items: %d.": "Objets màj localement : %d.",
|
||||
|
||||
@@ -41,44 +41,44 @@ locales['uk_UA'] = require('./uk_UA.json');
|
||||
locales['vi'] = require('./vi.json');
|
||||
locales['zh_CN'] = require('./zh_CN.json');
|
||||
locales['zh_TW'] = require('./zh_TW.json');
|
||||
stats['ar'] = {"percentDone":91};
|
||||
stats['ar'] = {"percentDone":90};
|
||||
stats['eu'] = {"percentDone":26};
|
||||
stats['bs_BA'] = {"percentDone":65};
|
||||
stats['bg_BG'] = {"percentDone":51};
|
||||
stats['ca'] = {"percentDone":97};
|
||||
stats['hr_HR'] = {"percentDone":95};
|
||||
stats['hr_HR'] = {"percentDone":94};
|
||||
stats['cs_CZ'] = {"percentDone":87};
|
||||
stats['da_DK'] = {"percentDone":98};
|
||||
stats['de_DE'] = {"percentDone":98};
|
||||
stats['de_DE'] = {"percentDone":99};
|
||||
stats['et_EE'] = {"percentDone":50};
|
||||
stats['en_GB'] = {"percentDone":100};
|
||||
stats['en_US'] = {"percentDone":100};
|
||||
stats['es_ES'] = {"percentDone":97};
|
||||
stats['es_ES'] = {"percentDone":96};
|
||||
stats['eo'] = {"percentDone":29};
|
||||
stats['fi_FI'] = {"percentDone":91};
|
||||
stats['fr_FR'] = {"percentDone":98};
|
||||
stats['fi_FI'] = {"percentDone":92};
|
||||
stats['fr_FR'] = {"percentDone":100};
|
||||
stats['gl_ES'] = {"percentDone":33};
|
||||
stats['id_ID'] = {"percentDone":90};
|
||||
stats['it_IT'] = {"percentDone":88};
|
||||
stats['it_IT'] = {"percentDone":87};
|
||||
stats['hu_HU'] = {"percentDone":76};
|
||||
stats['nl_BE'] = {"percentDone":89};
|
||||
stats['nl_NL'] = {"percentDone":83};
|
||||
stats['nb_NO'] = {"percentDone":88};
|
||||
stats['fa'] = {"percentDone":63};
|
||||
stats['fa'] = {"percentDone":62};
|
||||
stats['pl_PL'] = {"percentDone":82};
|
||||
stats['pt_BR'] = {"percentDone":91};
|
||||
stats['pt_PT'] = {"percentDone":82};
|
||||
stats['ro'] = {"percentDone":58};
|
||||
stats['ro'] = {"percentDone":57};
|
||||
stats['sl_SI'] = {"percentDone":91};
|
||||
stats['sv'] = {"percentDone":97};
|
||||
stats['sv'] = {"percentDone":96};
|
||||
stats['th_TH'] = {"percentDone":41};
|
||||
stats['vi'] = {"percentDone":88};
|
||||
stats['tr_TR'] = {"percentDone":98};
|
||||
stats['uk_UA'] = {"percentDone":81};
|
||||
stats['el_GR'] = {"percentDone":85};
|
||||
stats['el_GR'] = {"percentDone":84};
|
||||
stats['ru_RU'] = {"percentDone":91};
|
||||
stats['sr_RS'] = {"percentDone":74};
|
||||
stats['zh_CN'] = {"percentDone":98};
|
||||
stats['sr_RS'] = {"percentDone":73};
|
||||
stats['zh_CN'] = {"percentDone":99};
|
||||
stats['zh_TW'] = {"percentDone":88};
|
||||
stats['ja_JP'] = {"percentDone":96};
|
||||
stats['ko'] = {"percentDone":87};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"\"%s\" is missing the required \"%s\" property.": "“%s”缺少必须的属性“%s”。",
|
||||
"\"%s\" is missing the required \"%s\" property.": "“%s”缺少必须的属性\"%s”。",
|
||||
"%d days": "%d 天",
|
||||
"%d hour": "%d 小时",
|
||||
"%d hours": "%d 小时",
|
||||
@@ -7,7 +7,7 @@
|
||||
"%d notes match this pattern. Delete them?": "有 %d 条笔记匹配。是否删除?",
|
||||
"%s %s (%s, %s)": "%s %s (%s, %s)",
|
||||
"%s (%s) could not be uploaded: %s": "%s (%s) 无法上传:%s",
|
||||
"%s (%s) would like to share a notebook with you.": "%s (%s) 想要分享笔记本给你。",
|
||||
"%s (%s) would like to share a notebook with you.": "%s (%s) 想要分享笔记本给您。",
|
||||
"%s (%s): %s": "%s (%s): %s",
|
||||
"%s (pre-release)": "%s(预发行版)",
|
||||
"%s - Copy": "%s - 副本",
|
||||
@@ -31,7 +31,7 @@
|
||||
"(wysiwyg: %s)": "(兼容所见即所得编辑器: %s)",
|
||||
"- Camera: to allow taking a picture and attaching it to a note.": "- 相机:允许拍照并将照片添加到一条笔记中。",
|
||||
"- Location: to allow attaching geo-location information to a note.": "- 定位:允许将地理位置信息附加到一条笔记中。",
|
||||
"- Storage: to allow attaching files to notes and to enable filesystem synchronisation.": "- 存储:允许将文件附加到笔记中和启用文件系统同步。",
|
||||
"- Storage: to allow attaching files to notes and to enable filesystem synchronisation.": "- 存储:允许将文件附加到笔记中并启用文件系统同步。",
|
||||
"<tag-command> can be \"add\", \"remove\", \"list\", or \"notetags\" to assign or remove [tag] from [note], to list notes associated with [tag], or to list tags associated with [note]. The command `tag list` can be used to list all the tags (use -l for long option).": "<tag-command> 可以是 \"add\" 、 \"remove\" 、 \"list\" 或者 \"notetags\" ,用于从 [note] 中赋值或删除 [tag],或者列出与 [tag] 相关的笔记。`tag list` 命令可以用于列出所有的标签 (对于过长选项请使用 -l 参数) 。",
|
||||
"<todo-command> can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.": "<todo-command> 可以是 \"toggle\" 或者 \"clear\" 。使用 \"toggle\" 命令来切换待办事项的完成状态 (若目标为普通笔记则将会转换成待办事项) 。使用 \"clear\" 命令来把待办事项转换到普通笔记。",
|
||||
"A3": "A3",
|
||||
@@ -40,7 +40,7 @@
|
||||
"About Joplin": "关于 Joplin",
|
||||
"accelerator": "加速键",
|
||||
"Accelerator \"%s\" is not valid.": "加速键“%s”无效。",
|
||||
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to unexpected behaviour.": "加速键“%s”被用于“%s”和“%s”命令。这可能导致未期待的行为。",
|
||||
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to unexpected behaviour.": "加速键“%s”被用于“%s”和“%s”命令。这可能导致意外的表现。",
|
||||
"Accept": "接受",
|
||||
"Action": "动作",
|
||||
"Actions": "动作",
|
||||
@@ -48,7 +48,7 @@
|
||||
"Actual Size": "实际大小",
|
||||
"Add body": "添加内容",
|
||||
"Add or remove tags:": "添加或删除标签:",
|
||||
"Add recipient:": "添加接受者:",
|
||||
"Add recipient:": "添加收件人:",
|
||||
"Add title": "添加标题",
|
||||
"Add to dictionary": "添加到字典",
|
||||
"Admin": "管理员",
|
||||
@@ -79,6 +79,7 @@
|
||||
"Authentication was not completed (did not receive an authentication token).": "认证未完成 (未收到认证令牌) 。",
|
||||
"Authorisation token:": "授权令牌:",
|
||||
"Auto": "自动",
|
||||
"Auto-add disabled accounts for deletion": "自动添加已禁用的账户以便删除",
|
||||
"Auto-pair braces, parenthesis, quotations, etc.": "自动配对花括号、圆括号、引号等。",
|
||||
"Automatically check for updates": "自动检查更新",
|
||||
"Automatically switch theme to match system theme": "根据系统主题自动切换",
|
||||
@@ -86,7 +87,7 @@
|
||||
"Bold": "加粗",
|
||||
"Browse all plugins": "浏览所有插件",
|
||||
"Browse...": "浏览...",
|
||||
"Bulleted List": "项目符号列表",
|
||||
"Bulleted List": "无序列表",
|
||||
"Cancel": "取消",
|
||||
"Cancelling background synchronisation... Please wait.": "正在取消后台同步... 请稍候。",
|
||||
"Cancelling...": "正在取消...",
|
||||
@@ -95,7 +96,7 @@
|
||||
"Cannot change encrypted item": "无法更改已加密条目",
|
||||
"Cannot copy note to \"%s\" notebook": "无法复制笔记到笔记本 \"%s\"",
|
||||
"Cannot find \"%s\".": "无法找到“%s”。",
|
||||
"Cannot initialise synchroniser.": "无法初始化同步。",
|
||||
"Cannot initialise synchroniser.": "无法启动同步。",
|
||||
"Cannot load \"%s\" module for format \"%s\" and output \"%s\"": "无法为格式“%2$s”和输出“%3$s”加载“%1$s”模块",
|
||||
"Cannot load \"%s\" module for format \"%s\" and target \"%s\"": "无法加载为格式“%2$s”和目标“%3$s”加载“%1$s”模块",
|
||||
"Cannot move note to \"%s\" notebook": "无法移动笔记到笔记本 \"%s\"",
|
||||
@@ -135,8 +136,9 @@
|
||||
"Completed": "已完成",
|
||||
"Completed decryption.": "已完成解密。",
|
||||
"Completed: %s (%s)": "已完成: %s(%s)",
|
||||
"Compress old changes": "压缩旧的更改",
|
||||
"Configuration": "配置",
|
||||
"Confirm password cannot be empty": "确保密码不可为空",
|
||||
"Confirm password cannot be empty": "确认密码不可为空",
|
||||
"Confirm password:": "确认密码:",
|
||||
"Confirmation": "确认",
|
||||
"Conflicted: %d": "有冲突: %d 条",
|
||||
@@ -147,7 +149,7 @@
|
||||
"Convert to todo": "转换为待办事项",
|
||||
"Copy": "复制",
|
||||
"Copy dev mode command to clipboard": "复制开发者模式命令到剪贴板",
|
||||
"Copy external link": "拷贝外部访问地址",
|
||||
"Copy external link": "复制外部链接地址",
|
||||
"Copy image": "复制图片",
|
||||
"Copy Link Address": "复制链接地址",
|
||||
"Copy Markdown link": "复制 Markdown 链接",
|
||||
@@ -161,7 +163,7 @@
|
||||
"Could not install plugin: %s": "无法安装插件:%s",
|
||||
"Could not respond to the invitation. Please try again, or check with the notebook owner if they are still sharing it.\n\nThe error was: \"%s\"": "无法对邀请作出回应。请再试一次,或向笔记本所有者核实他们是否仍在共享它。\n\n错误是:“%s”",
|
||||
"Could not upgrade master key: %s": "无法升级主密钥:%s",
|
||||
"Could not verify the share status of this notebook - aborting. Please try again when you are connected to the internet.": "无法验证此笔记的分享状态 - 终止。 请在连接到互联网后再次尝试。",
|
||||
"Could not verify the share status of this notebook - aborting. Please try again when you are connected to the internet.": "无法验证此笔记的分享状态 - 正在终止。 请在连接到互联网后再次尝试。",
|
||||
"Create a notebook": "新建一个笔记本",
|
||||
"Create notebook": "新建笔记本",
|
||||
"Create user": "创建用户",
|
||||
@@ -191,22 +193,24 @@
|
||||
"Date": "日期",
|
||||
"Date format": "日期格式",
|
||||
"days": "天",
|
||||
"Decrypted items: %d": "解密条目: %d",
|
||||
"Decrypted items: %d": "已解密条目: %d",
|
||||
"Decrypted items: %s / %s": "已解密条目: %s / %s",
|
||||
"Decrypting items: %d/%d": "正在解密条目:%d/%d",
|
||||
"Default": "默认",
|
||||
"Default: %s": "默认值: %s",
|
||||
"Delete": "删除",
|
||||
"Delete attachment \"%s\"?": "是否删除附件“%s”?",
|
||||
"Delete expired sessions": "删除过期的会话",
|
||||
"Delete expired tokens": "删除过期的令牌",
|
||||
"Delete line": "删除行",
|
||||
"Delete local data and re-download from sync target": "删除本地数据并从同步目标导入数据",
|
||||
"Delete note \"%s\"?": "是否删除笔记“%s”?",
|
||||
"Delete note?": "是否删除笔记?",
|
||||
"Delete notebook \"%s\"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.": "是否删除笔记本“%s”?\n\n所有在该笔记本内的笔记和子笔记本也将同时被删除。",
|
||||
"Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.": "是否删除笔记本?所有在该笔记本内的笔记也将同时被删除。",
|
||||
"Delete plugin \"%s\"?": "删除插件“%s”吗?",
|
||||
"Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.": "是否删除笔记本?所有在该笔记本内的笔记和子笔记本也将同时被删除。",
|
||||
"Delete plugin \"%s\"?": "是否删除插件“%s”?",
|
||||
"Delete these %d notes?": "是否删除这 %d 条笔记?",
|
||||
"Delete this invitation? The recipient will no longer have access to this shared notebook.": "删除这个邀请?接受者将无法再访问到这个共享的笔记本。",
|
||||
"Delete this invitation? The recipient will no longer have access to this shared notebook.": "是否删除该邀请?收件人将无法再次访问此共享笔记本。",
|
||||
"Deleted local items: %d.": "已删除本地项目: %d。",
|
||||
"Deleted remote items: %d.": "已删除远程项目: %d。",
|
||||
"Deletes the given notebook.": "删除选定的笔记本。",
|
||||
@@ -219,7 +223,7 @@
|
||||
"Disable": "禁用",
|
||||
"Disable encryption": "禁用加密",
|
||||
"Disable safe mode and restart": "禁用安全模式并重启",
|
||||
"Disable Web Clipper Service": "禁用网页剪辑服务",
|
||||
"Disable Web Clipper Service": "禁用网页剪辑器",
|
||||
"Disabled": "已禁用",
|
||||
"Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?": "禁用加密会导致 *所有笔记与附件* 重新同步,并以非加密的数据形式发送到同步目标。确定继续吗?",
|
||||
"Discard changes": "放弃更改",
|
||||
@@ -228,16 +232,16 @@
|
||||
"Displays only the first top <num> notes.": "只显示最上方的 <num> 条笔记。",
|
||||
"Displays only the items of the specific type(s). Can be `n` for notes, `t` 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.": "仅显示指定类型的项目。可以把 `n` 用于笔记,`t` 用于待办事项,或者 `nt` 用于笔记和待办事项 (示例: `-tt` 只会显示待办事项,当使用 `-tnt` 时将会显示笔记和待办事项。",
|
||||
"Displays summary about the notes and notebooks.": "显示关于笔记与笔记本的概况。",
|
||||
"Displays the complete information about note.": "显示有关笔记的完整信息。",
|
||||
"Displays the complete information about note.": "显示关于笔记的完整信息。",
|
||||
"Displays the given note.": "显示选定笔记。",
|
||||
"Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.": "在当前笔记本中显示笔记。使用 `ls /` 显示笔记本列表。",
|
||||
"Displays usage information.": "显示用法提示。",
|
||||
"Displays version information": "显示版本信息",
|
||||
"Do it now": "现在完成",
|
||||
"Do it now": "立即执行",
|
||||
"Do not ask for confirmation.": "不再要求确认。",
|
||||
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": "不要丢失密码,因为出于安全考虑,这将是解密数据的*唯一*方式!要启用加密功能,请在下面输入密码。",
|
||||
"Download": "下载",
|
||||
"Download and install the relevant extension for your browser:": "为您的浏览器下载并安装相关的扩展:",
|
||||
"Download and install the relevant extension for your browser:": "为您的浏览器下载并安装相关的扩展插件:",
|
||||
"Downloaded": "已下载",
|
||||
"Downloaded and decrypted": "已下载并解密",
|
||||
"Downloaded and encrypted": "已下载并加密",
|
||||
@@ -262,6 +266,8 @@
|
||||
"Editor monospace font family": "编辑器等宽字体族",
|
||||
"Either \"text\" or \"json\"": "\"text\" 或 \"json\"",
|
||||
"Emacs": "Emacs",
|
||||
"Email": "邮箱",
|
||||
"Emails": "邮箱",
|
||||
"emphasised text": "强调文本",
|
||||
"Enable": "启用",
|
||||
"Enable ++insert++ syntax": "启用 ++insert++ 语法",
|
||||
@@ -269,7 +275,7 @@
|
||||
"Enable ^sup^ syntax": "启用 ^sup^ 语法",
|
||||
"Enable abbreviation syntax": "启用缩写语法",
|
||||
"Enable audio player": "启用音频播放器",
|
||||
"Enable deflist syntax": "启用清单语法",
|
||||
"Enable deflist syntax": "启用 deflist 语法",
|
||||
"Enable encryption": "启用加密",
|
||||
"Enable footnotes": "启用脚注",
|
||||
"Enable Fountain syntax support": "启用 Fountain 语法支持",
|
||||
@@ -280,11 +286,11 @@
|
||||
"Enable multimarkdown table extension": "启用 MultiMarkdown 表格扩展",
|
||||
"Enable note history": "启用笔记历史",
|
||||
"Enable PDF viewer": "启用 PDF 查看器",
|
||||
"Enable soft breaks": "启动软换行",
|
||||
"Enable table of contents extension": "启用目录扩展",
|
||||
"Enable soft breaks": "启用软换行",
|
||||
"Enable table of contents extension": "启用目录扩展 ([TOC])",
|
||||
"Enable typographer support": "启用 Typographer 支持",
|
||||
"Enable video player": "启用视频播放器",
|
||||
"Enable Web Clipper Service": "启用网页剪辑服务",
|
||||
"Enable Web Clipper Service": "启用网页剪辑器",
|
||||
"Enable ~sub~ syntax": "启用 ~sub~ 语法",
|
||||
"Enabled": "已启用",
|
||||
"Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target.": "启用加密意味着您的*所有*笔记与附件都将被重新同步,并被加密发送到同步目标。",
|
||||
@@ -301,25 +307,25 @@
|
||||
"Enter master password:": "输入主密码:",
|
||||
"Enter notebook title": "输入笔记本标题",
|
||||
"Enum": "枚举",
|
||||
"Error": "错误",
|
||||
"Error opening note in editor: %s": "在编辑器中打开笔记出错:%s",
|
||||
"Error": "发生错误",
|
||||
"Error opening note in editor: %s": "在编辑器中打开笔记时出现错误:%s",
|
||||
"Error. Please check that URL, username, password, etc. are correct and that the sync target is accessible. The reported error was:": "发生错误。请检查 URL 、用户名、密码等是否正确以及同步目标是否可访问。报告的错误为:",
|
||||
"Error: %s": "错误:%s",
|
||||
"Error: %s": "发生错误:%s",
|
||||
"Errors only": "仅显示错误",
|
||||
"Evernote Export File (as HTML)": "Evernote 导出文件 (HTML)",
|
||||
"Evernote Export File (as Markdown)": "Evernote 导出文件 (Markdown)",
|
||||
"Exits the application.": "退出应用。",
|
||||
"Export": "导出",
|
||||
"Export all": "导出全部",
|
||||
"Export all": "全部导出",
|
||||
"Export debug report": "导出调试报告",
|
||||
"Export Debug Report": "导出调试报告",
|
||||
"Export profile": "导出配置文件",
|
||||
"Exporting profile...": "正在导出配置文件...",
|
||||
"Exporting to \"%s\" as \"%s\" format. Please wait...": "导出到“%s”,格式为“%s”。请稍等...",
|
||||
"Exports Joplin data to the given path. By default, it will export the complete database including notebooks, notes, tags and resources.": "导出 Joplin 数据到选定路径。默认将导出包含笔记本、笔记、标签与资源等完整的数据库。",
|
||||
"Exports Joplin data to the given path. By default, it will export the complete database including notebooks, notes, tags and resources.": "导出 Joplin 数据到选定路径。默认将导出包含笔记本、笔记、标签与资源等的完整数据库。",
|
||||
"Exports only the given note.": "仅导出选定笔记。",
|
||||
"Exports only the given notebook.": "仅导出选定笔记本。",
|
||||
"Fail-safe": "故障保护",
|
||||
"Fail-safe": "故障保护 (Fail-safe)",
|
||||
"Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)": "故障保护 (Fail-safe) :当同步目标为空时 (通常是配置错误或 Bug ) ,不要删除本地数据",
|
||||
"Fatal error:": "严重错误:",
|
||||
"Feature flags": "特性标志",
|
||||
@@ -327,7 +333,7 @@
|
||||
"Fetching resources: %d/%d": "正在获取资源:%d/%d",
|
||||
"File": "文件",
|
||||
"File system": "文件系统",
|
||||
"Firefox Extension": "Firefox 扩展",
|
||||
"Firefox Extension": "Firefox 扩展插件",
|
||||
"Fix search index": "修复搜索索引",
|
||||
"Fixing search index...": "正在修复搜索索引...",
|
||||
"Focus": "聚焦于",
|
||||
@@ -335,7 +341,7 @@
|
||||
"Focus title": "聚焦标题",
|
||||
"Folders": "文件夹",
|
||||
"For debugging purpose only: export your profile to an external SD card.": "仅用于调试目的:将您的配置文件导出到外部 SD 卡。",
|
||||
"For information on how to customise the shortcuts please visit %s": "有关如何自定义快捷键,请访问 %s",
|
||||
"For information on how to customise the shortcuts please visit %s": "若想了解有关如何自定义快捷键的信息,请访问 %s",
|
||||
"For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:": "若想了解有关端到端加密 (E2EE) 的更多信息,以及如何启用它的建议,请查阅文档:",
|
||||
"For the list of keyboard shortcuts and config options, type `help keymap`": "输入 `help keymap` 来获取完整的键盘快捷键列表",
|
||||
"Force path style": "强制路径风格",
|
||||
@@ -345,13 +351,13 @@
|
||||
"Full changelog": "完整更新记录",
|
||||
"General": "通用选项",
|
||||
"Generated": "已生成",
|
||||
"Generating link...": "生成链接...",
|
||||
"Generating link...": "生成链接中...",
|
||||
"Get it now:": "立即获取:",
|
||||
"Get pre-releases when checking for updates": "检查更新时获取预发行版本",
|
||||
"Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.": "读取或设置配置数值。如果 [value] 值没有提供,它将显示 [name] 值。如果没有提供 [name] 或 [value] 值,它将列出当前配置。",
|
||||
"Go to source URL": "转到源 URL",
|
||||
"Goto Anything...": "跳转到任意内容...",
|
||||
"Grant authorisation": "授权令牌",
|
||||
"Grant authorisation": "批准授权",
|
||||
"Heading": "标题",
|
||||
"Help": "帮助",
|
||||
"Hide %s": "隐藏 %s",
|
||||
@@ -372,12 +378,12 @@
|
||||
"Importing from \"%s\" as \"%s\" format. Please wait...": "从“%s” 导入为“%s”格式 。请稍等...",
|
||||
"Importing notes...": "正在导入笔记...",
|
||||
"Imports data into Joplin.": "导入数据到 Jolin。",
|
||||
"In \"Manual\" mode, attachments are downloaded only when you click on them. In \"Auto\", they are downloaded when you open the note. In \"Always\", all the attachments are downloaded whether you open the note or not.": "在“手动”模式下,只有单击附件时才会下载它们。在“自动”中,当你打开笔记时,它们就会被下载下来。在 “总是”中,无论你是否打开笔记,所有的附件都会被下载。",
|
||||
"In \"Manual\" mode, attachments are downloaded only when you click on them. In \"Auto\", they are downloaded when you open the note. In \"Always\", all the attachments are downloaded whether you open the note or not.": "“手动”模式下,仅在单击附件时才会下载。“自动”模式下,打开笔记,其中的附件就会被全部下载。 “总是”模式下,无论是否打开笔记,所有的附件均会被下载。",
|
||||
"In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.": "在任何命令中,某个笔记或笔记本可通过它的名称或 ID 引用,也可使用代表当前所选笔记和笔记本的快捷变量 ‘$n' 和 '$b'。`$c` 可用于引用当前所选的项目。",
|
||||
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": "为了将地理位置与笔记关联,本应用需要您的授权以访问您的位置。\n\n您可以在“配置”页面中随时关闭此选项。",
|
||||
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": "为此,必须对整个数据集进行加密和同步,因此最好在夜间休息时分彻夜运行。\n\n首先,请按照以下说明进行操作: \n\n1.同步您的所有设备。\n2.单击“%s”。\n3.让它运行完成。 在运行时,请避免在其他设备上更改任何笔记,以免发生冲突。\n4.在此设备上完成同步后,同步其他所有设备,并使其同步完成。\n\n重要提醒: 在一台设备上只需要运行一次。",
|
||||
"In order to use file system synchronisation your permission to write to external storage is required.": "要使用文件系统同步,您需要写入外部存储的权限。",
|
||||
"In order to use the web clipper, you need to do the following:": "要使用网页剪辑器,你需要执行以下步骤:",
|
||||
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": "为了将地理位置与笔记关联,本应用需要获取您的位置的权限。\n\n您可以在“配置”页面中随时关闭此选项。",
|
||||
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": "为此,必须对整个数据集进行加密和同步,因此最好在夜间休息时分运行。\n\n首先,请按照以下说明进行操作: \n\n1.同步您的所有设备。\n2.单击“%s”。\n3.让其完成运行。 在运行时,请不要在其他设备上更改任何笔记,以免发生冲突。\n4.在此设备上完成同步后,同步其他所有设备,并运行至同步完成。\n\n重要提醒: 在一台设备上只需要运行一次。",
|
||||
"In order to use file system synchronisation your permission to write to external storage is required.": "为使用文件系统同步,需要获取写入外部存储的权限。",
|
||||
"In order to use the web clipper, you need to do the following:": "要使用网页剪辑器,您需要执行以下步骤:",
|
||||
"In progress": "正在进行",
|
||||
"In: %s": "位于:%s",
|
||||
"Indent less": "减少缩进",
|
||||
@@ -393,7 +399,7 @@
|
||||
"Installing...": "正在安装…",
|
||||
"Invalid": "无效",
|
||||
"Invalid %s: %s.": "无效的%s:%s。",
|
||||
"Invalid answer: %s": "答案无效:%s",
|
||||
"Invalid answer: %s": "无效答案:%s",
|
||||
"Invalid command: \"%s\"": "无效命令:“%s”",
|
||||
"Invalid option value: \"%s\". Possible values are: %s.": "无效的选项值: \"%s\" 。可用值有: %s。",
|
||||
"Invalid password": "无效密码",
|
||||
@@ -402,21 +408,21 @@
|
||||
"Items": "条目",
|
||||
"Items that cannot be decrypted": "无法解密的条目",
|
||||
"Items that cannot be synchronised": "无法同步的条目",
|
||||
"Joplin can synchronise your notes using various providers. Select one from the list below.": "Joplin 可以使用多种途径来同步你的笔记。可以从下列选项中选择一种方式。",
|
||||
"Joplin Cloud": "Joplin 论坛",
|
||||
"Joplin Cloud email": "Joplin 论坛邮箱",
|
||||
"Joplin Cloud password": "Joplin 论坛密码",
|
||||
"Joplin can synchronise your notes using various providers. Select one from the list below.": "Joplin 可以使用多种途径来同步您的笔记。可以从下列选项中选择一种方式。",
|
||||
"Joplin Cloud": "Joplin Cloud",
|
||||
"Joplin Cloud email": "Joplin Cloud 邮箱",
|
||||
"Joplin Cloud password": "Joplin Cloud 密码",
|
||||
"Joplin Export Directory": "Joplin 导出目录",
|
||||
"Joplin Export File": "Joplin 导出文件",
|
||||
"Joplin failed to decrypt these items multiple times, possibly because they are corrupted or too large. These items will remain on the device but Joplin will no longer attempt to decrypt them.": "Joplin 多次解密这些条目失败,可能由于它们太大或已经损坏。这些条目会保留在设备上,但 Joplin 不会再尝试解密它们。",
|
||||
"Joplin Forum": "Joplin 论坛",
|
||||
"Joplin Server": "Joplin 服务器",
|
||||
"Joplin Server email": "Joplin Server 邮箱",
|
||||
"Joplin Server email": "Joplin 服务器邮箱",
|
||||
"Joplin Server password": "Joplin 服务器密码",
|
||||
"Joplin Server URL": "Joplin 服务器 URL",
|
||||
"Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.": "Joplin 网页剪辑器可以让你将浏览器中的网页和屏幕截图保存到 Joplin。",
|
||||
"Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.": "Joplin 网页剪辑器可以让您将浏览器中的网页和屏幕截图保存到 Joplin。",
|
||||
"Joplin website": "Joplin 官网",
|
||||
"Joplin's own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.": "Joplin 自己的同步服务。同时也提供 Joplin 的一些特性,比如发布笔记或者与他人协作笔记本。",
|
||||
"Joplin's own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.": "Joplin 自己的同步服务。还可以访问 Joplin 的特定功能,比如发布笔记或者与他人协作笔记本。",
|
||||
"Keep note history for": "保留笔记历史",
|
||||
"Keyboard Mode": "键盘模式",
|
||||
"Keyboard Shortcut": "键盘快捷键",
|
||||
@@ -425,13 +431,13 @@
|
||||
"Keys that need upgrading": "需要升级的密钥",
|
||||
"Landscape": "横板",
|
||||
"Language": "语言",
|
||||
"Last error: %s": "最后错误: %s",
|
||||
"Last error: %s": "最后的错误: %s",
|
||||
"Later": "稍后",
|
||||
"Layout": "布局",
|
||||
"Layout button sequence": "布局按钮序列",
|
||||
"Leave notebook...": "离开笔记本...",
|
||||
"Legal": "法律专用纸",
|
||||
"Letter": "信函",
|
||||
"Legal": "法律专用纸 (Legal)",
|
||||
"Letter": "信函 (Letter)",
|
||||
"Light": "明亮",
|
||||
"Lines": "行",
|
||||
"Link has been copied to clipboard!": "链接已复制到剪贴板!",
|
||||
@@ -439,7 +445,7 @@
|
||||
"List item": "列表项",
|
||||
"Loaded": "已加载",
|
||||
"Location": "位置",
|
||||
"Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.": "锁定文件已被保存。如果您确认当前未在进行任何同步,可删除锁定文件 \"%s\" 后继续上一部操作。",
|
||||
"Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.": "锁定文件已被占用。如果您确认当前未在进行任何同步,可在删除锁定文件 \"%s\" 后继续上一步操作。",
|
||||
"Log": "日志",
|
||||
"Login": "登录",
|
||||
"Login below.": "在下方登录。",
|
||||
@@ -465,7 +471,7 @@
|
||||
"Missing keys": "缺少密钥",
|
||||
"Missing Master Keys": "缺少主密钥",
|
||||
"Missing required argument: %s": "缺失必选参数:%s",
|
||||
"Mobile data - auto-sync disabled": "手机数据自动同步被禁用",
|
||||
"Mobile data - auto-sync disabled": "移动数据自动同步被禁用",
|
||||
"More info": "更多信息",
|
||||
"More information": "更多信息",
|
||||
"More than one item match \"%s\". Please narrow down your query.": "有多条项目符合“%s”。请缩小您的检索范围。",
|
||||
@@ -483,14 +489,14 @@
|
||||
"New sub-notebook": "新建子笔记本",
|
||||
"New tags:": "新建标签:",
|
||||
"New to-do": "新建待办事项",
|
||||
"New version: %s": "最新版本:%s",
|
||||
"New version: %s": "新版本:%s",
|
||||
"Nextcloud": "Nextcloud",
|
||||
"Nextcloud password": "Nextcloud 密码",
|
||||
"Nextcloud username": "Nextcloud 用户名",
|
||||
"Nextcloud WebDAV URL": "Nextcloud WebDAV URL",
|
||||
"no": "否",
|
||||
"No": "否",
|
||||
"No active notebook.": "无活动笔记本。",
|
||||
"No active notebook.": "无有效的笔记本。",
|
||||
"No item with ID %s": "没有 ID 为 %s 的项",
|
||||
"No notebook has been specified.": "未指定笔记本。",
|
||||
"No notebook selected.": "未选择笔记本。",
|
||||
@@ -529,7 +535,7 @@
|
||||
"Notes": "笔记",
|
||||
"Notes and settings are stored in: %s": "笔记与设置储存于:%s",
|
||||
"Notes can only be created within a notebook.": "笔记只能在笔记本内创建。",
|
||||
"Numbered List": "编号列表",
|
||||
"Numbered List": "有序列表",
|
||||
"OK": "确认",
|
||||
"OLED Dark": "纯黑 (OLED)",
|
||||
"On %s: %s": "位于 %s: %s",
|
||||
@@ -557,7 +563,7 @@
|
||||
"Paste": "粘贴",
|
||||
"Path:": "路径:",
|
||||
"PDF File": "PDF 文件",
|
||||
"Permission needed": "需要的权限",
|
||||
"Permission needed": "需要权限",
|
||||
"Permission to use camera": "使用相机的权限",
|
||||
"Please click on \"%s\" to proceed": "请点击“%s”继续",
|
||||
"Please confirm that you would like to re-encrypt your complete database.": "请确认您要重新加密整个数据库。",
|
||||
@@ -588,7 +594,10 @@
|
||||
"Previous versions of this note": "此笔记的早期版本",
|
||||
"Print": "打印",
|
||||
"Privacy Policy": "隐私政策",
|
||||
"Profile": "资料",
|
||||
"Process failed payment subscriptions": "处理失败的付费订阅",
|
||||
"Process oversized accounts": "处理超容量的账户",
|
||||
"Process user deletions": "处理用户删除",
|
||||
"Profile": "配置文件",
|
||||
"Profile Version: %s": "配置文件版本: %s",
|
||||
"Properties": "笔记属性",
|
||||
"Public-private key pair:": "公、私密钥对:",
|
||||
@@ -601,14 +610,14 @@
|
||||
"Re-upload local data to sync target": "重新上传本地数据到同步目标",
|
||||
"Read more about it": "进一步了解",
|
||||
"Read time: %s min": "阅读时间:%s 分钟",
|
||||
"Recipient has accepted the invitation": "接受者接受了邀请",
|
||||
"Recipient has not yet accepted the invitation": "接受者还没有接受邀请",
|
||||
"Recipient has rejected the invitation": "接受者拒绝了邀请",
|
||||
"Recipients:": "接受者:",
|
||||
"Recipient has accepted the invitation": "收件人接受了邀请",
|
||||
"Recipient has not yet accepted the invitation": "收件人还没有接受邀请",
|
||||
"Recipient has rejected the invitation": "收件人拒绝了邀请",
|
||||
"Recipients:": "收件人:",
|
||||
"Redo": "恢复",
|
||||
"Refresh": "刷新",
|
||||
"Reject": "拒绝",
|
||||
"Remove": "移除",
|
||||
"Remove": "删除",
|
||||
"Remove tag \"%s\" from all notes?": "从所有笔记中删除标签“%s”?",
|
||||
"Remove this search from the sidebar?": "从边栏中删除该项搜索?",
|
||||
"Rename": "重命名",
|
||||
@@ -626,7 +635,7 @@
|
||||
"Retry All": "全部重试",
|
||||
"Reveal file in folder": "在文件夹中展示文件",
|
||||
"Reverse sort order": "倒序",
|
||||
"Reverses the sorting order.": "反转排序顺序。",
|
||||
"Reverses the sorting order.": "倒序。",
|
||||
"Revision: %s (%s)": "修订: %s (%s)",
|
||||
"Runs the commands contained in the text file. There should be one command per line.": "执行文本文件中包含的命令。每个命令占一行。",
|
||||
"S3": "S3",
|
||||
@@ -662,7 +671,7 @@
|
||||
"Share": "分享",
|
||||
"Share Notebook": "分享笔记本",
|
||||
"Share notebook...": "分享笔记本...",
|
||||
"Sharing notebook...": "分享笔记本...",
|
||||
"Sharing notebook...": "分享笔记本中...",
|
||||
"Shortcuts are not available in CLI mode.": "快捷键在 CLI 模式下不可用。",
|
||||
"Show Advanced Settings": "显示高级选项",
|
||||
"Show all": "显示全部",
|
||||
@@ -674,13 +683,13 @@
|
||||
"Sidebar": "边栏",
|
||||
"Size": "大小",
|
||||
"Skip this version": "跳过该版本",
|
||||
"Skipped items: %d (use --retry-failed-items to retry decrypting them)": "跳过条目: %d (使用 —retry-failed-items 来尝试重新解密)",
|
||||
"Skipped items: %d (use --retry-failed-items to retry decrypting them)": "已跳过条目: %d (使用 --retry-failed-items 来尝试重新解密)",
|
||||
"Skipped: %d.": "已跳过: %d条。",
|
||||
"Solarised Dark": "日光暗 (Solarised)",
|
||||
"Solarised Light": "日光亮 (Solarised)",
|
||||
"Some items cannot be decrypted.": "一些项目无法被解密。",
|
||||
"Some items cannot be synchronised.": "一些条目无法被同步。",
|
||||
"Some items cannot be synchronised. Press for more info.": "某些条目无法同步。点按以获取更多信息。",
|
||||
"Some items cannot be decrypted.": "某些项目无法被解密。",
|
||||
"Some items cannot be synchronised.": "某些条目无法被同步。",
|
||||
"Some items cannot be synchronised. Press for more info.": "某些条目无法被同步。点按以获取更多信息。",
|
||||
"Sort notebooks by": "笔记本排序方式",
|
||||
"Sort notes by": "笔记排序方式",
|
||||
"Sort selected lines": "排序所选行",
|
||||
@@ -692,7 +701,7 @@
|
||||
"Split View": "分栏视图",
|
||||
"Start application minimised in the tray icon": "应用程序启动时最小化到托盘",
|
||||
"Start, stop or check the API server. To specify on which port it should run, set the api.port config variable. Commands are (%s).": "启动,停止或检查 API 服务。可以通过设置 ‘api.port’ 变量指定 API 服务运行在哪个端口上。执行命令 (%s) 。",
|
||||
"Starting decryption... Please wait as it may take several minutes depending on how much there is to decrypt.": "开始解密,请稍候... 取决于需解密的文件数量,解密环节可能需要等待几分钟。",
|
||||
"Starting decryption... Please wait as it may take several minutes depending on how much there is to decrypt.": "开始解密,请稍候... 取决于需解密的文件数量,该环节可能需要几分钟。",
|
||||
"Starting synchronisation...": "开始同步...",
|
||||
"Starting to edit note. Close the editor to get back to the prompt.": "开始编辑笔记。关闭编辑器将回到提示符。",
|
||||
"Statistics": "统计数据",
|
||||
@@ -700,7 +709,7 @@
|
||||
"Status": "状态",
|
||||
"Status: %s": "状态:%s",
|
||||
"Status: Started on port %d": "状态:在 %d 端口运行",
|
||||
"Step 1: Enable the clipper service": "步骤一: 启用网页剪辑服务",
|
||||
"Step 1: Enable the clipper service": "步骤一: 启用网页剪辑器",
|
||||
"Step 1: Open this URL in your browser to authorise the application:": "步骤一:在浏览器中打开此 URL 来授权应用程序:",
|
||||
"Step 2: Enter the code provided by Dropbox:": "步骤二:输入 Dropbox 提供的代码:",
|
||||
"Step 2: Install the extension": "步骤二: 安装扩展",
|
||||
@@ -742,7 +751,7 @@
|
||||
"Take photo": "拍照",
|
||||
"Tasks": "任务",
|
||||
"Text editor command": "文本编辑器命令",
|
||||
"Thank you! Your Joplin Cloud account is now setup and ready to use.": "感谢!你的 Joplin Cloud 帐号已经设置完毕。",
|
||||
"Thank you! Your Joplin Cloud account is now setup and ready to use.": "感谢!您的 Joplin Cloud 帐号已经设置完毕。",
|
||||
"The app is now going to close. Please relaunch it to complete the process.": "应用将要关闭。请重新启动它以完成此过程。",
|
||||
"The application has been authorised - you may now close this browser tab.": "授权成功 - 您可以关闭此页面了。",
|
||||
"The application has been authorised!": "应用已成功授权!",
|
||||
@@ -753,15 +762,15 @@
|
||||
"The default admin password is insecure and has not been changed! [Change it now](%s)": "默认管理员密码不安全且尚未更改",
|
||||
"The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.": "默认的加密方法已更改为一种更安全的方法,建议您将其应用于您的数据。",
|
||||
"The default encryption method has been changed, you should re-encrypt your data.": "默认的加密方法已更改,您应当重新加密数据。",
|
||||
"The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.": "该文本编辑器命令 (可包括参数) 将会被用于打开笔记。若未提供将尝试自动检测默认编辑器。",
|
||||
"The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an 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.": "因子属性决定了列表上的一项如何增长或缩小,以适应其容器中相对于其他项目的可用空间。因此因子为 2 的项是因数为 1 的项占用空间的两倍。重启软件以查看变化。",
|
||||
"The following attachments are being watched for changes:": "下列附件将被监控变化:",
|
||||
"The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.": "该文本编辑器命令 (可包含参数) 将会被用于打开笔记。若未提供将尝试自动检测默认编辑器。",
|
||||
"The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an 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.": "拉伸系数属性用于指定项目之间的容量比例。如,因子为 2 的项目所占容量是因子为 1 的项目的两倍。该更改在软件重启后生效。",
|
||||
"The following attachments are being watched for changes:": "下列附件发生的改动正在被监控:",
|
||||
"The following keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded key will still be able to decrypt and encrypt your data as usual.": "以下密钥使用了过时的加密算法,建议对其进行升级。 升级后的密钥仍将能够照常解密和加密您的数据。",
|
||||
"The Joplin mobile app does not currently support this type of link: %s": "Joplin 移动应用目前不支持这种类型的链接:%s",
|
||||
"The Joplin team has vetted this plugin and it meets our standards for security and performance.": "Joplin 团队已经通过了该插件的审查,它符合我们对于安全和性能的要求。",
|
||||
"The keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.": "具有这些 ID 的密钥用于加密你的部分项目,但应用程序目前无法访问它们。它们很可能最终会通过同步下载。",
|
||||
"The Joplin team has vetted this plugin and it meets our standards for security and performance.": "Joplin 团队已经核准了该插件,它符合我们对于安全和性能的要求。",
|
||||
"The keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.": "具有这些 ID 的密钥正被用于加密您的某些项,但应用程序目前无法访问它们。项目最终会通过同步获取,但目前可能仍未被同步。",
|
||||
"The master key has been upgraded successfully!": "主密钥已成功升级!",
|
||||
"The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.": "具有这些 ID 的主密钥用于加密某些项,但应用程序目前无法访问它们。最终它们很可能通过同步下载。",
|
||||
"The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.": "具有这些 ID 的主密钥正被用于加密某些项,但应用程序目前无法访问它们。项目最终会通过同步获取,但目前可能仍未被同步。",
|
||||
"The note \"%s\" has been successfully restored to the notebook \"%s\".": "笔记“%s”已成功恢复到笔记本“%s”中。",
|
||||
"The notebook could not be saved: %s": "无法保存笔记本:%s",
|
||||
"The notes have been imported: %s": "以下笔记已被导入: %s",
|
||||
@@ -771,8 +780,8 @@
|
||||
"The sync target needs to be upgraded. Press this banner to proceed.": "同步目标需要升级。按这个横幅继续。",
|
||||
"The tag \"%s\" already exists. Please choose a different name.": "标签 \"%s\" 已存在。请选择一个不同的名称。",
|
||||
"The target to synchronise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).": "所要同步的目标。每个同步目标都可能有名为 `sync.NUM.NAME` 的附加参数 (见下文) 。",
|
||||
"The Web Clipper needs your authorisation to access your data.": "Web Clipper 需要你的授权才能访问你的数据。",
|
||||
"The web clipper service is enabled and set to auto-start.": "网页剪辑服务已启用且将自动启动。",
|
||||
"The Web Clipper needs your authorisation to access your data.": "网页剪辑器需要您的授权才能访问您的数据。",
|
||||
"The web clipper service is enabled and set to auto-start.": "网页剪辑器已启用并已设置为自动启动。",
|
||||
"The web clipper service is not enabled.": "网页剪辑未启用。",
|
||||
"Theme": "主题",
|
||||
"There are currently no notes. Create one by clicking on the (+) button.": "当前没有任何笔记。点击 (+) 按钮创建。",
|
||||
@@ -780,9 +789,9 @@
|
||||
"There is no data to export.": "没有可导出的数据。",
|
||||
"There was a [conflict](%s) on the attachment below.\n\n%s": "以下附件存在一个[冲突](%s) \n\n%s",
|
||||
"There was an error downloading this attachment:": "下载此附件时出错:",
|
||||
"There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s": "设置你的 Joplin Cloud 帐号时发生了一个错误。请确认你的邮箱和密码是否正确,然后再试一次。错误是:\n\n%s",
|
||||
"There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s": "设置您的 Joplin Cloud 帐号时发生了一个错误。请确认您的邮箱和密码是否正确,然后再试一次。错误是:\n\n%s",
|
||||
"These items will remain on the device but will not be uploaded to the sync target. In order to find these items, either search for the title or the ID (which is displayed in brackets above).": "这些条目将只保留在本设备上,不会上传到同步目标。若需查找这些项,请搜索标题或 ID (显示在上方括号中) 。",
|
||||
"These plugins enhance the Markdown renderer with additional features. Please note that, while these features might be useful, they are not standard Markdown and thus most of them will only work in Joplin. Additionally, some of them are *incompatible* with the WYSIWYG editor. If you open a note that uses one of these plugins in that editor, you will lose the plugin formatting. It is indicated below which plugins are compatible or not with the WYSIWYG editor.": "这些插件为 Markdown 渲染器提供额外特性。请注意,尽管这些额外特性可能是可用的,但它们并非标准 Markdown 语法,因此许多功能只能在 Joplin 内运行。此外,其中的一些功能与所见即所得 (WYSIWYG) 编辑器 *不兼容*。如果您在上述编辑器中打开使用了这些插件的笔记,就将会丢失插件的格式。下面已表明插件与所见即所得 (WYSIWYG) 编辑器兼容与否。",
|
||||
"These plugins enhance the Markdown renderer with additional features. Please note that, while these features might be useful, they are not standard Markdown and thus most of them will only work in Joplin. Additionally, some of them are *incompatible* with the WYSIWYG editor. If you open a note that uses one of these plugins in that editor, you will lose the plugin formatting. It is indicated below which plugins are compatible or not with the WYSIWYG editor.": "这些插件为 Markdown 渲染器提供额外特性。请注意,尽管这些额外特性可能有用,但它们并非标准 Markdown 语法,因此许多功能只能在 Joplin 内运行。此外,其中的一些功能与所见即所得 (WYSIWYG) 编辑器 *不兼容*。如果您在上述 (WYSIWYG) 编辑器中打开使用了这些插件的笔记,就会丢失插件的格式。下已注明插件与所见即所得 (WYSIWYG) 编辑器兼容与否。",
|
||||
"This attachment is not downloaded or not decrypted yet": "该附件没有下载或者尚未解密",
|
||||
"This attachment is not downloaded or not decrypted yet.": "该附件没有下载或者尚未解密。",
|
||||
"This authorisation token is only needed to allow third-party applications to access Joplin.": "该授权令牌仅用于允许第三方应用程序访问 Joplin。",
|
||||
@@ -791,9 +800,9 @@
|
||||
"This note has been modified:": "该笔记已被修改:",
|
||||
"This note has no content. Click on \"%s\" to toggle the editor and edit the note.": "该笔记没有任何内容。点击 \"%s\" 切换到编辑器并编辑笔记。",
|
||||
"This note has no history": "此笔记没有历史记录",
|
||||
"This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.": "此富文本编辑器有许多限制,建议在使用它之前注意它们。",
|
||||
"This service allows the browser extension to communicate with Joplin. When enabling it your firewall may ask you to give permission to Joplin to listen to a particular port.": "该服务允许浏览器扩展与 Joplin 通信。当启用它时,你的防火墙可能会要求你允许 Joplin 监听一个特定的端口。",
|
||||
"This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.": "这将允许 Joplin 在后台运行,如果你经常修改和同步笔记,推荐启用该设置来减少可能的冲突。",
|
||||
"This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.": "此富文本编辑器有许多限制,建议在使用之前留意。",
|
||||
"This service allows the browser extension to communicate with Joplin. When enabling it your firewall may ask you to give permission to Joplin to listen to a particular port.": "该服务允许浏览器扩展插件与 Joplin 通信。当启用它时,您的防火墙可能会请求允许 Joplin 监听一个特定的端口。",
|
||||
"This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.": "这将允许 Joplin 在后台运行,如果您经常修改和同步笔记,推荐启用该设置来减少可能的冲突。",
|
||||
"This will open a new screen. Save your current changes?": "这将打开一个新页面。是否保存当前更改?",
|
||||
"This will remove the notebook from your collection and you will no longer have access to its content. Do you wish to continue?": "这将从您的收藏中删除该笔记本,您将不再有机会访问其内容。您希望继续吗?",
|
||||
"Time format": "时间格式",
|
||||
@@ -820,7 +829,7 @@
|
||||
"Toggle own sort order": "切换自有排序顺序",
|
||||
"Toggle safe mode": "切换安全模式",
|
||||
"Toggle sidebar": "切换边栏",
|
||||
"Toggle sort order field": "切换排序顺序字段",
|
||||
"Toggle sort order field": "切换排序字段",
|
||||
"Token has been copied to the clipboard!": "令牌已复制到剪贴板!",
|
||||
"Tools": "工具",
|
||||
"Total: %d/%d": "总数: %d/%d 条",
|
||||
@@ -836,12 +845,13 @@
|
||||
"Unknown item type downloaded - please upgrade Joplin to the latest version": "已下载项目为未知类型,请将 Joplin 升级至最新版本",
|
||||
"Unpublish note": "取消分享笔记",
|
||||
"Unshare": "取消分享",
|
||||
"Unshare this notebook? The recipients will no longer have access to its content.": "取消分享这个笔记本?接受者将无法再访问到它的内容。",
|
||||
"Unshare this notebook? The recipients will no longer have access to its content.": "取消分享这个笔记本?收件人将无法再访问到它的内容。",
|
||||
"Unsupported image type: %s": "不支持的图片类型:%s",
|
||||
"Unsupported link or message: %s": "不被支持的链接或信息:%s",
|
||||
"Untitled": "无标题",
|
||||
"Update": "更新",
|
||||
"Update profile": "更新资料",
|
||||
"Update profile": "更新配置文件",
|
||||
"Update total sizes": "更新总大小",
|
||||
"Updated": "已更新",
|
||||
"updated date": "更新日期",
|
||||
"Updated local items: %d.": "已更新本地项目: %d。",
|
||||
@@ -889,7 +899,7 @@
|
||||
"You currently have no notebooks.": "您当前没有笔记本。",
|
||||
"You do not have any installed plugin.": "您尚未安装任何插件。",
|
||||
"You may also type `status` for more information.": "输入 `status` 可获取更多信息。",
|
||||
"You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.": "您可以使用下面的工具重新加密您的数据。例如,在当您知道某些笔记使用了过时的加密方法时。",
|
||||
"You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.": "您可以使用下面的工具重新加密您的数据。例如当您知道某些笔记使用了过时的加密方法时,可使用该项目。",
|
||||
"Your choice: ": "您的选择: ",
|
||||
"Your data is going to be re-encrypted and synced again.": "您的数据将被重新加密并再次同步。",
|
||||
"Your master password is needed to decrypt some of your data.": "要解密您的某些数据,必须使用您的主密码。",
|
||||
|
||||
@@ -15,8 +15,17 @@ export class MarkupLanguageUtils {
|
||||
throw new Error(`Unsupported markup language: ${language}`);
|
||||
}
|
||||
|
||||
public extractImageUrls(language: MarkupLanguage, text: string) {
|
||||
return this.lib_(language).extractImageUrls(text);
|
||||
public extractImageUrls(language: MarkupLanguage, text: string): string[] {
|
||||
let urls: string[] = [];
|
||||
|
||||
if (language === MarkupLanguage.Any) {
|
||||
urls = urls.concat(this.lib_(MarkupLanguage.Markdown).extractImageUrls(text));
|
||||
urls = urls.concat(this.lib_(MarkupLanguage.Html).extractImageUrls(text));
|
||||
} else {
|
||||
urls = this.lib_(language).extractImageUrls(text);
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
// Create a new MarkupToHtml instance while injecting options specific to Joplin
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FolderEntity, FolderIcon, NoteEntity } from '../services/database/types';
|
||||
import { defaultFolderIcon, FolderEntity, FolderIcon, NoteEntity } from '../services/database/types';
|
||||
import BaseModel, { DeleteOptions } from '../BaseModel';
|
||||
import time from '../time';
|
||||
import { _ } from '../locale';
|
||||
@@ -767,7 +767,11 @@ export default class Folder extends BaseItem {
|
||||
}
|
||||
|
||||
public static unserializeIcon(icon: string): FolderIcon {
|
||||
return icon ? JSON.parse(icon) : null;
|
||||
if (!icon) return null;
|
||||
return {
|
||||
...defaultFolderIcon(),
|
||||
...JSON.parse(icon),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const { supportDir, setupDatabaseAndSynchronizer, switchClient } = require('../testing/test-utils.js');
|
||||
const Folder = require('../models/Folder').default;
|
||||
const Note = require('../models/Note').default;
|
||||
const Resource = require('../models/Resource').default;
|
||||
const shim = require('../shim').default;
|
||||
import { supportDir, setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils';
|
||||
import Folder from '../models/Folder';
|
||||
import Note from '../models/Note';
|
||||
import Resource from '../models/Resource';
|
||||
import shim from '../shim';
|
||||
|
||||
const testImagePath = `${supportDir}/photo.jpg`;
|
||||
|
||||
@@ -77,4 +77,24 @@ describe('models/Resource', function() {
|
||||
expect(originalStat.size).toBe(newStat.size);
|
||||
}));
|
||||
|
||||
// it('should encrypt a shared resource using the correct encryption key', (async () => {
|
||||
// const folder1 = await Folder.save({ title: 'folder1' });
|
||||
// const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
// await shim.attachFileToNote(note1, testImagePath);
|
||||
|
||||
// Resource.shareService_ = {
|
||||
// shareById: () => {
|
||||
// return {
|
||||
// master_key_id: '',
|
||||
// };
|
||||
// },
|
||||
// } as any;
|
||||
|
||||
// try {
|
||||
|
||||
// } finally {
|
||||
// Resource.shareService_ = null;
|
||||
// }
|
||||
// }));
|
||||
|
||||
});
|
||||
@@ -14,6 +14,7 @@ const { FsDriverDummy } = require('../fs-driver-dummy.js');
|
||||
import JoplinError from '../JoplinError';
|
||||
import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
|
||||
import { getEncryptionEnabled } from '../services/synchronizer/syncInfoUtils';
|
||||
import ShareService from '../services/share/ShareService';
|
||||
|
||||
export default class Resource extends BaseItem {
|
||||
|
||||
@@ -24,6 +25,8 @@ export default class Resource extends BaseItem {
|
||||
public static FETCH_STATUS_DONE = 2;
|
||||
public static FETCH_STATUS_ERROR = 3;
|
||||
|
||||
public static shareService_: ShareService = null;
|
||||
|
||||
public static fsDriver_: any;
|
||||
|
||||
static tableName() {
|
||||
@@ -39,6 +42,11 @@ export default class Resource extends BaseItem {
|
||||
return this.encryptionService_;
|
||||
}
|
||||
|
||||
protected static shareService() {
|
||||
if (!this.shareService_) throw new Error('Resource.shareService_ is not set!!');
|
||||
return this.shareService_;
|
||||
}
|
||||
|
||||
static isSupportedImageMimeType(type: string) {
|
||||
const imageMimeTypes = ['image/jpg', 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp'];
|
||||
return imageMimeTypes.indexOf(type.toLowerCase()) >= 0;
|
||||
@@ -197,6 +205,8 @@ export default class Resource extends BaseItem {
|
||||
public static async fullPathForSyncUpload(resource: ResourceEntity) {
|
||||
const plainTextPath = this.fullPath(resource);
|
||||
|
||||
const share = resource.share_id ? await this.shareService().shareById(resource.share_id) : null;
|
||||
|
||||
if (!getEncryptionEnabled() || !itemCanBeEncrypted(resource as any)) {
|
||||
// Normally not possible since itemsThatNeedSync should only return decrypted items
|
||||
if (resource.encryption_blob_encrypted) throw new Error('Trying to access encrypted resource but encryption is currently disabled');
|
||||
@@ -207,7 +217,9 @@ export default class Resource extends BaseItem {
|
||||
if (resource.encryption_blob_encrypted) return { path: encryptedPath, resource: resource };
|
||||
|
||||
try {
|
||||
await this.encryptionService().encryptFile(plainTextPath, encryptedPath);
|
||||
await this.encryptionService().encryptFile(plainTextPath, encryptedPath, {
|
||||
masterKeyId: share && share.master_key_id ? share.master_key_id : '',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') throw new JoplinError(`File not found:${error.toString()}`, 'fileNotFound');
|
||||
throw error;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expectNotThrow, naughtyStrings, setupDatabaseAndSynchronizer, switchClient } from '../testing/test-utils';
|
||||
import Note from '../models/Note';
|
||||
import Revision from '../models/Revision';
|
||||
import Revision, { ObjectPatch } from '../models/Revision';
|
||||
|
||||
describe('models/Revision', function() {
|
||||
|
||||
@@ -139,6 +139,53 @@ describe('models/Revision', function() {
|
||||
expect(JSON.stringify(merged)).toBe(JSON.stringify(newObject));
|
||||
}));
|
||||
|
||||
it('should handle invalid object patch', (async () => {
|
||||
const oldObject = {
|
||||
one: '123',
|
||||
two: '456',
|
||||
three: '789',
|
||||
};
|
||||
|
||||
const brokenPatch = `{"new":{"four":"444
|
||||
"},"deleted":["one"]}`;
|
||||
|
||||
const expected = {
|
||||
two: '456',
|
||||
three: '789',
|
||||
four: '444',
|
||||
};
|
||||
|
||||
const merged = Revision.applyObjectPatch(oldObject, brokenPatch);
|
||||
|
||||
expect(JSON.stringify(merged)).toBe(JSON.stringify(expected));
|
||||
}));
|
||||
|
||||
it('should not strip off newlines from object values', (async () => {
|
||||
const oldObject = {
|
||||
one: '123',
|
||||
two: '456',
|
||||
three: '789',
|
||||
};
|
||||
|
||||
const patch: ObjectPatch = {
|
||||
'new': {
|
||||
'four': 'one line\ntwo line',
|
||||
},
|
||||
'deleted': [],
|
||||
};
|
||||
|
||||
const expected = {
|
||||
one: '123',
|
||||
two: '456',
|
||||
three: '789',
|
||||
four: 'one line\ntwo line',
|
||||
};
|
||||
|
||||
const merged = Revision.applyObjectPatch(oldObject, JSON.stringify(patch));
|
||||
|
||||
expect(JSON.stringify(merged)).toBe(JSON.stringify(expected));
|
||||
}));
|
||||
|
||||
it('should move target revision to the top', (async () => {
|
||||
const revs = [
|
||||
{ id: '123' },
|
||||
@@ -182,6 +229,10 @@ describe('models/Revision', function() {
|
||||
%0A%0A# `,
|
||||
expected: [-(19 + 27 + 2), 17 + 67 + 4],
|
||||
},
|
||||
{
|
||||
patch: '',
|
||||
expected: [-0, +0],
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
|
||||
@@ -8,6 +8,11 @@ const { sprintf } = require('sprintf-js');
|
||||
|
||||
const dmp = new DiffMatchPatch();
|
||||
|
||||
export interface ObjectPatch {
|
||||
new: Record<string, any>;
|
||||
deleted: string[];
|
||||
}
|
||||
|
||||
export default class Revision extends BaseItem {
|
||||
static tableName() {
|
||||
return 'revisions';
|
||||
@@ -48,7 +53,7 @@ export default class Revision extends BaseItem {
|
||||
|
||||
private static isNewPatch(patch: string): boolean {
|
||||
if (!patch) return true;
|
||||
return patch.indexOf('[{') === 0;
|
||||
return patch.indexOf('[{') === 0 || patch === '[]';
|
||||
}
|
||||
|
||||
public static applyTextPatch(text: string, patch: string): string {
|
||||
@@ -58,7 +63,7 @@ export default class Revision extends BaseItem {
|
||||
// An empty patch should be '[]', but legacy data may be just "".
|
||||
// However an empty string would make JSON.parse fail so we set it
|
||||
// to '[]'.
|
||||
const result = dmp.patch_apply(JSON.parse(patch ? patch : '[]'), text);
|
||||
const result = dmp.patch_apply(this.parsePatch(patch), text);
|
||||
if (!result || !result.length) throw new Error('Could not apply patch');
|
||||
return result[0];
|
||||
}
|
||||
@@ -78,10 +83,10 @@ export default class Revision extends BaseItem {
|
||||
return true;
|
||||
}
|
||||
|
||||
static createObjectPatch(oldObject: any, newObject: any) {
|
||||
public static createObjectPatch(oldObject: any, newObject: any) {
|
||||
if (!oldObject) oldObject = {};
|
||||
|
||||
const output: any = {
|
||||
const output: ObjectPatch = {
|
||||
new: {},
|
||||
deleted: [],
|
||||
};
|
||||
@@ -100,16 +105,22 @@ export default class Revision extends BaseItem {
|
||||
return JSON.stringify(output);
|
||||
}
|
||||
|
||||
static applyObjectPatch(object: any, patch: any) {
|
||||
patch = JSON.parse(patch);
|
||||
// We need to sanitise the object patch because it seems some are broken and
|
||||
// may contain new lines: https://github.com/laurent22/joplin/issues/6209
|
||||
private static sanitizeObjectPatch(patch: string): string {
|
||||
return patch.replace(/[\n\r]/g, '');
|
||||
}
|
||||
|
||||
public static applyObjectPatch(object: any, patch: string) {
|
||||
const parsedPatch: ObjectPatch = JSON.parse(this.sanitizeObjectPatch(patch));
|
||||
const output = Object.assign({}, object);
|
||||
|
||||
for (const k in patch.new) {
|
||||
output[k] = patch.new[k];
|
||||
for (const k in parsedPatch.new) {
|
||||
output[k] = parsedPatch.new[k];
|
||||
}
|
||||
|
||||
for (let i = 0; i < patch.deleted.length; i++) {
|
||||
delete output[patch.deleted[i]];
|
||||
for (let i = 0; i < parsedPatch.deleted.length; i++) {
|
||||
delete output[parsedPatch.deleted[i]];
|
||||
}
|
||||
|
||||
return output;
|
||||
@@ -120,7 +131,7 @@ export default class Revision extends BaseItem {
|
||||
// line, so that it can be processed by patchStats().
|
||||
private static newPatchToDiffFormat(patch: string): string {
|
||||
const changeList: string[] = [];
|
||||
const patchArray = JSON.parse(patch);
|
||||
const patchArray = this.parsePatch(patch);
|
||||
for (const patchItem of patchArray) {
|
||||
for (const d of patchItem.diffs) {
|
||||
if (d[0] !== 0) changeList.push(d[0] < 0 ? `-${d[1].replace(/[\n\r]/g, ' ')}` : `+${d[1].trim().replace(/[\n\r]/g, ' ')}`);
|
||||
@@ -237,7 +248,7 @@ export default class Revision extends BaseItem {
|
||||
}
|
||||
|
||||
// Note: revs must be sorted by update_time ASC (as returned by allByType)
|
||||
static async mergeDiffs(revision: RevisionEntity, revs: RevisionEntity[] = null) {
|
||||
public static async mergeDiffs(revision: RevisionEntity, revs: RevisionEntity[] = null) {
|
||||
if (!('encryption_applied' in revision) || !!revision.encryption_applied) throw new JoplinError('Target revision is encrypted', 'revision_encrypted');
|
||||
|
||||
if (!revs) {
|
||||
@@ -273,7 +284,12 @@ export default class Revision extends BaseItem {
|
||||
if (rev.encryption_applied) throw new JoplinError(sprintf('Revision "%s" is encrypted', rev.id), 'revision_encrypted');
|
||||
output.title = this.applyTextPatch(output.title, rev.title_diff);
|
||||
output.body = this.applyTextPatch(output.body, rev.body_diff);
|
||||
output.metadata = this.applyObjectPatch(output.metadata, rev.metadata_diff);
|
||||
try {
|
||||
output.metadata = this.applyObjectPatch(output.metadata, rev.metadata_diff);
|
||||
} catch (error) {
|
||||
error.message = `Revision ${rev.id}: Could not apply patch: ${error.message}: ${rev.metadata_diff}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
@@ -330,4 +346,9 @@ export default class Revision extends BaseItem {
|
||||
const existingRev = await Revision.latestRevision(itemType, itemId);
|
||||
return existingRev && existingRev.item_updated_time === updatedTime;
|
||||
}
|
||||
|
||||
private static parsePatch(patch: any): any[] {
|
||||
return patch ? JSON.parse(patch) : [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import Setting, { SettingSectionSource } from '../models/Setting';
|
||||
import Setting, { SettingSectionSource, SettingStorage } from '../models/Setting';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow, msleep } from '../testing/test-utils';
|
||||
import * as fs from 'fs-extra';
|
||||
import { readFile, stat, mkdirp, writeFile, pathExists, readdir } from 'fs-extra';
|
||||
import Logger from '../Logger';
|
||||
import { defaultProfileConfig } from '../services/profileConfig/types';
|
||||
import { createNewProfile, saveProfileConfig } from '../services/profileConfig';
|
||||
import initProfile from '../services/profileConfig/initProfile';
|
||||
|
||||
async function loadSettingsFromFile(): Promise<any> {
|
||||
return JSON.parse(await fs.readFile(Setting.settingFilePath, 'utf8'));
|
||||
return JSON.parse(await readFile(Setting.settingFilePath, 'utf8'));
|
||||
}
|
||||
|
||||
const switchToSubProfileSettings = async () => {
|
||||
await Setting.reset();
|
||||
const rootProfileDir = Setting.value('profileDir');
|
||||
const profileConfigPath = `${rootProfileDir}/profiles.json`;
|
||||
let profileConfig = defaultProfileConfig();
|
||||
profileConfig = createNewProfile(profileConfig, 'Sub-profile');
|
||||
profileConfig.currentProfile = 1;
|
||||
await saveProfileConfig(profileConfigPath, profileConfig);
|
||||
const { profileDir } = await initProfile(rootProfileDir);
|
||||
await mkdirp(profileDir);
|
||||
await Setting.load();
|
||||
};
|
||||
|
||||
describe('models/Setting', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
@@ -180,19 +196,19 @@ describe('models/Setting', function() {
|
||||
{
|
||||
// Double-check that timestamp is indeed changed when the content is
|
||||
// changed.
|
||||
const beforeStat = await fs.stat(Setting.settingFilePath);
|
||||
const beforeStat = await stat(Setting.settingFilePath);
|
||||
await msleep(1001);
|
||||
Setting.setValue('sync.mobileWifiOnly', false);
|
||||
await Setting.saveAll();
|
||||
const afterStat = await fs.stat(Setting.settingFilePath);
|
||||
const afterStat = await stat(Setting.settingFilePath);
|
||||
expect(afterStat.mtime.getTime()).toBeGreaterThan(beforeStat.mtime.getTime());
|
||||
}
|
||||
|
||||
{
|
||||
const beforeStat = await fs.stat(Setting.settingFilePath);
|
||||
const beforeStat = await stat(Setting.settingFilePath);
|
||||
await msleep(1001);
|
||||
Setting.setValue('sync.mobileWifiOnly', false);
|
||||
const afterStat = await fs.stat(Setting.settingFilePath);
|
||||
const afterStat = await stat(Setting.settingFilePath);
|
||||
await Setting.saveAll();
|
||||
expect(afterStat.mtime.getTime()).toBe(beforeStat.mtime.getTime());
|
||||
}
|
||||
@@ -200,7 +216,7 @@ describe('models/Setting', function() {
|
||||
|
||||
it('should handle invalid JSON', (async () => {
|
||||
const badContent = '{ oopsIforgotTheQuotes: true}';
|
||||
await fs.writeFile(Setting.settingFilePath, badContent, 'utf8');
|
||||
await writeFile(Setting.settingFilePath, badContent, 'utf8');
|
||||
await Setting.reset();
|
||||
|
||||
Logger.globalLogger.enabled = false;
|
||||
@@ -208,12 +224,12 @@ describe('models/Setting', function() {
|
||||
Logger.globalLogger.enabled = true;
|
||||
|
||||
// Invalid JSON file has been moved to .bak file
|
||||
expect(await fs.pathExists(Setting.settingFilePath)).toBe(false);
|
||||
expect(await pathExists(Setting.settingFilePath)).toBe(false);
|
||||
|
||||
const files = await fs.readdir(Setting.value('profileDir'));
|
||||
const files = await readdir(Setting.value('profileDir'));
|
||||
expect(files.length).toBe(1);
|
||||
expect(files[0].endsWith('.bak')).toBe(true);
|
||||
expect(await fs.readFile(`${Setting.value('profileDir')}/${files[0]}`, 'utf8')).toBe(badContent);
|
||||
expect(await readFile(`${Setting.value('profileDir')}/${files[0]}`, 'utf8')).toBe(badContent);
|
||||
}));
|
||||
|
||||
it('should allow applying default migrations', (async () => {
|
||||
@@ -256,4 +272,67 @@ describe('models/Setting', function() {
|
||||
expect(Setting.value('style.editor.contentMaxWidth')).toBe(600); // Changed
|
||||
}));
|
||||
|
||||
it('should load sub-profile settings', async () => {
|
||||
await Setting.reset();
|
||||
|
||||
Setting.setValue('locale', 'fr_FR'); // Global setting
|
||||
Setting.setValue('theme', Setting.THEME_DARK); // Global setting
|
||||
Setting.setValue('sync.target', 9); // Local setting
|
||||
await Setting.saveAll();
|
||||
|
||||
await switchToSubProfileSettings();
|
||||
|
||||
expect(Setting.value('locale')).toBe('fr_FR'); // Should come from the root profile
|
||||
expect(Setting.value('theme')).toBe(Setting.THEME_DARK); // Should come from the root profile
|
||||
expect(Setting.value('sync.target')).toBe(0); // Should come from the local profile
|
||||
|
||||
// Also check that the special loadOne() function works as expected
|
||||
|
||||
expect((await Setting.loadOne('locale')).value).toBe('fr_FR');
|
||||
expect((await Setting.loadOne('theme')).value).toBe(Setting.THEME_DARK);
|
||||
expect((await Setting.loadOne('sync.target')).value).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should save sub-profile settings', async () => {
|
||||
await Setting.reset();
|
||||
Setting.setValue('locale', 'fr_FR'); // Global setting
|
||||
Setting.setValue('theme', Setting.THEME_DARK); // Global setting
|
||||
await Setting.saveAll();
|
||||
|
||||
await switchToSubProfileSettings();
|
||||
|
||||
Setting.setValue('locale', 'en_GB'); // Should be saved to global
|
||||
Setting.setValue('sync.target', 8); // Should be saved to local
|
||||
|
||||
await Setting.saveAll();
|
||||
await Setting.reset();
|
||||
await Setting.load();
|
||||
|
||||
expect(Setting.value('locale')).toBe('en_GB');
|
||||
expect(Setting.value('theme')).toBe(Setting.THEME_DARK);
|
||||
expect(Setting.value('sync.target')).toBe(8);
|
||||
|
||||
// Double-check that actual file content is correct
|
||||
|
||||
const globalSettings = JSON.parse(await readFile(`${Setting.value('rootProfileDir')}/settings-1.json`, 'utf8'));
|
||||
const localSettings = JSON.parse(await readFile(`${Setting.value('profileDir')}/settings-1.json`, 'utf8'));
|
||||
|
||||
expect(globalSettings).toEqual({
|
||||
'$schema': 'https://joplinapp.org/schema/settings.json',
|
||||
locale: 'en_GB',
|
||||
theme: 2,
|
||||
});
|
||||
|
||||
expect(localSettings).toEqual({
|
||||
'$schema': 'https://joplinapp.org/schema/settings.json',
|
||||
'sync.target': 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('all global settings should be saved to file', async () => {
|
||||
for (const [k, v] of Object.entries(Setting.metadata())) {
|
||||
if (v.isGlobal && v.storage !== SettingStorage.File) throw new Error(`Setting "${k}" is global but storage is not "file"`);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ import SyncTargetRegistry from '../SyncTargetRegistry';
|
||||
import time from '../time';
|
||||
import FileHandler, { SettingValues } from './settings/FileHandler';
|
||||
import Logger from '../Logger';
|
||||
import mergeGlobalAndLocalSettings from '../services/profileConfig/mergeGlobalAndLocalSettings';
|
||||
import splitGlobalAndLocalSettings from '../services/profileConfig/splitGlobalAndLocalSettings';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const ObjectUtils = require('../ObjectUtils');
|
||||
const { toTitleCase } = require('../string-utils.js');
|
||||
@@ -59,6 +61,18 @@ export interface SettingItem {
|
||||
autoSave?: boolean;
|
||||
storage?: SettingStorage;
|
||||
hideLabel?: boolean;
|
||||
|
||||
// In a multi-profile context, all settings are by default local - they take
|
||||
// their value from the current profile. This flag can be set to specify
|
||||
// that the setting is global and that its value should come from the root
|
||||
// profile. This flag only applies to sub-profiles.
|
||||
//
|
||||
// At the moment, all global settings must be saved to file (have the
|
||||
// storage attribute set to "file") because it's simpler to load the root
|
||||
// profile settings.json than load the whole SQLite database. This
|
||||
// restriction is not an issue normally since all settings that are
|
||||
// considered global are also the user-facing ones.
|
||||
isGlobal?: boolean;
|
||||
}
|
||||
|
||||
interface SettingItems {
|
||||
@@ -112,6 +126,7 @@ export interface Constants {
|
||||
resourceDirName: string;
|
||||
resourceDir: string;
|
||||
profileDir: string;
|
||||
rootProfileDir: string;
|
||||
tempDir: string;
|
||||
pluginDataDir: string;
|
||||
cacheDir: string;
|
||||
@@ -119,6 +134,7 @@ export interface Constants {
|
||||
flagOpenDevTools: boolean;
|
||||
syncVersion: number;
|
||||
startupDevPlugins: string[];
|
||||
isSubProfile: boolean;
|
||||
}
|
||||
|
||||
interface SettingSections {
|
||||
@@ -243,6 +259,7 @@ class Setting extends BaseModel {
|
||||
resourceDirName: '',
|
||||
resourceDir: '',
|
||||
profileDir: '',
|
||||
rootProfileDir: '',
|
||||
tempDir: '',
|
||||
pluginDataDir: '',
|
||||
cacheDir: '',
|
||||
@@ -250,6 +267,7 @@ class Setting extends BaseModel {
|
||||
flagOpenDevTools: false,
|
||||
syncVersion: 3,
|
||||
startupDevPlugins: [],
|
||||
isSubProfile: false,
|
||||
};
|
||||
|
||||
public static autoSaveEnabled = true;
|
||||
@@ -264,6 +282,7 @@ class Setting extends BaseModel {
|
||||
private static customSections_: SettingSections = {};
|
||||
private static changedKeys_: string[] = [];
|
||||
private static fileHandler_: FileHandler = null;
|
||||
private static rootFileHandler_: FileHandler = null;
|
||||
private static settingFilename_: string = 'settings.json';
|
||||
|
||||
static tableName() {
|
||||
@@ -291,6 +310,10 @@ class Setting extends BaseModel {
|
||||
return `${this.value('profileDir')}/${this.settingFilename_}`;
|
||||
}
|
||||
|
||||
public static get rootSettingFilePath(): string {
|
||||
return `${this.value('rootProfileDir')}/${this.settingFilename_}`;
|
||||
}
|
||||
|
||||
public static get settingFilename(): string {
|
||||
return this.settingFilename_;
|
||||
}
|
||||
@@ -306,6 +329,13 @@ class Setting extends BaseModel {
|
||||
return this.fileHandler_;
|
||||
}
|
||||
|
||||
public static get rootFileHandler(): FileHandler {
|
||||
if (!this.rootFileHandler_) {
|
||||
this.rootFileHandler_ = new FileHandler(this.rootSettingFilePath);
|
||||
}
|
||||
return this.rootFileHandler_;
|
||||
}
|
||||
|
||||
static keychainService() {
|
||||
if (!this.keychainService_) throw new Error('keychainService has not been set!!');
|
||||
return this.keychainService_;
|
||||
@@ -359,6 +389,7 @@ class Setting extends BaseModel {
|
||||
public: false,
|
||||
appTypes: [AppType.Desktop],
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
'sync.openSyncWizard': {
|
||||
@@ -669,6 +700,7 @@ class Setting extends BaseModel {
|
||||
};
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
'sync.3.auth': { value: '', type: SettingItemType.String, public: false },
|
||||
@@ -687,7 +719,7 @@ class Setting extends BaseModel {
|
||||
'sync.9.context': { value: '', type: SettingItemType.String, public: false },
|
||||
'sync.10.context': { value: '', type: SettingItemType.String, public: false },
|
||||
|
||||
'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, storage: SettingStorage.File, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
|
||||
'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, storage: SettingStorage.File, isGlobal: true, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
|
||||
|
||||
// The active folder ID is guaranteed to be valid as long as there's at least one
|
||||
// existing folder, so it is a good default in contexts where there's no currently
|
||||
@@ -695,7 +727,7 @@ class Setting extends BaseModel {
|
||||
// to the last folder that was selected.
|
||||
activeFolderId: { value: '', type: SettingItemType.String, public: false },
|
||||
|
||||
richTextBannerDismissed: { value: false, type: SettingItemType.Bool, public: false },
|
||||
richTextBannerDismissed: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false },
|
||||
|
||||
firstStart: { value: true, type: SettingItemType.Bool, public: false },
|
||||
locale: {
|
||||
@@ -708,6 +740,7 @@ class Setting extends BaseModel {
|
||||
return ObjectUtils.sortByValue(supportedLocalesToLanguages({ includeStats: true }));
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
dateFormat: {
|
||||
value: Setting.DATE_FORMAT_1,
|
||||
@@ -730,6 +763,7 @@ class Setting extends BaseModel {
|
||||
return options;
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
timeFormat: {
|
||||
value: Setting.TIME_FORMAT_1,
|
||||
@@ -746,6 +780,7 @@ class Setting extends BaseModel {
|
||||
return options;
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
theme: {
|
||||
@@ -761,6 +796,7 @@ class Setting extends BaseModel {
|
||||
section: 'appearance',
|
||||
options: () => themeOptions(),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
themeAutoDetect: {
|
||||
@@ -771,6 +807,7 @@ class Setting extends BaseModel {
|
||||
public: true,
|
||||
label: () => _('Automatically switch theme to match system theme'),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
preferredLightTheme: {
|
||||
@@ -786,6 +823,7 @@ class Setting extends BaseModel {
|
||||
section: 'appearance',
|
||||
options: () => themeOptions(),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
preferredDarkTheme: {
|
||||
@@ -801,6 +839,7 @@ class Setting extends BaseModel {
|
||||
section: 'appearance',
|
||||
options: () => themeOptions(),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
notificationPermission: {
|
||||
@@ -809,7 +848,7 @@ class Setting extends BaseModel {
|
||||
public: false,
|
||||
},
|
||||
|
||||
showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, public: false, advanced: true, appTypes: [AppType.Desktop], label: () => _('Show note counts') },
|
||||
showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false, advanced: true, appTypes: [AppType.Desktop], label: () => _('Show note counts') },
|
||||
|
||||
layoutButtonSequence: {
|
||||
value: Setting.LAYOUT_ALL,
|
||||
@@ -824,9 +863,10 @@ class Setting extends BaseModel {
|
||||
[Setting.LAYOUT_VIEWER_SPLIT]: _('%s / %s', _('Viewer'), _('Split View')),
|
||||
}),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
uncompletedTodosOnTop: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Uncompleted to-dos on top') },
|
||||
showCompletedTodos: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Show completed to-dos') },
|
||||
uncompletedTodosOnTop: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Uncompleted to-dos on top') },
|
||||
showCompletedTodos: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Show completed to-dos') },
|
||||
'notes.sortOrder.field': {
|
||||
value: 'user_updated_time',
|
||||
type: SettingItemType.String,
|
||||
@@ -845,6 +885,7 @@ class Setting extends BaseModel {
|
||||
return options;
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
'editor.autoMatchingBraces': {
|
||||
value: true,
|
||||
@@ -854,8 +895,9 @@ class Setting extends BaseModel {
|
||||
appTypes: [AppType.Desktop],
|
||||
label: () => _('Auto-pair braces, parenthesis, quotations, etc.'),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
'notes.sortOrder.reverse': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
|
||||
'notes.sortOrder.reverse': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
|
||||
// NOTE: A setting whose name starts with 'notes.sortOrder' is special,
|
||||
// which implies changing the setting automatically triggers the reflesh of notes.
|
||||
// See lib/BaseApplication.ts/generalMiddleware() for details.
|
||||
@@ -868,6 +910,7 @@ class Setting extends BaseModel {
|
||||
label: () => _('Show sort order buttons'),
|
||||
// description: () => _('If true, sort order buttons (field + reverse) for notes are shown at the top of Note List.'),
|
||||
appTypes: [AppType.Desktop],
|
||||
isGlobal: true,
|
||||
},
|
||||
'notes.perFieldReversalEnabled': {
|
||||
value: true,
|
||||
@@ -931,8 +974,8 @@ class Setting extends BaseModel {
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'folders.sortOrder.reverse': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
|
||||
trackLocation: { value: true, type: SettingItemType.Bool, section: 'note', storage: SettingStorage.File, public: true, label: () => _('Save geo-location with notes') },
|
||||
'folders.sortOrder.reverse': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
|
||||
trackLocation: { value: true, type: SettingItemType.Bool, section: 'note', storage: SettingStorage.File, isGlobal: true, public: true, label: () => _('Save geo-location with notes') },
|
||||
|
||||
// 2020-10-29: For now disable the beta editor due to
|
||||
// underlying bugs in the TextInput component which we cannot
|
||||
@@ -947,6 +990,8 @@ class Setting extends BaseModel {
|
||||
appTypes: [AppType.Mobile],
|
||||
label: () => 'Opt-in to the editor beta',
|
||||
description: () => 'This beta adds list continuation and syntax highlighting. If you find bugs, please report them in the Discourse forum.',
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
newTodoFocus: {
|
||||
@@ -964,6 +1009,7 @@ class Setting extends BaseModel {
|
||||
};
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
newNoteFocus: {
|
||||
value: 'body',
|
||||
@@ -980,6 +1026,7 @@ class Setting extends BaseModel {
|
||||
};
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
'plugins.states': {
|
||||
@@ -1005,31 +1052,31 @@ class Setting extends BaseModel {
|
||||
},
|
||||
|
||||
// Deprecated - use markdown.plugin.*
|
||||
'markdown.softbreaks': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
||||
'markdown.typographer': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
||||
'markdown.softbreaks': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
||||
'markdown.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
||||
// Deprecated
|
||||
|
||||
'markdown.plugin.softbreaks': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable soft breaks')}${wysiwygYes}` },
|
||||
'markdown.plugin.typographer': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable typographer support')}${wysiwygYes}` },
|
||||
'markdown.plugin.linkify': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Linkify')}${wysiwygYes}` },
|
||||
'markdown.plugin.softbreaks': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable soft breaks')}${wysiwygYes}` },
|
||||
'markdown.plugin.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable typographer support')}${wysiwygYes}` },
|
||||
'markdown.plugin.linkify': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Linkify')}${wysiwygYes}` },
|
||||
|
||||
'markdown.plugin.katex': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable math expressions')}${wysiwygYes}` },
|
||||
'markdown.plugin.fountain': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Fountain syntax support')}${wysiwygYes}` },
|
||||
'markdown.plugin.mermaid': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Mermaid diagrams support')}${wysiwygYes}` },
|
||||
'markdown.plugin.katex': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable math expressions')}${wysiwygYes}` },
|
||||
'markdown.plugin.fountain': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Fountain syntax support')}${wysiwygYes}` },
|
||||
'markdown.plugin.mermaid': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Mermaid diagrams support')}${wysiwygYes}` },
|
||||
|
||||
'markdown.plugin.audioPlayer': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable audio player')}${wysiwygNo}` },
|
||||
'markdown.plugin.videoPlayer': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable video player')}${wysiwygNo}` },
|
||||
'markdown.plugin.pdfViewer': { storage: SettingStorage.File, value: !mobilePlatform, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Desktop], label: () => `${_('Enable PDF viewer')}${wysiwygNo}` },
|
||||
'markdown.plugin.mark': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ==mark== syntax')}${wysiwygYes}` },
|
||||
'markdown.plugin.footnote': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable footnotes')}${wysiwygNo}` },
|
||||
'markdown.plugin.toc': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` },
|
||||
'markdown.plugin.sub': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ~sub~ syntax')}${wysiwygYes}` },
|
||||
'markdown.plugin.sup': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ^sup^ syntax')}${wysiwygYes}` },
|
||||
'markdown.plugin.deflist': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.abbr': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.emoji': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` },
|
||||
'markdown.plugin.insert': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` },
|
||||
'markdown.plugin.multitable': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` },
|
||||
'markdown.plugin.audioPlayer': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable audio player')}${wysiwygNo}` },
|
||||
'markdown.plugin.videoPlayer': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable video player')}${wysiwygNo}` },
|
||||
'markdown.plugin.pdfViewer': { storage: SettingStorage.File, isGlobal: true, value: !mobilePlatform, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Desktop], label: () => `${_('Enable PDF viewer')}${wysiwygNo}` },
|
||||
'markdown.plugin.mark': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ==mark== syntax')}${wysiwygYes}` },
|
||||
'markdown.plugin.footnote': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable footnotes')}${wysiwygNo}` },
|
||||
'markdown.plugin.toc': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` },
|
||||
'markdown.plugin.sub': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ~sub~ syntax')}${wysiwygYes}` },
|
||||
'markdown.plugin.sup': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ^sup^ syntax')}${wysiwygYes}` },
|
||||
'markdown.plugin.deflist': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.abbr': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.emoji': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` },
|
||||
'markdown.plugin.insert': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` },
|
||||
'markdown.plugin.multitable': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` },
|
||||
|
||||
// Tray icon (called AppIndicator) doesn't work in Ubuntu
|
||||
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
|
||||
@@ -1046,9 +1093,10 @@ class Setting extends BaseModel {
|
||||
return platform === 'linux' ? _('Note: Does not work in all desktop environments.') : _('This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.');
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
startMinimized: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: true, appTypes: [AppType.Desktop], label: () => _('Start application minimised in the tray icon') },
|
||||
startMinimized: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: true, appTypes: [AppType.Desktop], label: () => _('Start application minimised in the tray icon') },
|
||||
|
||||
collapsedFolderIds: { value: [], type: SettingItemType.Array, public: false },
|
||||
|
||||
@@ -1072,9 +1120,9 @@ class Setting extends BaseModel {
|
||||
},
|
||||
|
||||
// Deprecated in favour of windowContentZoomFactor
|
||||
'style.zoom': { value: 100, type: SettingItemType.Int, public: false, storage: SettingStorage.File, appTypes: [AppType.Desktop], section: 'appearance', label: () => '', minimum: 50, maximum: 500, step: 10 },
|
||||
'style.zoom': { value: 100, type: SettingItemType.Int, public: false, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => '', minimum: 50, maximum: 500, step: 10 },
|
||||
|
||||
'style.editor.fontSize': { value: 15, type: SettingItemType.Int, public: true, storage: SettingStorage.File, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor font size'), minimum: 4, maximum: 50, step: 1 },
|
||||
'style.editor.fontSize': { value: 15, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor font size'), minimum: 4, maximum: 50, step: 1 },
|
||||
'style.editor.fontFamily':
|
||||
(mobilePlatform) ?
|
||||
({
|
||||
@@ -1101,6 +1149,7 @@ class Setting extends BaseModel {
|
||||
};
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
}) : {
|
||||
value: '',
|
||||
type: SettingItemType.String,
|
||||
@@ -1111,6 +1160,7 @@ class Setting extends BaseModel {
|
||||
description: () =>
|
||||
_('Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.'),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
'style.editor.monospaceFontFamily': {
|
||||
value: '',
|
||||
@@ -1122,9 +1172,10 @@ class Setting extends BaseModel {
|
||||
description: () =>
|
||||
_('Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.'),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') },
|
||||
'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true,appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') },
|
||||
|
||||
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, public: false, appTypes: [AppType.Desktop] },
|
||||
|
||||
@@ -1149,6 +1200,8 @@ class Setting extends BaseModel {
|
||||
label: () => _('Custom stylesheet for rendered Markdown'),
|
||||
section: 'appearance',
|
||||
advanced: true,
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
'style.customCss.joplinApp': {
|
||||
value: null,
|
||||
@@ -1167,6 +1220,8 @@ class Setting extends BaseModel {
|
||||
section: 'appearance',
|
||||
advanced: true,
|
||||
description: () => 'CSS file support is provided for your convenience, but they are advanced settings, and styles you define may break from one version to the next. If you want to use them, please know that it might require regular development work from you to keep them working. The Joplin team cannot make a commitment to keep the application HTML structure stable.',
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
'sync.clearLocalSyncStateButton': {
|
||||
@@ -1192,9 +1247,9 @@ class Setting extends BaseModel {
|
||||
},
|
||||
|
||||
|
||||
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
|
||||
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
|
||||
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: false },
|
||||
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
|
||||
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
|
||||
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false },
|
||||
'sync.interval': {
|
||||
value: 300,
|
||||
type: SettingItemType.Int,
|
||||
@@ -1214,6 +1269,7 @@ class Setting extends BaseModel {
|
||||
};
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
'sync.mobileWifiOnly': {
|
||||
value: false,
|
||||
@@ -1223,12 +1279,13 @@ class Setting extends BaseModel {
|
||||
label: () => _('Synchronise only over WiFi connection'),
|
||||
storage: SettingStorage.File,
|
||||
appTypes: [AppType.Mobile],
|
||||
isGlobal: true,
|
||||
},
|
||||
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, public: false, appTypes: [AppType.Desktop] },
|
||||
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] },
|
||||
tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
|
||||
folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
|
||||
editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', storage: SettingStorage.File, public: true, appTypes: [AppType.Cli, AppType.Desktop], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
|
||||
'export.pdfPageSize': { value: 'A4', type: SettingItemType.String, advanced: true, storage: SettingStorage.File, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page size for PDF export'), options: () => {
|
||||
editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Cli, AppType.Desktop], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
|
||||
'export.pdfPageSize': { value: 'A4', type: SettingItemType.String, advanced: true, storage: SettingStorage.File, isGlobal: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page size for PDF export'), options: () => {
|
||||
return {
|
||||
'A4': _('A4'),
|
||||
'Letter': _('Letter'),
|
||||
@@ -1238,7 +1295,7 @@ class Setting extends BaseModel {
|
||||
'Legal': _('Legal'),
|
||||
};
|
||||
} },
|
||||
'export.pdfPageOrientation': { value: 'portrait', type: SettingItemType.String, storage: SettingStorage.File, advanced: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page orientation for PDF export'), options: () => {
|
||||
'export.pdfPageOrientation': { value: 'portrait', type: SettingItemType.String, storage: SettingStorage.File, isGlobal: true, advanced: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page orientation for PDF export'), options: () => {
|
||||
return {
|
||||
'portrait': _('Portrait'),
|
||||
'landscape': _('Landscape'),
|
||||
@@ -1261,6 +1318,7 @@ class Setting extends BaseModel {
|
||||
return output;
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
'editor.spellcheckBeta': {
|
||||
@@ -1270,6 +1328,8 @@ class Setting extends BaseModel {
|
||||
appTypes: [AppType.Desktop],
|
||||
label: () => 'Enable spell checking in Markdown editor? (WARNING BETA feature)',
|
||||
description: () => 'Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)',
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
'net.customCertificates': {
|
||||
@@ -1324,8 +1384,8 @@ class Setting extends BaseModel {
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'api.token': { value: null, type: SettingItemType.String, public: false, storage: SettingStorage.File },
|
||||
'api.port': { value: null, type: SettingItemType.Int, storage: SettingStorage.File, public: true, appTypes: [AppType.Cli], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
|
||||
'api.token': { value: null, type: SettingItemType.String, public: false, storage: SettingStorage.File, isGlobal: true },
|
||||
'api.port': { value: null, type: SettingItemType.Int, storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Cli], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
|
||||
|
||||
'resourceService.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false },
|
||||
'searchEngine.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false },
|
||||
@@ -1357,8 +1417,8 @@ class Setting extends BaseModel {
|
||||
'camera.type': { value: 0, type: SettingItemType.Int, public: false, appTypes: [AppType.Mobile] },
|
||||
'camera.ratio': { value: '4:3', type: SettingItemType.String, public: false, appTypes: [AppType.Mobile] },
|
||||
|
||||
'spellChecker.enabled': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, public: false },
|
||||
'spellChecker.language': { value: '', type: SettingItemType.String, storage: SettingStorage.File, public: false },
|
||||
'spellChecker.enabled': { value: true, type: SettingItemType.Bool, isGlobal: true, storage: SettingStorage.File, public: false },
|
||||
'spellChecker.language': { value: '', type: SettingItemType.String, isGlobal: true, storage: SettingStorage.File, public: false },
|
||||
|
||||
windowContentZoomFactor: {
|
||||
value: 100,
|
||||
@@ -1369,6 +1429,7 @@ class Setting extends BaseModel {
|
||||
maximum: 300,
|
||||
step: 10,
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
'layout.folderList.factor': {
|
||||
@@ -1384,6 +1445,7 @@ class Setting extends BaseModel {
|
||||
'Thus an 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.'),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
'layout.noteList.factor': {
|
||||
value: 1,
|
||||
@@ -1398,6 +1460,7 @@ class Setting extends BaseModel {
|
||||
'Thus an 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.'),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
'layout.note.factor': {
|
||||
value: 2,
|
||||
@@ -1412,6 +1475,7 @@ class Setting extends BaseModel {
|
||||
'Thus an 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.'),
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
'syncInfoCache': {
|
||||
@@ -1452,9 +1516,17 @@ class Setting extends BaseModel {
|
||||
|
||||
this.metadata_ = Object.assign(this.metadata_, this.customMetadata_);
|
||||
|
||||
if (this.value('env') === Env.Dev) this.validateMetadata(this.metadata_);
|
||||
|
||||
return this.metadata_;
|
||||
}
|
||||
|
||||
private static validateMetadata(md: SettingItems) {
|
||||
for (const [k, v] of Object.entries(md)) {
|
||||
if (v.isGlobal && v.storage !== SettingStorage.File) throw new Error(`Setting "${k}" is global but storage is not "file"`);
|
||||
}
|
||||
}
|
||||
|
||||
public static skipDefaultMigrations() {
|
||||
logger.info('Skipping all default migrations...');
|
||||
|
||||
@@ -1594,10 +1666,17 @@ class Setting extends BaseModel {
|
||||
// Low-level method to load a setting directly from the database. Should not be used in most cases.
|
||||
public static async loadOne(key: string): Promise<CacheItem | null> {
|
||||
if (this.keyStorage(key) === SettingStorage.File) {
|
||||
const fromFile = await this.fileHandler.load();
|
||||
let fileSettings = await this.fileHandler.load();
|
||||
|
||||
const md = this.settingMetadata(key);
|
||||
if (md.isGlobal) {
|
||||
const rootFileSettings = await this.rootFileHandler.load();
|
||||
fileSettings = mergeGlobalAndLocalSettings(rootFileSettings, fileSettings);
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
value: fromFile[key],
|
||||
value: fileSettings[key],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1664,11 +1743,17 @@ class Setting extends BaseModel {
|
||||
const itemsFromFile: CacheItem[] = [];
|
||||
|
||||
if (this.canUseFileStorage()) {
|
||||
const fromFile = await this.fileHandler.load();
|
||||
for (const k of Object.keys(fromFile)) {
|
||||
let fileSettings = await this.fileHandler.load();
|
||||
|
||||
if (this.value('isSubProfile')) {
|
||||
const rootFileSettings = await this.rootFileHandler.load();
|
||||
fileSettings = mergeGlobalAndLocalSettings(rootFileSettings, fileSettings);
|
||||
}
|
||||
|
||||
for (const k of Object.keys(fileSettings)) {
|
||||
itemsFromFile.push({
|
||||
key: k,
|
||||
value: fromFile[k],
|
||||
value: fileSettings[k],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2018,7 +2103,15 @@ class Setting extends BaseModel {
|
||||
|
||||
await BaseModel.db().transactionExecBatch(queries);
|
||||
|
||||
if (this.canUseFileStorage()) await this.fileHandler.save(valuesForFile);
|
||||
if (this.canUseFileStorage()) {
|
||||
if (this.value('isSubProfile')) {
|
||||
const { globalSettings, localSettings } = splitGlobalAndLocalSettings(valuesForFile);
|
||||
await this.rootFileHandler.save(globalSettings);
|
||||
await this.fileHandler.save(localSettings);
|
||||
} else {
|
||||
await this.fileHandler.save(valuesForFile);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Settings have been saved.');
|
||||
}
|
||||
|
||||