Compare commits
148 Commits
android-v1
...
android-v1
Author | SHA1 | Date | |
---|---|---|---|
|
715253da2f | ||
|
66356d83ab | ||
|
8e531ca87a | ||
|
18c46851fd | ||
|
5456dbbf16 | ||
|
5c54b83108 | ||
|
cbf7f03bff | ||
|
ea05fea234 | ||
|
f78729ad1f | ||
|
4ec9492f7c | ||
|
f86b953420 | ||
|
6563606799 | ||
|
c01bc1c363 | ||
|
6f8c634756 | ||
|
22a93994aa | ||
|
e0013858c4 | ||
|
b6e0df57eb | ||
|
be210233be | ||
|
1a1a1d3841 | ||
|
4283bbde7f | ||
|
fba325f60e | ||
|
fcd76dabac | ||
|
e9366a0d41 | ||
|
953aa5d0b5 | ||
|
3fed1abc36 | ||
|
6973bf9331 | ||
|
e8867fa0f1 | ||
|
d9c15b84d0 | ||
|
81876c7bf3 | ||
|
1dd7727e97 | ||
|
fe0318584e | ||
|
8508fe737b | ||
|
c7a9e5f656 | ||
|
3e43fbce13 | ||
|
b304e2ae1f | ||
|
35f4ede11a | ||
|
65cbb6e388 | ||
|
960d7f84eb | ||
|
8a392e1c06 | ||
|
d9d75d6c71 | ||
|
69f9e38730 | ||
|
7f95186a97 | ||
|
6f976abf42 | ||
|
d80ffeeba1 | ||
|
e078de25f0 | ||
|
cd284f78ad | ||
|
0a13c988fa | ||
|
b61bfd6ffe | ||
|
fc61b474cd | ||
|
bf25364333 | ||
|
bc7099d29b | ||
|
00c3ed715c | ||
|
701b57de89 | ||
|
e674d7d23b | ||
|
4a2d9bb028 | ||
|
ae3a278ac4 | ||
|
42ada7123c | ||
|
6d9f73eef7 | ||
|
541372eb91 | ||
|
8d7d70bc13 | ||
|
e77cc18468 | ||
|
193978a8be | ||
|
853ac0cca8 | ||
|
589f0803e6 | ||
|
fc67a44f95 | ||
|
204365b2ae | ||
|
2a63ecef2a | ||
|
1d660d7141 | ||
|
69000c0fc5 | ||
|
c8a0138b3b | ||
|
90de63e650 | ||
|
6b6e17cbad | ||
|
071bd2b0ca | ||
|
f74db06176 | ||
|
a6b3ddc7ed | ||
|
4ff889d4ec | ||
|
12b9f1b969 | ||
|
59bb1015ab | ||
|
f9c77171cf | ||
|
9628b64d3e | ||
|
d3f47a38b8 | ||
|
8111213691 | ||
|
a88ff902b4 | ||
|
1e57e1e486 | ||
|
172afb0789 | ||
|
5bfd1849c1 | ||
|
f61c9c93bb | ||
|
b0efdb6ee8 | ||
|
888a9ddaf4 | ||
|
d2482d6554 | ||
|
21cac248b3 | ||
|
ce7671151c | ||
|
b77525e570 | ||
|
e93cc50d1c | ||
|
c534305c7b | ||
|
797b71d903 | ||
|
74fd9e1e9e | ||
|
ff94a95589 | ||
|
0f5192bf19 | ||
|
eabbbba0c7 | ||
|
840cdf5512 | ||
|
757a6854ab | ||
|
b16dd051f1 | ||
|
68e73b658a | ||
|
af5f301276 | ||
|
2b9818a94d | ||
|
58200ecdb1 | ||
|
acaf22fa11 | ||
|
f60d6e0748 | ||
|
ae9163e9bb | ||
|
cad6b7971f | ||
|
ee38590c35 | ||
|
f10695fb8f | ||
|
b44ecc1958 | ||
|
931e7a7795 | ||
|
df85bb189d | ||
|
27e1f53b5f | ||
|
266ddedaef | ||
|
44dd327d22 | ||
|
13be56a2e3 | ||
|
5d0ba460ae | ||
|
6988b3accb | ||
|
3e2676a8c6 | ||
|
0f88c947f1 | ||
|
07b175c2ee | ||
|
6132cf2128 | ||
|
37dbb81425 | ||
|
c1028ec2cf | ||
|
f98dc4e576 | ||
|
da044960f9 | ||
|
03522b48a5 | ||
|
1b31525773 | ||
|
64a1408d6c | ||
|
3a5e68fca0 | ||
|
48ce788118 | ||
|
34f0a2951a | ||
|
66546418e3 | ||
|
611be7c0fa | ||
|
4f3e031f4f | ||
|
554c46182a | ||
|
b5d5d02a9c | ||
|
4640b65b85 | ||
|
1615c6bdc8 | ||
|
c003b8d32d | ||
|
3a1f924fb1 | ||
|
583460c0a8 | ||
|
1550a52002 | ||
|
9bd3bc8404 |
@@ -45,4 +45,10 @@ Server/docs/
|
||||
Server/dist/
|
||||
Server/bin/
|
||||
Server/node_modules/
|
||||
ElectronClient/app/packageInfo.js
|
||||
ElectronClient/app/packageInfo.js
|
||||
ReactNativeClient/pluginAssets/
|
||||
|
||||
# Ignore files generated from TypeScript files
|
||||
ElectronClient/app/gui/ShareNoteDialog.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
ReactNativeClient/PluginAssetsLoader.js
|
||||
|
6
.gitignore
vendored
@@ -44,3 +44,9 @@ ElectronClient/app/gui/note-viewer/fonts/
|
||||
ElectronClient/app/gui/note-viewer/lib.js
|
||||
Tools/commit_hook.txt
|
||||
.vscode/*
|
||||
*.map
|
||||
|
||||
# Ignore files generated from TypeScript files
|
||||
ElectronClient/app/gui/ShareNoteDialog.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
ReactNativeClient/PluginAssetsLoader.js
|
||||
|
19
.travis.yml
@@ -50,12 +50,17 @@ before_install:
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y yarn
|
||||
sudo apt-get install -y gettext
|
||||
fi
|
||||
|
||||
script:
|
||||
- |
|
||||
# Copy lib
|
||||
rsync -aP --delete ReactNativeClient/lib/ ElectronClient/app/lib/
|
||||
|
||||
# Install tools
|
||||
npm install
|
||||
npm run tsc
|
||||
cd Tools
|
||||
npm install
|
||||
cd ..
|
||||
@@ -84,6 +89,19 @@ script:
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate translations - this is needed as some users manually
|
||||
# edit .po files (and often make mistakes) instead of using a proper
|
||||
# tool like poedit. Doing it for Linux only is sufficient.
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
|
||||
if [ "$TRAVIS_OS_NAME" != "osx" ]; then
|
||||
node Tools/validate-translation.js
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Find out if we should run the build or not. Electron-builder gets stuck when
|
||||
# builing PRs so we disable it in this case. The Linux build should provide
|
||||
# enough info if the app builds or not.
|
||||
@@ -96,5 +114,4 @@ script:
|
||||
|
||||
# Prepare the Electron app and build it
|
||||
cd ElectronClient/app
|
||||
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
|
||||
npm install && USE_HARD_LINKS=false yarn dist
|
||||
|
BIN
Assets/GitHubSponsorIcon.png
Normal file
After Width: | Height: | Size: 19 KiB |
31
Assets/JoplinIconBlack.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg40"
|
||||
version="1.1"
|
||||
width="1536"
|
||||
height="1536"
|
||||
viewBox="0 0 1536 1536">
|
||||
<metadata
|
||||
id="metadata46">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs44" />
|
||||
<path
|
||||
id="path38"
|
||||
fill="#ffffff"
|
||||
d="M 373.834,0 C 168.227,0 0,168.223 0,373.834 V 1162.17 C 0,1367.778 168.227,1536 373.834,1536 H 1162.17 C 1367.778,1536 1536,1367.778 1536,1162.17 V 373.834 C 1536,168.224 1367.778,0 1162.17,0 Z m 397.222,205.431 h 417.424 a 7.132,7.132 0 0 1 7.132,7.133 v 132.552 c 0,4.461 -3.619,8.073 -8.077,8.073 h -57.23 c -24.168,0 -43.768,19.338 -44.284,43.374 v 2.377 h -0.017 v 136.191 h -0.053 l -0.466,509.375 c -5.02,77.667 -39.222,149.056 -96.324,201.046 -60.28,54.834 -141.948,85.017 -229.962,85.017 -12.45,0 -25.208,-0.61 -37.907,-1.785 -92.157,-8.682 -181.494,-48.601 -251.662,-112.438 -71.99,-65.517 -117.147,-150.03 -127.164,-238 -11.226,-98.763 23.42,-192.783 95.045,-257.937 81.99,-74.637 198.185,-101.768 316.613,-75.704 5.574,1.227 9.55,6.282 9.55,11.997 v 199.52 c -0.199,2.625 -1.481,6.599 -8.183,2.896 -0.663,-0.365 -1.194,-0.511 -1.653,-0.531 -21.987,-10.587 -45.159,-17.57 -68.559,-19.916 -0.38,-0.04 -0.757,-0.124 -1.138,-0.163 -0.537,-0.048 -1.034,-0.033 -1.556,-0.075 -4.13,-0.354 -8.183,-0.517 -12.203,-0.58 -0.87,-0.011 -1.771,-0.127 -2.641,-0.127 -0.486,0 -0.951,0.05 -1.437,0.057 -1.464,0.011 -2.886,0.115 -4.33,0.163 -2.76,0.102 -5.497,0.211 -8.182,0.448 -0.273,0.024 -0.547,0.07 -0.835,0.097 -25.509,2.4 -47.864,11.104 -65.012,25.47 -0.954,0.802 -1.974,1.53 -2.9,2.36 a 1.34,1.34 0 0 1 -0.168,0.146 c -23.96,21.8 -34.881,53.872 -30.726,90.316 4.62,40.737 26.94,81.156 62.841,113.823 35.908,32.67 80.335,52.977 125.113,57.186 35.118,3.36 66.547,-3.919 89.899,-20.461 a 97.255,97.255 0 0 0 9.365,-7.501 c 2.925,-2.661 5.569,-5.5 8.086,-8.416 0.3,-0.348 0.672,-0.673 0.975,-1.024 8.253,-9.864 14.222,-21.067 17.996,-33.148 0.639,-2.034 1.051,-4.148 1.564,-6.227 0.381,-1.563 0.81,-3.106 1.112,-4.693 0.555,-2.784 0.923,-5.632 1.253,-8.49 0.086,-0.709 0.183,-1.414 0.237,-2.128 0.492,-4.893 0.693,-9.858 0.55,-14.91 h 0.013 V 393.623 c -2.01,-22.626 -20.78,-40.434 -43.928,-40.434 h -57.23 a 8.071,8.071 0 0 1 -8.077,-8.073 V 212.564 a 7.132,7.132 0 0 1 7.136,-7.133 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
48
BUILD.md
@@ -5,6 +5,14 @@
|
||||
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when building each app.
|
||||
- In general, most of the backend (anything to do with the database, synchronisation, data import or export, etc.) is shared across all the apps, so when making a change please consider how it will affect all the apps.
|
||||
|
||||
# TypeScript
|
||||
|
||||
Most of the application is written in JavaScript, however new classes and files should generally be written in [TypeScript](https://www.typescriptlang.org/). Even if you don't write TypeScript code, you will need to build the existing .ts and .tsx files. This is done from the root of the project, by running `npm run tsc`.
|
||||
|
||||
If you are modifying TypeScript code, the best is to have the compiler watch for changes from a terminal. To do so, run `npm run tsc-watch`.
|
||||
|
||||
All TypeScript files are generated next to the .ts or .tsx file. So for example, if there's a file "lib/MyClass.ts", there will be a generated "lib/MyClass.js" next to it. If you create a new TypeScript file, make sure you add the generated .js file to .gitignore. It is implemented that way as it requires minimal changes to integrate TypeScript in the existing JavaScript code base.
|
||||
|
||||
## macOS dependencies
|
||||
|
||||
brew install yarn node
|
||||
@@ -14,7 +22,7 @@
|
||||
## Linux and Windows (WSL) dependencies
|
||||
|
||||
- Install yarn - https://yarnpkg.com/lang/en/docs/install/
|
||||
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- Install node v10.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
# Building the tools
|
||||
@@ -28,8 +36,9 @@ npm install && cd Tools && npm install
|
||||
# Building the Electron application
|
||||
|
||||
```
|
||||
npm run copyLib
|
||||
npm run tsc
|
||||
cd ElectronClient/app
|
||||
rsync --delete -a ../../ReactNativeClient/lib/ lib/
|
||||
npm install
|
||||
yarn dist
|
||||
```
|
||||
@@ -47,10 +56,9 @@ From `/ElectronClient` you can also run `run.sh` to run the app for testing.
|
||||
## Building Electron application on Windows
|
||||
|
||||
```
|
||||
cd Tools
|
||||
npm install
|
||||
cd ..\ElectronClient\app
|
||||
xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
|
||||
xcopy /C /I /H /R /Y /S ReactNativeClient\lib ElectronClient\app\lib
|
||||
npm run tsc
|
||||
cd ElectronClient\app
|
||||
npm install
|
||||
yarn dist
|
||||
```
|
||||
@@ -63,11 +71,36 @@ If you get an `error MSB8020: The build tools for v140 cannot be found.` try to
|
||||
|
||||
The [building\_win32\_tips on this page](./readme/building_win32_tips.md) might be helpful.
|
||||
|
||||
## Troubleshooting desktop application
|
||||
|
||||
> The application window doesn't open or is white
|
||||
|
||||
This is an indication that there's an early initialisation error. Try this:
|
||||
|
||||
- In ElectronAppWrapper, set `debugEarlyBugs` to `true`. This will force the window to show up and should open the console next to it, which should display any error.
|
||||
- In more rare cases, an already open instance of Joplin can create strange low-level bugs that will display no error but will result in this white window. A non-dev instance of Joplin, or a dev instance that wasn't properly closed might cause this. So make sure you close everything and try again. Perhaps even other Electron apps running (Skype, Slack, etc.) could cause this?
|
||||
- Also try to delete node_modules and rebuild.
|
||||
- If all else fail, switch your computer off and on again, to make sure you start clean.
|
||||
|
||||
> How to work on the app from Windows?
|
||||
|
||||
You should not use WSL at all because this is a GUI app that lives outside of WSL, and the WSL layer can cause all kind of very hard to debug issues. It can also lock files in node_modules that cannot be unlocked when the app crashes (you need to restart your computer). Likewise, don't run the TypeScript watch command from WSL.
|
||||
|
||||
So everything should be done from a Windows Command prompt running as Administrator. You can use `run.bat` to run the app in dev mode.
|
||||
|
||||
# Building the Mobile application
|
||||
|
||||
First you need to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "React Native CLI Quickstart" tab.
|
||||
|
||||
Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
|
||||
Then:
|
||||
|
||||
```
|
||||
npm run tsc
|
||||
cd ReactNativeClient
|
||||
npm install
|
||||
react-native run-ios
|
||||
# Or: react-native run-android
|
||||
```
|
||||
|
||||
# Building the Terminal application
|
||||
|
||||
@@ -75,7 +108,6 @@ Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios`
|
||||
cd CliClient
|
||||
npm install
|
||||
./build.sh
|
||||
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
|
||||
```
|
||||
|
||||
Run `run.sh` to start the application for testing.
|
||||
|
@@ -65,6 +65,12 @@ To run the test units, you must have an instance of the cli app running. In a fi
|
||||
> rsync -aP ./ReactNativeClient/locales/ ./CliClient/build/locales/
|
||||
> ```
|
||||
|
||||
> If you get an error like `Error: Cannot find module './path/to/node_sqlite3.node'`, that likely means you need to install sqlite:
|
||||
>
|
||||
> ```sh
|
||||
> npm install sqlite3 --build-from-source
|
||||
> ```
|
||||
|
||||
Then run the tests in a second window. To run all the test units:
|
||||
|
||||
./run_test.sh
|
||||
@@ -72,3 +78,17 @@ Then run the tests in a second window. To run all the test units:
|
||||
To run just one particular file:
|
||||
|
||||
./run_test.sh markdownUtils # Don't add the .js extension
|
||||
|
||||
To filter tests:
|
||||
|
||||
./run_test.sh "should handle conflict" # Will run all the test units that contain "should handle conflict" in their description
|
||||
|
||||
## 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.
|
||||
|
||||
In that case we will not merge the pull request, even if only small changes are missing. Our policy is simply to close the pull request. Why? Because an unfinished pull request essentially means giving us work and moving on. We would rather not encourage this behaviour.
|
||||
|
||||
Also, please note that since we have spent time reviewing the pull request and proposing solutions, we reserve the right to re-use that knowledge to create a new pull request, potentially based on your changes.
|
||||
|
||||
We’d much prefer that you complete the pull request though, so we’ll be sure to ping you a few times before that!
|
||||
|
@@ -131,6 +131,24 @@ class Command extends BaseCommand {
|
||||
lines.push('');
|
||||
lines.push('Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/#searching');
|
||||
lines.push('');
|
||||
lines.push('To retrieve non-notes items, such as notebooks or tags, add a `type` parameter and set it to the required [item type name](#item-type-id). In that case, full text search will not be used - instead it will be a simple case-insensitive search. You can also use `*` as a wildcard. This is convenient for example to retrieve notebooks or tags by title.');
|
||||
lines.push('');
|
||||
lines.push('For example, to retrieve the notebook named `recipes`: **GET /search?query=recipes&type=folder**');
|
||||
lines.push('');
|
||||
lines.push('To retrieve all the tags that start with `project-`: **GET /search?query=project-*&type=tag**');
|
||||
lines.push('');
|
||||
|
||||
lines.push('# Item type IDs');
|
||||
lines.push('');
|
||||
lines.push('Item type IDs might be refered to in certain object you will retrieve from the API. This is the correspondance between name and ID:');
|
||||
lines.push('');
|
||||
lines.push('Name | Value');
|
||||
lines.push('---- | -----');
|
||||
for (const t of BaseModel.typeEnum_) {
|
||||
const value = t[1];
|
||||
lines.push(`${BaseModel.modelTypeToName(value)} | ${value} `);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const model = models[i];
|
||||
|
@@ -1,6 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { app } = require('./app.js');
|
||||
const fs = require('fs-extra');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -13,11 +14,60 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
options() {
|
||||
return [['-v, --verbose', _('Also displays unset and hidden config variables.')]];
|
||||
return [
|
||||
['-v, --verbose', _('Also displays unset and hidden config variables.')],
|
||||
['--export', 'Writes all settings to STDOUT as JSON including secure variables.'],
|
||||
['--import', 'Reads in JSON formatted settings from STDIN.'],
|
||||
['--import-file <file>', 'Reads in settings from <file>. <file> must contain valid JSON.'],
|
||||
];
|
||||
}
|
||||
async __importSettings(inputStream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// being defensive and not attempting to settle twice
|
||||
let isSettled = false;
|
||||
const chunks = [];
|
||||
|
||||
inputStream.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = inputStream.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
});
|
||||
|
||||
inputStream.on('end', () => {
|
||||
let json = chunks.join('');
|
||||
let settingsObj;
|
||||
try {
|
||||
settingsObj = JSON.parse(json);
|
||||
} catch (err) {
|
||||
isSettled = true;
|
||||
return reject(new Error(`Invalid JSON passed to config --import: \n${err.message}.`));
|
||||
}
|
||||
if (settingsObj) {
|
||||
Object.entries(settingsObj)
|
||||
.forEach(([key, value]) => {
|
||||
Setting.setValue(key, value);
|
||||
});
|
||||
}
|
||||
if (!isSettled) {
|
||||
isSettled = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
inputStream.on('error', (error) => {
|
||||
if (!isSettled) {
|
||||
isSettled = true;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
async action(args) {
|
||||
const verbose = args.options.verbose;
|
||||
const isExport = args.options.export;
|
||||
const isImport = args.options.import || args.options.importFile;
|
||||
const importFile = args.options.importFile;
|
||||
|
||||
const renderKeyValue = name => {
|
||||
const md = Setting.settingMetadata(name);
|
||||
@@ -32,35 +82,45 @@ class Command extends BaseCommand {
|
||||
}
|
||||
};
|
||||
|
||||
if (!args.name && !args.value) {
|
||||
if (isExport || (!isImport && !args.value)) {
|
||||
let keys = Setting.keys(!verbose, 'cli');
|
||||
keys.sort();
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const value = Setting.value(keys[i]);
|
||||
if (!verbose && !value) continue;
|
||||
this.stdout(renderKeyValue(keys[i]));
|
||||
|
||||
if (isExport) {
|
||||
const resultObj = keys.reduce((acc, key) => {
|
||||
const value = Setting.value(key);
|
||||
if (!verbose && !value) return acc;
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
// Printing the object in "pretty" format so it's easy to read/edit
|
||||
this.stdout(JSON.stringify(resultObj, null, 2));
|
||||
} else if (!args.name) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const value = Setting.value(keys[i]);
|
||||
if (!verbose && !value) continue;
|
||||
this.stdout(renderKeyValue(keys[i]));
|
||||
}
|
||||
} else {
|
||||
this.stdout(renderKeyValue(args.name));
|
||||
}
|
||||
app()
|
||||
.gui()
|
||||
.showConsole();
|
||||
app()
|
||||
.gui()
|
||||
.maximizeConsole();
|
||||
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.name && !args.value) {
|
||||
this.stdout(renderKeyValue(args.name));
|
||||
app()
|
||||
.gui()
|
||||
.showConsole();
|
||||
app()
|
||||
.gui()
|
||||
.maximizeConsole();
|
||||
return;
|
||||
if (isImport) {
|
||||
let fileStream = process.stdin;
|
||||
if (importFile) {
|
||||
fileStream = fs.createReadStream(importFile, { autoClose: true });
|
||||
}
|
||||
await this.__importSettings(fileStream);
|
||||
} else {
|
||||
Setting.setValue(args.name, args.value);
|
||||
}
|
||||
|
||||
Setting.setValue(args.name, args.value);
|
||||
|
||||
if (args.name == 'locale') {
|
||||
setLocale(Setting.value('locale'));
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
|
||||
const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
|
@@ -8,8 +8,8 @@ require('app-module-path').addPath(__dirname);
|
||||
|
||||
const compareVersion = require('compare-version');
|
||||
const nodeVersion = process && process.versions && process.versions.node ? process.versions.node : '0.0.0';
|
||||
if (compareVersion(nodeVersion, '8.0.0') < 0) {
|
||||
console.error(`Joplin requires Node 8+. Detected version ${nodeVersion}`);
|
||||
if (compareVersion(nodeVersion, '10.0.0') < 0) {
|
||||
console.error(`Joplin requires Node 10+. Detected version ${nodeVersion}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@@ -7,4 +7,10 @@ rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
|
||||
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/locales/" "$BUILD_DIR/locales/"
|
||||
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
|
||||
|
||||
# Don't add TypeScript here or make it silent as output of Cli app must be clean
|
||||
# cd $ROOT_DIR/..
|
||||
# npm run tsc
|
||||
# cd $ROOT_DIR
|
||||
|
||||
chmod 755 "$BUILD_DIR/main.js"
|
3152
CliClient/locales/pt_PT.po
Normal file
350
CliClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "1.0.149",
|
||||
"version": "1.0.150",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -806,6 +806,11 @@
|
||||
"debug": "^2.6.9"
|
||||
}
|
||||
},
|
||||
"font-awesome-filetypes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/font-awesome-filetypes/-/font-awesome-filetypes-2.1.0.tgz",
|
||||
"integrity": "sha512-U6hi14GRjfZFIWsTNyVmCBuHyPhiizWEKVbaQqHipKQv3rA1l1PNvmKulzpqxonFnQMToty5ZhfWbc/0IjLDGA=="
|
||||
},
|
||||
"for-each-property": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/for-each-property/-/for-each-property-0.0.4.tgz",
|
||||
@@ -1131,9 +1136,9 @@
|
||||
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
|
||||
},
|
||||
"ignore-walk": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
|
||||
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
|
||||
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
@@ -1533,6 +1538,73 @@
|
||||
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
|
||||
"dev": true
|
||||
},
|
||||
"joplin-renderer": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/joplin-renderer/-/joplin-renderer-1.0.8.tgz",
|
||||
"integrity": "sha512-Q4SSYXl9ErcnUDTz7N4FjWcOOgfPEf5yyNRjU2J1fuxZ/1VbYt6MnfBB7OeiRW+XF+4Arhihk+/XVK++of4hEA==",
|
||||
"requires": {
|
||||
"base-64": "^0.1.0",
|
||||
"font-awesome-filetypes": "^2.1.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"highlight.js": "^9.17.1",
|
||||
"html-entities": "^1.2.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"katex": "^0.11.1",
|
||||
"markdown-it": "^10.0.0",
|
||||
"markdown-it-abbr": "^1.0.4",
|
||||
"markdown-it-anchor": "^5.2.5",
|
||||
"markdown-it-deflist": "^2.0.3",
|
||||
"markdown-it-emoji": "^1.4.0",
|
||||
"markdown-it-footnote": "^3.0.2",
|
||||
"markdown-it-ins": "^3.0.0",
|
||||
"markdown-it-mark": "^3.0.0",
|
||||
"markdown-it-multimd-table": "^4.0.1",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"markdown-it-toc-done-right": "^4.1.0",
|
||||
"md5": "^2.2.1",
|
||||
"uslug": "^1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"entities": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
|
||||
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
|
||||
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.0.tgz",
|
||||
"integrity": "sha512-A97kI1KAUzKoAiEoaGcf2O9YPS8nbDTCRFokaaeBhnqjQTvbAuAJrQMm21zw8s8xzaMtCQBtgbyGXLGxdxQyqQ=="
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
|
||||
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~2.0.0",
|
||||
"linkify-it": "^2.0.0",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"joplin-turndown": {
|
||||
"version": "4.0.19",
|
||||
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.19.tgz",
|
||||
@@ -1544,9 +1616,9 @@
|
||||
}
|
||||
},
|
||||
"joplin-turndown-plugin-gfm": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.11.tgz",
|
||||
"integrity": "sha512-S2I+VCTqIhpWKKkPHsyJ5rdll9H/JjMXoBVClRX1TnphcmrSxufevdoXWWVgLncdXpSSiuoifCXgFZy3ueVElg=="
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.12.tgz",
|
||||
"integrity": "sha512-qL4+1iycQjZ1fs8zk3jSRk7cg3ROBUHk7GKtiLAQLFzLPKErnILUvz5DLszSQvz3s1sTjPbywLDISVUtBY6HaA=="
|
||||
},
|
||||
"jpeg-js": {
|
||||
"version": "0.1.2",
|
||||
@@ -1636,6 +1708,21 @@
|
||||
"resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz",
|
||||
"integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po="
|
||||
},
|
||||
"katex": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.11.1.tgz",
|
||||
"integrity": "sha512-5oANDICCTX0NqYIyAiFCCwjQ7ERu3DQG2JFHLbYOf+fXaMoH8eg/zOq5WSYJsKMi/QebW+Eh3gSM+oss1H/bww==",
|
||||
"requires": {
|
||||
"commander": "^2.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"klaw": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
|
||||
@@ -1755,6 +1842,64 @@
|
||||
"uc.micro": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"markdown-it-abbr": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz",
|
||||
"integrity": "sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g="
|
||||
},
|
||||
"markdown-it-anchor": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.5.tgz",
|
||||
"integrity": "sha512-xLIjLQmtym3QpoY9llBgApknl7pxAcN3WDRc2d3rwpl+/YvDZHPmKscGs+L6E05xf2KrCXPBvosWt7MZukwSpQ=="
|
||||
},
|
||||
"markdown-it-deflist": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.0.3.tgz",
|
||||
"integrity": "sha512-/BNZ8ksW42bflm1qQLnRI09oqU2847Z7MVavrR0MORyKLtiUYOMpwtlAfMSZAQU9UCvaUZMpgVAqoS3vpToJxw=="
|
||||
},
|
||||
"markdown-it-emoji": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz",
|
||||
"integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw="
|
||||
},
|
||||
"markdown-it-footnote": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.2.tgz",
|
||||
"integrity": "sha512-JVW6fCmZWjvMdDQSbOT3nnOQtd9iAXmw7hTSh26+v42BnvXeVyGMDBm5b/EZocMed2MbCAHiTX632vY0FyGB8A=="
|
||||
},
|
||||
"markdown-it-ins": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-3.0.0.tgz",
|
||||
"integrity": "sha512-+vyAdBuMGwmT2yMlAFJSx2VR/0QZ1onQ/Mkkmr4l9tDFOh5sVoAgRbkgbuSsk+sxJ9vaMH/IQ323ydfvQrPO/Q=="
|
||||
},
|
||||
"markdown-it-mark": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.0.tgz",
|
||||
"integrity": "sha512-HqMWeKfMMOu4zBO0emmxsoMWmbf2cPKZY1wP6FsTbKmicFfp5y4L3KXAsNeO1rM6NTJVOrNlLKMPjWzriBGspw=="
|
||||
},
|
||||
"markdown-it-multimd-table": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-multimd-table/-/markdown-it-multimd-table-4.0.1.tgz",
|
||||
"integrity": "sha512-ZgRV8LlGz6JXTZ5zd82yCL8IVG5MRastMWxxrc6hQC8aC8kq/7zpp+ksBqVqcdTmTdabnkuSo/7h3SyKM31YCA==",
|
||||
"requires": {
|
||||
"markdown-it": "^8.4.2"
|
||||
}
|
||||
},
|
||||
"markdown-it-sub": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz",
|
||||
"integrity": "sha1-N1/WAm6ufdywEkl/ZBEZXqHjr+g="
|
||||
},
|
||||
"markdown-it-sup": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz",
|
||||
"integrity": "sha1-y5yf+RpSVawI8/09YyhuFd8KH8M="
|
||||
},
|
||||
"markdown-it-toc-done-right": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-toc-done-right/-/markdown-it-toc-done-right-4.1.0.tgz",
|
||||
"integrity": "sha512-UhD2Oj6cZV3ycYPoelt4hTkwKIK3zbPP1wjjdpCq7UGtWQOFalDFDv1s2zBYV6aR2gMs/X8kpJcOYsQmUbiXDw=="
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
|
||||
@@ -1859,9 +2004,9 @@
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
|
||||
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw=="
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
|
||||
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
|
||||
},
|
||||
"napi-build-utils": {
|
||||
"version": "1.0.1",
|
||||
@@ -1887,27 +2032,27 @@
|
||||
}
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.3.1.tgz",
|
||||
"integrity": "sha512-CaLXV3W8Vnbps8ZANqDGz7j4x7Yj1LW4TWF/TQuDfj7Cfx4nAPTvw98qgTevtto1oHDrh3pQkaODbqupXlsWTg==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz",
|
||||
"integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==",
|
||||
"requires": {
|
||||
"debug": "^4.1.0",
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1996,14 +2141,22 @@
|
||||
}
|
||||
},
|
||||
"npm-bundled": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
|
||||
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g=="
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
|
||||
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
|
||||
"requires": {
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npm-normalize-package-bin": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
|
||||
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
|
||||
},
|
||||
"npm-packlist": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz",
|
||||
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.7.tgz",
|
||||
"integrity": "sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ==",
|
||||
"requires": {
|
||||
"ignore-walk": "^3.0.1",
|
||||
"npm-bundled": "^1.0.1"
|
||||
@@ -2226,7 +2379,7 @@
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||
}
|
||||
}
|
||||
@@ -2676,137 +2829,13 @@
|
||||
"integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw="
|
||||
},
|
||||
"sqlite3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.7.tgz",
|
||||
"integrity": "sha512-TGEeSBB8O48bEu8KUUMqzeB22WrfTxzhIf0lFm8wLTo3a6yJBonF2sPKMYrYtOne1F1t9AHAEn+DTISq8WebQg==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.1.tgz",
|
||||
"integrity": "sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==",
|
||||
"requires": {
|
||||
"nan": "^2.12.1",
|
||||
"node-pre-gyp": "^0.11.0",
|
||||
"request": "^2.87.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
|
||||
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
|
||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
|
||||
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
|
||||
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
|
||||
"requires": {
|
||||
"ajv": "^6.5.5",
|
||||
"har-schema": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
|
||||
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.24",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
|
||||
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
|
||||
"requires": {
|
||||
"mime-db": "1.40.0"
|
||||
}
|
||||
},
|
||||
"oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
|
||||
},
|
||||
"request": {
|
||||
"version": "2.88.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
|
||||
"requires": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
"caseless": "~0.12.0",
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.2",
|
||||
"har-validator": "~5.1.0",
|
||||
"http-signature": "~1.2.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"oauth-sign": "~0.9.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.4.3",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
|
||||
"requires": {
|
||||
"psl": "^1.1.24",
|
||||
"punycode": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"sshpk": {
|
||||
@@ -3205,6 +3234,11 @@
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
|
||||
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
|
||||
},
|
||||
"unorm": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
|
||||
"integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA=="
|
||||
},
|
||||
"unpack-string": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unpack-string/-/unpack-string-0.0.2.tgz",
|
||||
@@ -3244,6 +3278,14 @@
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"uslug": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uslug/-/uslug-1.0.4.tgz",
|
||||
"integrity": "sha1-uaIvCRTgqGFAYz2swwLl9PpFBnc=",
|
||||
"requires": {
|
||||
"unorm": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@@ -25,7 +25,7 @@
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.7.0"
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"app-module-path": "^2.2.0",
|
||||
@@ -44,8 +44,9 @@
|
||||
"html-minifier": "^3.5.15",
|
||||
"image-data-uri": "^2.0.0",
|
||||
"image-type": "^3.0.0",
|
||||
"joplin-renderer": "^1.0.8",
|
||||
"joplin-turndown": "^4.0.19",
|
||||
"joplin-turndown-plugin-gfm": "^1.0.11",
|
||||
"joplin-turndown-plugin-gfm": "^1.0.12",
|
||||
"jssha": "^2.3.0",
|
||||
"levenshtein": "^1.0.5",
|
||||
"markdown-it": "^8.4.2",
|
||||
@@ -66,7 +67,7 @@
|
||||
"server-destroy": "^1.0.1",
|
||||
"sharp": "^0.23.2",
|
||||
"sprintf-js": "^1.1.1",
|
||||
"sqlite3": "^4.0.7",
|
||||
"sqlite3": "^4.1.1",
|
||||
"string-padding": "^1.0.2",
|
||||
"string-to-stream": "^1.1.0",
|
||||
"strip-ansi": "^4.0.0",
|
||||
|
41
CliClient/tests/MdToMd.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const mdImporterService = require('lib/services/InteropService_Importer_Md');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { setupDatabaseAndSynchronizer, switchClient } = require('test-utils.js');
|
||||
|
||||
const importer = new mdImporterService();
|
||||
|
||||
|
||||
describe('InteropService_Importer_Md: importLocalImages', function() {
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
done();
|
||||
});
|
||||
it('should import linked files and modify tags appropriately', async function() {
|
||||
const tagNonExistentFile = '';
|
||||
const note = await importer.importFile(`${__dirname}/md_to_md/sample.md`, 'notebook');
|
||||
let items = await Note.linkedItems(note.body);
|
||||
expect(items.length).toBe(2);
|
||||
const inexistentLinkUnchanged = note.body.includes(tagNonExistentFile);
|
||||
expect(inexistentLinkUnchanged).toBe(true);
|
||||
});
|
||||
it('should only create 1 resource for duplicate links, all tags should be updated', async function() {
|
||||
const note = await importer.importFile(`${__dirname}/md_to_md/sample-duplicate-links.md`, 'notebook');
|
||||
let items = await Note.linkedItems(note.body);
|
||||
expect(items.length).toBe(1);
|
||||
const reg = new RegExp(items[0].id, 'g');
|
||||
const matched = note.body.match(reg);
|
||||
expect(matched.length).toBe(2);
|
||||
});
|
||||
it('should import linked files and modify tags appropriately when link is also in alt text', async function() {
|
||||
const note = await importer.importFile(`${__dirname}/md_to_md/sample-link-in-alt-text.md`, 'notebook');
|
||||
let items = await Note.linkedItems(note.body);
|
||||
expect(items.length).toBe(1);
|
||||
});
|
||||
it('should passthrough unchanged if no links present', async function() {
|
||||
const note = await importer.importFile(`${__dirname}/md_to_md/sample-no-links.md`, 'notebook');
|
||||
let items = await Note.linkedItems(note.body);
|
||||
expect(items.length).toBe(0);
|
||||
expect(note.body).toContain('Unidentified vessel travelling at sub warp speed, bearing 235.7. Fluctuations in energy readings from it, Captain. All transporters off.');
|
||||
});
|
||||
});
|
@@ -41,7 +41,7 @@ describe('Encryption', function() {
|
||||
};
|
||||
|
||||
const encodedHeader = service.encodeHeader_(header);
|
||||
const decodedHeader = service.decodeHeader_(encodedHeader);
|
||||
const decodedHeader = service.decodeHeaderBytes_(encodedHeader);
|
||||
delete decodedHeader.length;
|
||||
|
||||
expect(objectsEqual(header, decodedHeader)).toBe(true);
|
||||
@@ -54,14 +54,14 @@ describe('Encryption', function() {
|
||||
|
||||
let hasThrown = false;
|
||||
try {
|
||||
await service.decryptMasterKey(masterKey, 'wrongpassword');
|
||||
await service.decryptMasterKey_(masterKey, 'wrongpassword');
|
||||
} catch (error) {
|
||||
hasThrown = true;
|
||||
}
|
||||
|
||||
expect(hasThrown).toBe(true);
|
||||
|
||||
const decryptedMasterKey = await service.decryptMasterKey(masterKey, '123456');
|
||||
const decryptedMasterKey = await service.decryptMasterKey_(masterKey, '123456');
|
||||
expect(decryptedMasterKey.length).toBe(512);
|
||||
}));
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('Encryption', function() {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
const cipherText = await service.encryptString('some secret');
|
||||
const plainText = await service.decryptString(cipherText);
|
||||
@@ -87,11 +87,37 @@ describe('Encryption', function() {
|
||||
expect(plainText2 === veryLongSecret).toBe(true);
|
||||
}));
|
||||
|
||||
it('should decrypt various encryption methods', asyncTest(async () => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
{
|
||||
const cipherText = await service.encryptString('some secret', {
|
||||
encryptionMethod: EncryptionService.METHOD_SJCL_2,
|
||||
});
|
||||
const plainText = await service.decryptString(cipherText);
|
||||
expect(plainText).toBe('some secret');
|
||||
const header = await service.decodeHeaderString(cipherText);
|
||||
expect(header.encryptionMethod).toBe(EncryptionService.METHOD_SJCL_2);
|
||||
}
|
||||
|
||||
{
|
||||
const cipherText = await service.encryptString('some secret', {
|
||||
encryptionMethod: EncryptionService.METHOD_SJCL_3,
|
||||
});
|
||||
const plainText = await service.decryptString(cipherText);
|
||||
expect(plainText).toBe('some secret');
|
||||
const header = await service.decodeHeaderString(cipherText);
|
||||
expect(header.encryptionMethod).toBe(EncryptionService.METHOD_SJCL_3);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should fail to decrypt if master key not present', asyncTest(async () => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
const cipherText = await service.encryptString('some secret');
|
||||
|
||||
@@ -107,7 +133,7 @@ describe('Encryption', function() {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
let cipherText = await service.encryptString('some secret');
|
||||
cipherText += 'ABCDEFGHIJ';
|
||||
@@ -120,7 +146,7 @@ describe('Encryption', function() {
|
||||
it('should encrypt and decrypt notes and folders', asyncTest(async () => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
let folder = await Folder.save({ title: 'folder' });
|
||||
let note = await Note.save({ title: 'encrypted note', body: 'something', parent_id: folder.id });
|
||||
@@ -151,7 +177,7 @@ describe('Encryption', function() {
|
||||
it('should encrypt and decrypt files', asyncTest(async () => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
const sourcePath = `${__dirname}/../tests/support/photo.jpg`;
|
||||
const encryptedPath = `${__dirname}/data/photo.crypted`;
|
||||
@@ -164,4 +190,34 @@ describe('Encryption', function() {
|
||||
expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true);
|
||||
}));
|
||||
|
||||
// it('should upgrade master key encryption mode', asyncTest(async () => {
|
||||
// let masterKey = await service.generateMasterKey('123456', {
|
||||
// encryptionMethod: EncryptionService.METHOD_SJCL_2,
|
||||
// });
|
||||
// masterKey = await MasterKey.save(masterKey);
|
||||
// Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
||||
// Setting.setValue('encryption.activeMasterKeyId', masterKey.id);
|
||||
|
||||
// await sleep(0.01);
|
||||
|
||||
// await service.loadMasterKeysFromSettings();
|
||||
|
||||
// masterKeyNew = await MasterKey.load(masterKey.id);
|
||||
|
||||
// // Check that the master key has been upgraded
|
||||
|
||||
// expect(masterKeyNew.created_time).toBe(masterKey.created_time);
|
||||
// expect(masterKeyNew.updated_time === masterKey.updated_time).toBe(false);
|
||||
// expect(masterKeyNew.content === masterKey.content).toBe(false);
|
||||
// expect(masterKeyNew.encryption_method === masterKey.encryption_method).toBe(false);
|
||||
// expect(masterKeyNew.checksum === masterKey.checksum).toBe(false);
|
||||
// expect(masterKeyNew.encryption_method).toBe(service.defaultMasterKeyEncryptionMethod_);
|
||||
|
||||
// // Check that encryption still works
|
||||
|
||||
// const cipherText = await service.encryptString('some secret');
|
||||
// const plainText = await service.decryptString(cipherText);
|
||||
// expect(plainText).toBe('some secret');
|
||||
// }));
|
||||
|
||||
});
|
||||
|
2
CliClient/tests/md_to_md/sample-duplicate-links.md
Normal file
@@ -0,0 +1,2 @@
|
||||

|
||||

|
3
CliClient/tests/md_to_md/sample-link-in-alt-text.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Markdown
|
||||
 should put resource link inside () not []
|
||||
 this case (spaces before/after link but within parens) is not currently covered
|
3
CliClient/tests/md_to_md/sample-no-links.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Markdown
|
||||
|
||||
Unidentified vessel travelling at sub warp speed, bearing 235.7. Fluctuations in energy readings from it, Captain. All transporters off.
|
13
CliClient/tests/md_to_md/sample.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Markdown
|
||||
|
||||
lorem ipsum 
|
||||
- [ ] check!
|
||||
- [ ] boxes!
|
||||
|
||||
ipsum lorem
|
||||
|
||||
**strong text**
|
||||
 lorem ipsum
|
||||
|
||||
**some directory**
|
||||
 lorem ipsum
|
@@ -150,4 +150,31 @@ describe('models_Folder', function() {
|
||||
expect(foldersById[f4.id].note_count).toBe(0);
|
||||
}));
|
||||
|
||||
it('should not count completed to-dos', asyncTest(async () => {
|
||||
|
||||
let f1 = await Folder.save({ title: 'folder1' });
|
||||
let f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
|
||||
let f3 = await Folder.save({ title: 'folder3', parent_id: f2.id });
|
||||
let f4 = await Folder.save({ title: 'folder4' });
|
||||
|
||||
let n1 = await Note.save({ title: 'note1', parent_id: f3.id });
|
||||
let n2 = await Note.save({ title: 'note2', parent_id: f3.id });
|
||||
let n3 = await Note.save({ title: 'note3', parent_id: f1.id });
|
||||
let n4 = await Note.save({ title: 'note4', parent_id: f3.id, is_todo: true, todo_completed: 0 });
|
||||
let n5 = await Note.save({ title: 'note5', parent_id: f3.id, is_todo: true, todo_completed: 999 });
|
||||
let n6 = await Note.save({ title: 'note6', parent_id: f3.id, is_todo: true, todo_completed: 999 });
|
||||
|
||||
const folders = await Folder.all();
|
||||
await Folder.addNoteCounts(folders, false);
|
||||
|
||||
const foldersById = {};
|
||||
folders.forEach((f) => { foldersById[f.id] = f; });
|
||||
|
||||
expect(folders.length).toBe(4);
|
||||
expect(foldersById[f1.id].note_count).toBe(4);
|
||||
expect(foldersById[f2.id].note_count).toBe(3);
|
||||
expect(foldersById[f3.id].note_count).toBe(3);
|
||||
expect(foldersById[f4.id].note_count).toBe(0);
|
||||
}));
|
||||
|
||||
});
|
||||
|
350
CliClient/tests/reducer.js
Normal file
@@ -0,0 +1,350 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
require('app-module-path').addPath(__dirname);
|
||||
const {setupDatabaseAndSynchronizer, switchClient, asyncTest } = require('test-utils.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const { reducer, defaultState, stateUtils} = require('lib/reducer.js');
|
||||
|
||||
async function createNTestFolders(n) {
|
||||
let folders = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let folder = await Folder.save({ title: 'folder' });
|
||||
folders.push(folder);
|
||||
}
|
||||
return folders;
|
||||
}
|
||||
|
||||
async function createNTestNotes(n, folder) {
|
||||
let notes = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let note = await Note.save({ title: 'note', parent_id: folder.id });
|
||||
notes.push(note);
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
async function createNTestTags(n) {
|
||||
let tags = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let tag = await Tag.save({ title: 'tag' });
|
||||
tags.push(tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
function initTestState(folders, selectedFolderIndex, notes, selectedIndexes, tags=null, selectedTagIndex=null) {
|
||||
let state = defaultState;
|
||||
if (folders != null) {
|
||||
state = reducer(state, { type: 'FOLDER_UPDATE_ALL', items: folders });
|
||||
}
|
||||
if (selectedFolderIndex != null) {
|
||||
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[selectedFolderIndex].id });
|
||||
}
|
||||
if (notes != null) {
|
||||
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, noteSource: 'test' });
|
||||
}
|
||||
if (selectedIndexes != null) {
|
||||
let selectedIds = [];
|
||||
for (let i = 0; i < selectedIndexes.length; i++) {
|
||||
selectedIds.push(notes[selectedIndexes[i]].id);
|
||||
}
|
||||
state = reducer(state, { type: 'NOTE_SELECT', ids: selectedIds });
|
||||
}
|
||||
if (tags != null) {
|
||||
state = reducer(state, { type: 'TAG_UPDATE_ALL', items: tags });
|
||||
}
|
||||
if (selectedTagIndex != null) {
|
||||
state = reducer(state, { type: 'TAG_SELECT', id: tags[selectedTagIndex].id });
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function createExpectedState(items, keepIndexes, selectedIndexes) {
|
||||
let expected = { items: [], selectedIds: []};
|
||||
|
||||
for (let i = 0; i < selectedIndexes.length; i++) {
|
||||
expected.selectedIds.push(items[selectedIndexes[i]].id);
|
||||
}
|
||||
for (let i = 0; i < keepIndexes.length; i++) {
|
||||
expected.items.push(items[keepIndexes[i]]);
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
function getIds(items, indexes=null) {
|
||||
let ids = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (indexes == null || i in indexes) {
|
||||
ids.push(items[i].id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
let insideBeforeEach = false;
|
||||
|
||||
describe('Reducer', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
insideBeforeEach = true;
|
||||
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
|
||||
done();
|
||||
|
||||
insideBeforeEach = false;
|
||||
});
|
||||
|
||||
// tests for NOTE_DELETE
|
||||
it('should delete selected note', asyncTest(async () => {
|
||||
// create 1 folder
|
||||
let folders = await createNTestFolders(1);
|
||||
// create 5 notes
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
// select the 1st folder and the 3rd note
|
||||
let state = initTestState(folders, 0, notes, [2]);
|
||||
|
||||
// test action
|
||||
// delete the third note
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id});
|
||||
|
||||
// expect that the third note is missing, and the 4th note is now selected
|
||||
let expected = createExpectedState(notes, [0,1,3,4], [3]);
|
||||
|
||||
// check the ids of all the remaining notes
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
// check the ids of the selected notes
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete selected note at top', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id});
|
||||
|
||||
let expected = createExpectedState(notes, [1,2,3,4], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete last remaining note', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(1, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [0]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id});
|
||||
|
||||
let expected = createExpectedState(notes, [], []);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete selected note at bottom', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2,3], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a note below is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,2,3,4], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a note above is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2,4], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete selected notes', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1,2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id});
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,3,4], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a notes below it are selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3,4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,2,3,4], [3,4]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a notes above it are selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1,2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2,4], [1,2]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete notes at end', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3,4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id});
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id});
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2], [2]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete notes when non-contiguous selection', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [0,2,4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id});
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id});
|
||||
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id});
|
||||
|
||||
let expected = createExpectedState(notes, [1,3], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
// tests for FOLDER_DELETE
|
||||
it('should delete selected notebook', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(5);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 2, notes, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id});
|
||||
|
||||
let expected = createExpectedState(folders, [0,1,3,4], [3]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete notebook when a book above is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(5);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 1, notes, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id});
|
||||
|
||||
let expected = createExpectedState(folders, [0,1,3,4], [1]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete notebook when a book below is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(5);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 4, notes, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id});
|
||||
|
||||
let expected = createExpectedState(folders, [0,1,3,4], [4]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
// tests for TAG_DELETE
|
||||
it('should delete selected tag', asyncTest(async () => {
|
||||
let tags = await createNTestTags(5);
|
||||
let state = initTestState(null, null, null, null, tags, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'TAG_DELETE', id: tags[2].id});
|
||||
|
||||
let expected = createExpectedState(tags, [0,1,3,4], [3]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete tag when a tag above is selected', asyncTest(async () => {
|
||||
let tags = await createNTestTags(5);
|
||||
let state = initTestState(null, null, null, null, tags, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'TAG_DELETE', id: tags[4].id});
|
||||
|
||||
let expected = createExpectedState(tags, [0,1,2,3], [2]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete tag when a tag below is selected', asyncTest(async () => {
|
||||
let tags = await createNTestTags(5);
|
||||
let state = initTestState(null, null, null, null, tags, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, {type: 'TAG_DELETE', id: tags[0].id});
|
||||
|
||||
let expected = createExpectedState(tags, [1,2,3,4], [2]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
});
|
336
CliClient/tests/services_InteropService_Exporter_Md.js
Normal file
@@ -0,0 +1,336 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('test-utils.js');
|
||||
const InteropService_Exporter_Md = require('lib/services/InteropService_Exporter_Md.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
|
||||
|
||||
const exportDir = `${__dirname}/export`;
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
describe('services_InteropService_Exporter_Md', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
|
||||
await fs.remove(exportDir);
|
||||
await fs.mkdirp(exportDir);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create resources directory', asyncTest(async () => {
|
||||
const service = new InteropService_Exporter_Md();
|
||||
await service.init(exportDir);
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/_resources/`)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should create note paths and add them to context', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note1 = await Note.load(note1.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2' });
|
||||
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id });
|
||||
await shim.attachFileToNote(note3, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note3 = await Note.load(note3.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note3);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note3.body))[0]);
|
||||
|
||||
expect(!exporter.context() && !(exporter.context().notePaths || Object.keys(exporter.context().notePaths).length)).toBe(false, 'Context should be empty before processing.');
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(3, 'There should be 3 note paths in the context.');
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
|
||||
expect(exporter.context().notePaths[note2.id]).toBe('folder1/note2.md');
|
||||
expect(exporter.context().notePaths[note3.id]).toBe('folder2/note3.md');
|
||||
}));
|
||||
|
||||
it('should handle duplicate note names', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
let note1_2 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1_2);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(2, 'There should be 2 note paths in the context.');
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
|
||||
expect(exporter.context().notePaths[note1_2.id]).toBe('folder1/note1 (1).md');
|
||||
}));
|
||||
|
||||
it('should not override existing files', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
// Create a file with the path of note1 before processing note1
|
||||
await shim.fsDriver().writeFile(`${exportDir}/folder1/note1.md`, 'Note content', 'utf-8');
|
||||
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(1, 'There should be 1 note paths in the context.');
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1 (1).md');
|
||||
}));
|
||||
|
||||
it('should save resource files in _resource directory', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note1 = await Note.load(note1.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
|
||||
let resource1 = await Resource.load(itemsToExport[2].itemOrId);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note2 = await Note.load(note2.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note2.body))[0]);
|
||||
let resource2 = await Resource.load(itemsToExport[5].itemOrId);
|
||||
|
||||
await exporter.processResource(resource1, Resource.fullPath(resource1));
|
||||
await exporter.processResource(resource2, Resource.fullPath(resource2));
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/_resources/${Resource.filename(resource1)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/_resources/${Resource.filename(resource2)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
|
||||
}));
|
||||
|
||||
it('should save notes in fs', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
|
||||
let folder3 = await Folder.save({ title: 'folder3' });
|
||||
let note3 = await Note.save({ title: 'note3', parent_id: folder3.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder3.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note3);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.processItem(Folder, folder3);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
await exporter.processItem(Note, note3);
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note1.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note2.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note3.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
}));
|
||||
|
||||
it('should replace resource ids with relative paths', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note1 = await Note.load(note1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
let resource1 = await Resource.load((await Note.linkedResourceIds(note1.body))[0]);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note2 = await Note.load(note2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
let resource2 = await Resource.load((await Note.linkedResourceIds(note2.body))[0]);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
let context = {
|
||||
resourcePaths: {},
|
||||
};
|
||||
context.resourcePaths[resource1.id] = 'resource1.jpg';
|
||||
context.resourcePaths[resource2.id] = 'resource2.jpg';
|
||||
exporter.updateContext(context);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
|
||||
let note1_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note1.id]}`);
|
||||
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
|
||||
|
||||
expect(note1_body).toContain('](../_resources/resource1.jpg)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../_resources/resource2.jpg)', 'Resource id should be replaced with a relative path.');
|
||||
}));
|
||||
|
||||
it('should replace note ids with relative paths', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
const changeNoteBodyAndReload = async (note, newBody) => {
|
||||
note.body = newBody;
|
||||
await Note.save(note);
|
||||
return await Note.load(note.id);
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
|
||||
let folder3 = await Folder.save({ title: 'folder3' });
|
||||
let note3 = await Note.save({ title: 'note3', parent_id: folder3.id });
|
||||
|
||||
note1 = await changeNoteBodyAndReload(note1, `# Some text \n\n [A link to note3](:/${note3.id})`);
|
||||
note2 = await changeNoteBodyAndReload(note2, `# Some text \n\n [A link to note3](:/${note3.id}) some more text \n ## And some headers \n and [A link to note1](:/${note1.id}) more links`);
|
||||
note3 = await changeNoteBodyAndReload(note3, `[A link to note3](:/${note2.id})`);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note3);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.processItem(Folder, folder3);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
await exporter.processItem(Note, note3);
|
||||
|
||||
let note1_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note1.id]}`);
|
||||
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
|
||||
let note3_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note3.id]}`);
|
||||
|
||||
expect(note1_body).toContain('](../folder3/note3.md)', 'Note id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../folder3/note3.md)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../folder1/note1.md)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note3_body).toContain('](../folder1/folder2/note2.md)', 'Resource id should be replaced with a relative path.');
|
||||
}));
|
||||
|
||||
it('should url encode relative note links', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder with space1' });
|
||||
let note1 = await Note.save({ title: 'note1 name with space', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder1.id, body: `[link](:/${note1.id})` });
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
|
||||
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
|
||||
expect(note2_body).toContain('[link](../folder%20with%20space1/note1%20name%20with%20space.md)', 'Whitespace in URL should be encoded');
|
||||
}));
|
||||
});
|
BIN
CliClient/tests/support/photo-two.jpg
Normal file
After Width: | Height: | Size: 2.7 KiB |
@@ -539,7 +539,7 @@ describe('Synchronizer', function() {
|
||||
let context2 = await synchronizer().start();
|
||||
if (withEncryption) {
|
||||
const masterKey_2 = await MasterKey.load(masterKey.id);
|
||||
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
||||
await encryptionService().loadMasterKey_(masterKey_2, '123456', true);
|
||||
let t = await Tag.load(tag.id);
|
||||
await Tag.decrypt(t);
|
||||
}
|
||||
@@ -743,7 +743,7 @@ describe('Synchronizer', function() {
|
||||
expect(masterKey_2.content).toBe(masterKey.content);
|
||||
expect(masterKey_2.checksum).toBe(masterKey.checksum);
|
||||
// Now load the master key we got from client 1 and try to decrypt
|
||||
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
||||
await encryptionService().loadMasterKey_(masterKey_2, '123456', true);
|
||||
// Get the decrypted items back
|
||||
await Folder.decrypt(folder1_2);
|
||||
await Note.decrypt(note1_2);
|
||||
@@ -1540,5 +1540,43 @@ describe('Synchronizer', function() {
|
||||
expect((await synchronizer().lockFiles_()).length).toBe(0);
|
||||
}));
|
||||
|
||||
it('should not encrypt notes that are shared', asyncTest(async () => {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
await loadEncryptionMasterKey();
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'un', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'deux', parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
const origNote2 = Object.assign({}, note2);
|
||||
await BaseItem.updateShareStatus(note2, true);
|
||||
note2 = await Note.load(note2.id);
|
||||
|
||||
// Sharing a note should not modify the timestamps
|
||||
expect(note2.user_updated_time).toBe(origNote2.user_updated_time);
|
||||
expect(note2.user_created_time).toBe(origNote2.user_created_time);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
// The shared note should be decrypted
|
||||
let note2_2 = await Note.load(note2.id);
|
||||
expect(note2_2.title).toBe('deux');
|
||||
expect(note2_2.is_shared).toBe(1);
|
||||
|
||||
// The non-shared note should be encrypted
|
||||
let note1_2 = await Note.load(note1.id);
|
||||
expect(note1_2.title).toBe('');
|
||||
}));
|
||||
|
||||
});
|
||||
|
@@ -299,7 +299,7 @@ async function loadEncryptionMasterKey(id = null, useExisting = false) {
|
||||
masterKey = masterKeys[0];
|
||||
}
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
return masterKey;
|
||||
}
|
||||
@@ -370,8 +370,12 @@ function asyncTest(callback) {
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
expect('good').toBe('not good', 'Test has thrown an exception - see above error');
|
||||
if (error.constructor && error.constructor.name === 'ExpectationFailed') {
|
||||
// OK - will be reported by Jasmine
|
||||
} else {
|
||||
console.error(error);
|
||||
expect(0).toBe(1, 'Test has thrown an exception - see above error');
|
||||
}
|
||||
} finally {
|
||||
done();
|
||||
}
|
||||
|
Before Width: | Height: | Size: 585 B After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -33,7 +33,14 @@ class ElectronAppWrapper {
|
||||
return this.win_;
|
||||
}
|
||||
|
||||
env() {
|
||||
return this.env_;
|
||||
}
|
||||
|
||||
createWindow() {
|
||||
// Set to true to view errors if the application does not start
|
||||
const debugEarlyBugs = this.env_ === 'dev' && false;
|
||||
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
|
||||
const stateOptions = {
|
||||
@@ -56,9 +63,10 @@ class ElectronAppWrapper {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
},
|
||||
webviewTag: true,
|
||||
// We start with a hidden window, which is then made visible depending on the showTrayIcon setting
|
||||
// https://github.com/laurent22/joplin/issues/2031
|
||||
show: false,
|
||||
show: debugEarlyBugs,
|
||||
};
|
||||
|
||||
// Linux icon workaround for bug https://github.com/electron-userland/electron-builder/issues/2098
|
||||
@@ -81,8 +89,11 @@ class ElectronAppWrapper {
|
||||
slashes: true,
|
||||
}));
|
||||
|
||||
// Uncomment this to view errors if the application does not start
|
||||
// if (this.env_ === 'dev') this.win_.webContents.openDevTools();
|
||||
// Note that on Windows, calling openDevTools() too early results in a white window with no error message.
|
||||
// Waiting for one of the ready events might work but they might not be triggered if there's an error, so
|
||||
// the easiest is to use a timeout. Keep in mind that if you get a white window on Windows it might be due
|
||||
// to this line though.
|
||||
if (debugEarlyBugs) setTimeout(() => this.win_.webContents.openDevTools(), 3000);
|
||||
|
||||
this.win_.on('close', (event) => {
|
||||
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
||||
|
@@ -1,9 +1,97 @@
|
||||
const { _ } = require('lib/locale');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const InteropService = require('lib/services/InteropService');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const md5 = require('md5');
|
||||
const url = require('url');
|
||||
const { shim } = require('lib/shim');
|
||||
|
||||
class InteropServiceHelper {
|
||||
|
||||
static async exportNoteToHtmlFile(noteId, exportOptions) {
|
||||
const tempFile = `${Setting.value('tempDir')}/${md5(Date.now() + Math.random())}.html`;
|
||||
|
||||
exportOptions = Object.assign({}, {
|
||||
path: tempFile,
|
||||
format: 'html',
|
||||
target: 'file',
|
||||
sourceNoteIds: [noteId],
|
||||
customCss: '',
|
||||
}, exportOptions);
|
||||
|
||||
const service = new InteropService();
|
||||
|
||||
const result = await service.export(exportOptions);
|
||||
console.info('Export HTML result: ', result);
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
static async exportNoteTo_(target, noteId, options = {}) {
|
||||
let win = null;
|
||||
let htmlFile = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (win) win.destroy();
|
||||
if (htmlFile) shim.fsDriver().remove(htmlFile);
|
||||
};
|
||||
|
||||
try {
|
||||
const exportOptions = {
|
||||
customCss: options.customCss ? options.customCss : '',
|
||||
};
|
||||
|
||||
htmlFile = await this.exportNoteToHtmlFile(noteId, exportOptions);
|
||||
|
||||
const windowOptions = {
|
||||
show: false,
|
||||
};
|
||||
|
||||
win = bridge().newBrowserWindow(windowOptions);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
win.webContents.on('did-finish-load', async () => {
|
||||
|
||||
if (target === 'pdf') {
|
||||
try {
|
||||
const data = await win.webContents.printToPDF(options);
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
} else {
|
||||
win.webContents.print(options, (success) => {
|
||||
// TODO: This is correct but broken in Electron 4. Need to upgrade to 5+
|
||||
// It calls the callback right away with "false" even if the document hasn't be print yet.
|
||||
|
||||
cleanup();
|
||||
if (!success) reject(new Error('Could not print'));
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
win.loadURL(url.format({
|
||||
pathname: htmlFile,
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
}));
|
||||
});
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async exportNoteToPdf(noteId, options = {}) {
|
||||
return this.exportNoteTo_('pdf', noteId, options);
|
||||
}
|
||||
|
||||
static async printNote(noteId, options = {}) {
|
||||
return this.exportNoteTo_('printer', noteId, options);
|
||||
}
|
||||
|
||||
static async export(dispatch, module, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
@@ -32,6 +120,8 @@ class InteropServiceHelper {
|
||||
const exportOptions = {};
|
||||
exportOptions.path = path;
|
||||
exportOptions.format = module.format;
|
||||
exportOptions.modulePath = module.path;
|
||||
exportOptions.target = module.target;
|
||||
if (options.sourceFolderIds) exportOptions.sourceFolderIds = options.sourceFolderIds;
|
||||
if (options.sourceNoteIds) exportOptions.sourceNoteIds = options.sourceNoteIds;
|
||||
|
||||
@@ -41,6 +131,7 @@ class InteropServiceHelper {
|
||||
const result = await service.export(exportOptions);
|
||||
console.info('Export result: ', result);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
bridge().showErrorMessageBox(_('Could not export notes: %s', error.message));
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ const Setting = require('lib/models/Setting.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const Note = require('lib/models/Note');
|
||||
const { MarkupToHtml } = require('joplin-renderer');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const fs = require('fs-extra');
|
||||
@@ -28,6 +29,7 @@ const PluginManager = require('lib/services/PluginManager');
|
||||
const RevisionService = require('lib/services/RevisionService');
|
||||
const MigrationService = require('lib/services/MigrationService');
|
||||
const TemplateUtils = require('lib/TemplateUtils');
|
||||
const CssUtils = require('lib/CssUtils');
|
||||
|
||||
const pluginClasses = [
|
||||
require('./plugins/GotoAnything.min'),
|
||||
@@ -48,7 +50,7 @@ const appDefaultState = Object.assign({}, defaultState, {
|
||||
windowContentSize: bridge().windowContentSize(),
|
||||
watchedNoteFiles: [],
|
||||
lastEditorScrollPercents: {},
|
||||
noteDevToolsVisible: false,
|
||||
devToolsVisible: false,
|
||||
});
|
||||
|
||||
class Application extends BaseApplication {
|
||||
@@ -220,7 +222,12 @@ class Application extends BaseApplication {
|
||||
|
||||
case 'NOTE_DEVTOOLS_TOGGLE':
|
||||
newState = Object.assign({}, state);
|
||||
newState.noteDevToolsVisible = !newState.noteDevToolsVisible;
|
||||
newState.devToolsVisible = !newState.devToolsVisible;
|
||||
break;
|
||||
|
||||
case 'NOTE_DEVTOOLS_SET':
|
||||
newState = Object.assign({}, state);
|
||||
newState.devToolsVisible = action.value;
|
||||
break;
|
||||
|
||||
}
|
||||
@@ -232,6 +239,14 @@ class Application extends BaseApplication {
|
||||
return super.reducer(newState, action);
|
||||
}
|
||||
|
||||
toggleDevTools(visible) {
|
||||
if (visible) {
|
||||
bridge().openDevTools();
|
||||
} else {
|
||||
bridge().closeDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
async generalMiddleware(store, next, action) {
|
||||
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
|
||||
setLocale(Setting.value('locale'));
|
||||
@@ -273,12 +288,12 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
if (action.type.indexOf('NOTE_SELECT') === 0 || action.type.indexOf('FOLDER_SELECT') === 0) {
|
||||
this.updateMenuItemStates();
|
||||
this.updateMenuItemStates(newState);
|
||||
}
|
||||
|
||||
if (action.type === 'NOTE_DEVTOOLS_TOGGLE') {
|
||||
const menuItem = Menu.getApplicationMenu().getMenuItemById('help:toggleDevTools');
|
||||
menuItem.checked = newState.noteDevToolsVisible;
|
||||
if (['NOTE_DEVTOOLS_TOGGLE', 'NOTE_DEVTOOLS_SET'].indexOf(action.type) >= 0) {
|
||||
this.toggleDevTools(newState.devToolsVisible);
|
||||
this.updateMenuItemStates(newState);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -371,13 +386,15 @@ class Application extends BaseApplication {
|
||||
for (let i = 0; i < ioModules.length; i++) {
|
||||
const module = ioModules[i];
|
||||
if (module.type === 'exporter') {
|
||||
exportItems.push({
|
||||
label: module.fullLabel(),
|
||||
screens: ['Main'],
|
||||
click: async () => {
|
||||
await InteropServiceHelper.export(this.dispatch.bind(this), module);
|
||||
},
|
||||
});
|
||||
if (module.canDoMultiExport !== false) {
|
||||
exportItems.push({
|
||||
label: module.fullLabel(),
|
||||
screens: ['Main'],
|
||||
click: async () => {
|
||||
await InteropServiceHelper.export(this.dispatch.bind(this), module);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (let j = 0; j < module.sources.length; j++) {
|
||||
const moduleSource = module.sources[j];
|
||||
@@ -445,6 +462,7 @@ class Application extends BaseApplication {
|
||||
this.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'exportPdf',
|
||||
noteId: null,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1104,18 +1122,23 @@ class Application extends BaseApplication {
|
||||
this.lastMenuScreen_ = screen;
|
||||
}
|
||||
|
||||
async updateMenuItemStates() {
|
||||
async updateMenuItemStates(state = null) {
|
||||
if (!this.lastMenuScreen_) return;
|
||||
if (!this.store()) return;
|
||||
if (!this.store() && !state) return;
|
||||
|
||||
const selectedNoteIds = this.store().getState().selectedNoteIds;
|
||||
if (!state) state = this.store().getState();
|
||||
|
||||
const selectedNoteIds = state.selectedNoteIds;
|
||||
const note = selectedNoteIds.length === 1 ? await Note.load(selectedNoteIds[0]) : null;
|
||||
|
||||
for (const itemId of ['copy', 'paste', 'cut', 'selectAll', 'bold', 'italic', 'link', 'code', 'insertDateTime', 'commandStartExternalEditing', 'setTags', 'showLocalSearch']) {
|
||||
const menuItem = Menu.getApplicationMenu().getMenuItemById(`edit:${itemId}`);
|
||||
if (!menuItem) continue;
|
||||
menuItem.enabled = !!note && note.markup_language === Note.MARKUP_LANGUAGE_MARKDOWN;
|
||||
menuItem.enabled = !!note && note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
|
||||
}
|
||||
|
||||
const menuItem = Menu.getApplicationMenu().getMenuItemById('help:toggleDevTools');
|
||||
menuItem.checked = state.devToolsVisible;
|
||||
}
|
||||
|
||||
updateTray() {
|
||||
@@ -1167,6 +1190,39 @@ class Application extends BaseApplication {
|
||||
return cssString;
|
||||
}
|
||||
|
||||
// async createManyNotes() {
|
||||
// return;
|
||||
// const folderIds = [];
|
||||
|
||||
// const randomFolderId = (folderIds) => {
|
||||
// if (!folderIds.length) return '';
|
||||
// const idx = Math.floor(Math.random() * folderIds.length);
|
||||
// if (idx > folderIds.length - 1) throw new Error('Invalid index ' + idx + ' / ' + folderIds.length);
|
||||
// return folderIds[idx];
|
||||
// }
|
||||
|
||||
// let rootFolderCount = 0;
|
||||
// let folderCount = 100;
|
||||
|
||||
// for (let i = 0; i < folderCount; i++) {
|
||||
// let parentId = '';
|
||||
|
||||
// if (Math.random() >= 0.9 || rootFolderCount >= folderCount / 10) {
|
||||
// parentId = randomFolderId(folderIds);
|
||||
// } else {
|
||||
// rootFolderCount++;
|
||||
// }
|
||||
|
||||
// const folder = await Folder.save({ title: 'folder' + i, parent_id: parentId });
|
||||
// folderIds.push(folder.id);
|
||||
// }
|
||||
|
||||
// for (let i = 0; i < 10000; i++) {
|
||||
// const parentId = randomFolderId(folderIds);
|
||||
// Note.save({ title: 'note' + i, parent_id: parentId });
|
||||
// }
|
||||
// }
|
||||
|
||||
async start(argv) {
|
||||
const electronIsDev = require('electron-is-dev');
|
||||
|
||||
@@ -1176,13 +1232,18 @@ class Application extends BaseApplication {
|
||||
|
||||
argv = await super.start(argv);
|
||||
|
||||
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
|
||||
const dir = Setting.value('profileDir');
|
||||
const filename = Setting.custom_css_files.JOPLIN_APP;
|
||||
await CssUtils.injectCustomStyles(`${dir}/${filename}`);
|
||||
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
AlarmService.setLogger(reg.logger());
|
||||
|
||||
reg.setShowErrorMessageBoxHandler((message) => { bridge().showErrorMessageBox(message); });
|
||||
|
||||
if (Setting.value('openDevTools')) {
|
||||
bridge().window().webContents.openDevTools();
|
||||
if (Setting.value('flagOpenDevTools')) {
|
||||
bridge().openDevTools();
|
||||
}
|
||||
|
||||
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
|
||||
@@ -1224,8 +1285,8 @@ class Application extends BaseApplication {
|
||||
ids: Setting.value('collapsedFolderIds'),
|
||||
});
|
||||
|
||||
const cssString = await this.loadCustomCss(`${Setting.value('profileDir')}/userstyle.css`);
|
||||
|
||||
// Loads custom Markdown preview styles
|
||||
const cssString = await CssUtils.loadCustomCss(`${Setting.value('profileDir')}/userstyle.css`);
|
||||
this.store().dispatch({
|
||||
type: 'LOAD_CUSTOM_CSS',
|
||||
css: cssString,
|
||||
@@ -1238,6 +1299,11 @@ class Application extends BaseApplication {
|
||||
templates: templates,
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_DEVTOOLS_SET',
|
||||
value: Setting.value('flagOpenDevTools'),
|
||||
});
|
||||
|
||||
// Note: Auto-update currently doesn't work in Linux: it downloads the update
|
||||
// but then doesn't install it on exit.
|
||||
if (shim.isWindows() || shim.isMac()) {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { dirname } = require('lib/path-utils.js');
|
||||
const { BrowserWindow } = require('electron');
|
||||
|
||||
class Bridge {
|
||||
|
||||
@@ -13,6 +14,10 @@ class Bridge {
|
||||
return this.electronWrapper_;
|
||||
}
|
||||
|
||||
env() {
|
||||
return this.electronWrapper_.env();
|
||||
}
|
||||
|
||||
processArgv() {
|
||||
return process.argv;
|
||||
}
|
||||
@@ -21,6 +26,10 @@ class Bridge {
|
||||
return this.electronWrapper_.window();
|
||||
}
|
||||
|
||||
newBrowserWindow(options) {
|
||||
return new BrowserWindow(options);
|
||||
}
|
||||
|
||||
windowContentSize() {
|
||||
if (!this.window()) return { width: 0, height: 0 };
|
||||
const s = this.window().getContentSize();
|
||||
@@ -38,11 +47,19 @@ class Bridge {
|
||||
return this.window().setSize(width, height);
|
||||
}
|
||||
|
||||
openDevTools() {
|
||||
return this.window().webContents.openDevTools();
|
||||
}
|
||||
|
||||
closeDevTools() {
|
||||
return this.window().webContents.closeDevTools();
|
||||
}
|
||||
|
||||
showSaveDialog(options) {
|
||||
const {dialog} = require('electron');
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
|
||||
const filePath = dialog.showSaveDialog(this.window(), options);
|
||||
const filePath = dialog.showSaveDialogSync(this.window(), options);
|
||||
if (filePath) {
|
||||
this.lastSelectedPath_ = filePath;
|
||||
}
|
||||
@@ -54,7 +71,7 @@ class Bridge {
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
|
||||
if (!('createDirectory' in options)) options.createDirectory = true;
|
||||
const filePaths = dialog.showOpenDialog(this.window(), options);
|
||||
const filePaths = dialog.showOpenDialogSync(this.window(), options);
|
||||
if (filePaths && filePaths.length) {
|
||||
this.lastSelectedPath_ = dirname(filePaths[0]);
|
||||
}
|
||||
@@ -65,7 +82,7 @@ class Bridge {
|
||||
showMessageBox_(window, options) {
|
||||
const {dialog} = require('electron');
|
||||
if (!window) window = this.window();
|
||||
return dialog.showMessageBox(window, options);
|
||||
return dialog.showMessageBoxSync(window, options);
|
||||
}
|
||||
|
||||
showErrorMessageBox(message) {
|
||||
@@ -126,6 +143,10 @@ class Bridge {
|
||||
return this.electronApp().buildDir();
|
||||
}
|
||||
|
||||
screen() {
|
||||
return require('electron').screen;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let bridge_ = null;
|
||||
|
@@ -126,13 +126,13 @@ function checkForUpdates(inBackground, window, logFilePath, options) {
|
||||
|
||||
autoUpdateLogger_.info(`checkForUpdates: Checking with options ${JSON.stringify(options)}`);
|
||||
|
||||
fetchLatestRelease(options).then(release => {
|
||||
fetchLatestRelease(options).then(async (release) => {
|
||||
autoUpdateLogger_.info(`Current version: ${packageInfo.version}`);
|
||||
autoUpdateLogger_.info(`Latest version: ${release.version}`);
|
||||
autoUpdateLogger_.info('Is Pre-release:', release.prerelease);
|
||||
|
||||
if (compareVersions(release.version, packageInfo.version) <= 0) {
|
||||
if (!checkInBackground_) dialog.showMessageBox({
|
||||
if (!checkInBackground_) await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
message: _('Current version is up-to-date.'),
|
||||
buttons: [_('OK')],
|
||||
@@ -145,12 +145,13 @@ function checkForUpdates(inBackground, window, logFilePath, options) {
|
||||
|
||||
const newVersionString = release.prerelease ? _('%s (pre-release)', release.version) : release.version;
|
||||
|
||||
const buttonIndex = dialog.showMessageBox(parentWindow_, {
|
||||
const result = await dialog.showMessageBox(parentWindow_, {
|
||||
type: 'info',
|
||||
message: `${_('An update is available, do you want to download it now?')}\n\n${_('Your version: %s', packageInfo.version)}\n${_('New version: %s', newVersionString)}${releaseNotes}`,
|
||||
buttons: [_('Yes'), _('No')].concat(truncateReleaseNotes ? [_('Full Release Notes')] : []),
|
||||
});
|
||||
|
||||
const buttonIndex = result.response;
|
||||
if (buttonIndex === 0) require('electron').shell.openExternal(release.downloadUrl ? release.downloadUrl : release.pageUrl);
|
||||
if (buttonIndex === 2) require('electron').shell.openExternal(release.pageUrl);
|
||||
}
|
||||
|
@@ -24,6 +24,19 @@ class ConfigScreenComponent extends React.Component {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
};
|
||||
|
||||
this.checkNextcloudAppButton_click = async () => {
|
||||
this.setState({ showNextcloudAppLog: true });
|
||||
await shared.checkNextcloudApp(this, this.state.settings);
|
||||
};
|
||||
|
||||
this.showLogButton_click = () => {
|
||||
this.setState({ showNextcloudAppLog: true });
|
||||
};
|
||||
|
||||
this.nextcloudAppHelpLink_click = () => {
|
||||
bridge().openExternal('https://joplinapp.org/nextcloud_app');
|
||||
};
|
||||
|
||||
this.rowStyle_ = {
|
||||
marginBottom: 10,
|
||||
};
|
||||
@@ -31,7 +44,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
this.configMenuBar_selectionChange = this.configMenuBar_selectionChange.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
@@ -93,14 +106,21 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
sectionToComponent(key, section, settings, selected) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const settingComps = [];
|
||||
// const settingComps = [];
|
||||
|
||||
for (let i = 0; i < section.metadatas.length; i++) {
|
||||
const md = section.metadatas[i];
|
||||
const createSettingComponents = (advanced) => {
|
||||
const output = [];
|
||||
for (let i = 0; i < section.metadatas.length; i++) {
|
||||
const md = section.metadatas[i];
|
||||
if (!!md.advanced !== advanced) continue;
|
||||
const settingComp = this.settingToComponent(md.key, settings[md.key]);
|
||||
output.push(settingComp);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const settingComp = this.settingToComponent(md.key, settings[md.key]);
|
||||
settingComps.push(settingComp);
|
||||
}
|
||||
const settingComps = createSettingComponents(false);
|
||||
const advancedSettingComps = createSettingComponents(true);
|
||||
|
||||
const sectionStyle = {
|
||||
marginTop: 20,
|
||||
@@ -117,10 +137,10 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
if (section.name === 'sync') {
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
|
||||
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
|
||||
|
||||
if (syncTargetMd.supportsConfigCheck) {
|
||||
const messages = shared.checkSyncConfigMessages(this);
|
||||
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
|
||||
const statusComp = !messages.length ? null : (
|
||||
<div style={statusStyle}>
|
||||
{messages[0]}
|
||||
@@ -137,12 +157,69 @@ class ConfigScreenComponent extends React.Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (syncTargetMd.name === 'nextcloud') {
|
||||
const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']];
|
||||
|
||||
let status = _('Unknown');
|
||||
let errorMessage = null;
|
||||
|
||||
if (this.state.checkNextcloudAppResult === 'checking') {
|
||||
status = _('Checking...');
|
||||
} else if (syncTarget) {
|
||||
if (syncTarget.uuid) status = _('OK');
|
||||
if (syncTarget.error) {
|
||||
status = _('Error');
|
||||
errorMessage = syncTarget.error;
|
||||
}
|
||||
}
|
||||
|
||||
const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : (
|
||||
<div style={statusStyle}>
|
||||
<p style={theme.textStyle}>{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}</p>
|
||||
<pre>{errorMessage}</pre>
|
||||
</div>
|
||||
);
|
||||
|
||||
const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : (
|
||||
<a style={theme.urlStyle} href="#" onClick={this.showLogButton_click}>[{_('Show Log')}]</a>
|
||||
);
|
||||
|
||||
const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' });
|
||||
|
||||
settingComps.push(
|
||||
<div key="nextcloud_app_check" style={this.rowStyle_}>
|
||||
<span style={theme.textStyle}>Beta: {_('Joplin Nextcloud App status:')} </span><span style={appStatusStyle}>{status}</span>
|
||||
|
||||
{showLogButton}
|
||||
|
||||
<button disabled={this.state.checkNextcloudAppResult === 'checking'} style={theme.buttonStyle} onClick={this.checkNextcloudAppButton_click}>
|
||||
{_('Check Status')}
|
||||
</button>
|
||||
|
||||
<a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a>
|
||||
{statusComp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let advancedSettingsButton = null;
|
||||
let advancedSettingsSectionStyle = { display: 'none' };
|
||||
|
||||
if (advancedSettingComps.length) {
|
||||
const iconName = this.state.showAdvancedSettings ? 'fa fa-toggle-up' : 'fa fa-toggle-down';
|
||||
const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 });
|
||||
advancedSettingsButton = <button onClick={() => shared.advancedSettingsButton_click(this)} style={advancedSettingsButtonStyle}><i style={{fontSize: 14}} className={iconName}></i> {_('Show Advanced Settings')}</button>;
|
||||
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key} style={sectionStyle}>
|
||||
{noteComp}
|
||||
<div>{settingComps}</div>
|
||||
{advancedSettingsButton}
|
||||
<div style={advancedSettingsSectionStyle}>{advancedSettingComps}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -413,6 +490,24 @@ class ConfigScreenComponent extends React.Component {
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BUTTON) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const buttonStyle = Object.assign({}, theme.buttonStyle, {
|
||||
display: 'inline-block',
|
||||
marginRight: 10,
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}>
|
||||
<label>{md.label()}</label>
|
||||
</div>
|
||||
<button style={buttonStyle} onClick={md.onClick}>
|
||||
{_('Edit')}
|
||||
</button>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
console.warn(`Type not implemented: ${key}`);
|
||||
}
|
||||
|
45
ElectronClient/app/gui/DialogButtonRow.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
const React = require('react');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
|
||||
function DialogButtonRow(props) {
|
||||
const theme = themeStyle(props.theme);
|
||||
|
||||
const okButton_click = () => {
|
||||
if (props.onClick) props.onClick({ buttonName: 'ok' });
|
||||
};
|
||||
|
||||
const cancelButton_click = () => {
|
||||
if (props.onClick) props.onClick({ buttonName: 'cancel' });
|
||||
};
|
||||
|
||||
const onKeyDown = (event) => {
|
||||
if (event.keyCode === 13) {
|
||||
okButton_click();
|
||||
} else if (event.keyCode === 27) {
|
||||
cancelButton_click();
|
||||
}
|
||||
};
|
||||
|
||||
const buttonComps = [];
|
||||
|
||||
if (props.okButtonShow !== false) {
|
||||
buttonComps.push(
|
||||
<button key="ok" style={theme.buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
||||
{_('OK')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.cancelButtonShow !== false) {
|
||||
buttonComps.push(
|
||||
<button key="cancel" style={Object.assign({}, theme.buttonStyle, { marginLeft: 10 })} onClick={cancelButton_click}>
|
||||
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>;
|
||||
}
|
||||
|
||||
module.exports = DialogButtonRow;
|
@@ -13,7 +13,7 @@ class DropboxLoginScreenComponent extends React.Component {
|
||||
this.shared_ = new Shared(this, msg => bridge().showInfoMessageBox(msg), msg => bridge().showErrorMessageBox(msg));
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.shared_.refreshUrl();
|
||||
}
|
||||
|
||||
|
@@ -31,11 +31,11 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
return shared.refreshStats(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.initState(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.initState(nextProps);
|
||||
}
|
||||
|
||||
|
@@ -71,7 +71,7 @@ class HeaderComponent extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
async UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.windowCommand) {
|
||||
this.doCommand(nextProps.windowCommand);
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ const { filename, basename } = require('lib/path-utils.js');
|
||||
const { importEnex } = require('lib/import-enex');
|
||||
|
||||
class ImportScreenComponent extends React.Component {
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
doImport: true,
|
||||
filePath: this.props.filePath,
|
||||
@@ -16,7 +16,7 @@ class ImportScreenComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.filePath) {
|
||||
this.setState(
|
||||
{
|
||||
|
@@ -23,7 +23,7 @@ class ItemList extends React.Component {
|
||||
const topItemIndex = Math.floor(this.scrollTop_ / props.itemHeight);
|
||||
const visibleItemCount = this.visibleItemCount(props);
|
||||
|
||||
let bottomItemIndex = topItemIndex + visibleItemCount;
|
||||
let bottomItemIndex = topItemIndex + (visibleItemCount - 1);
|
||||
if (bottomItemIndex >= props.items.length) bottomItemIndex = props.items.length - 1;
|
||||
|
||||
this.setState({
|
||||
@@ -32,11 +32,11 @@ class ItemList extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
this.updateStateItemIndexes(newProps);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class ItemList extends React.Component {
|
||||
}
|
||||
|
||||
makeItemIndexVisible(itemIndex) {
|
||||
const top = Math.min(this.props.items.length - 1, this.state.topItemIndex + 1);
|
||||
const top = Math.min(this.props.items.length - 1, this.state.topItemIndex);
|
||||
const bottom = Math.max(0, this.state.bottomItemIndex);
|
||||
|
||||
if (itemIndex >= top && itemIndex <= bottom) return;
|
||||
|
@@ -6,6 +6,7 @@ const { NoteList } = require('./NoteList.min.js');
|
||||
const { NoteText } = require('./NoteText.min.js');
|
||||
const { PromptDialog } = require('./PromptDialog.min.js');
|
||||
const NotePropertiesDialog = require('./NotePropertiesDialog.min.js');
|
||||
const ShareNoteDialog = require('./ShareNoteDialog.js').default;
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
@@ -24,6 +25,7 @@ class MainScreenComponent extends React.Component {
|
||||
super();
|
||||
|
||||
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
|
||||
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
|
||||
this.sidebar_onDrag = this.sidebar_onDrag.bind(this);
|
||||
this.noteList_onDrag = this.noteList_onDrag.bind(this);
|
||||
}
|
||||
@@ -40,7 +42,11 @@ class MainScreenComponent extends React.Component {
|
||||
this.setState({ notePropertiesDialogOptions: {} });
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
shareNoteDialog_close() {
|
||||
this.setState({ shareNoteDialogOptions: {} });
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
promptOptions: null,
|
||||
modalLayer: {
|
||||
@@ -48,10 +54,11 @@ class MainScreenComponent extends React.Component {
|
||||
message: '',
|
||||
},
|
||||
notePropertiesDialogOptions: {},
|
||||
shareNoteDialogOptions: {},
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.windowCommand) {
|
||||
this.doCommand(newProps.windowCommand);
|
||||
}
|
||||
@@ -247,6 +254,13 @@ class MainScreenComponent extends React.Component {
|
||||
onRevisionLinkClick: command.onRevisionLinkClick,
|
||||
},
|
||||
});
|
||||
} else if (command.name === 'commandShareNoteDialog') {
|
||||
this.setState({
|
||||
shareNoteDialogOptions: {
|
||||
noteIds: command.noteIds,
|
||||
visible: true,
|
||||
},
|
||||
});
|
||||
} else if (command.name === 'toggleVisiblePanes') {
|
||||
this.toggleVisiblePanes();
|
||||
} else if (command.name === 'toggleSidebar') {
|
||||
@@ -362,14 +376,16 @@ class MainScreenComponent extends React.Component {
|
||||
backgroundColor: theme.warningBackgroundColor,
|
||||
};
|
||||
|
||||
const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0);
|
||||
|
||||
this.styles_.verticalResizer = {
|
||||
width: 5,
|
||||
height: height,
|
||||
// HACK: For unknown reasons, the resizers are just a little bit taller than the other elements,
|
||||
// making the whole window scroll vertically. So we remove 10 extra pixels here.
|
||||
height: rowHeight - 10,
|
||||
display: 'inline-block',
|
||||
};
|
||||
|
||||
const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0);
|
||||
|
||||
this.styles_.sideBar = {
|
||||
width: sidebarWidth - this.styles_.verticalResizer.width,
|
||||
height: rowHeight,
|
||||
@@ -573,6 +589,7 @@ class MainScreenComponent extends React.Component {
|
||||
const modalLayerStyle = Object.assign({}, styles.modalLayer, { display: this.state.modalLayer.visible ? 'block' : 'none' });
|
||||
|
||||
const notePropertiesDialogOptions = this.state.notePropertiesDialogOptions;
|
||||
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
|
||||
const keyboardMode = Setting.value('editor.keyboardMode');
|
||||
|
||||
return (
|
||||
@@ -580,6 +597,7 @@ class MainScreenComponent extends React.Component {
|
||||
<div style={modalLayerStyle}>{this.state.modalLayer.message}</div>
|
||||
|
||||
{notePropertiesDialogOptions.visible && <NotePropertiesDialog theme={this.props.theme} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
|
||||
{shareNoteDialogOptions.visible && <ShareNoteDialog theme={this.props.theme} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />}
|
||||
|
||||
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} theme={this.props.theme} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
|
||||
|
||||
@@ -589,7 +607,7 @@ class MainScreenComponent extends React.Component {
|
||||
<VerticalResizer style={styles.verticalResizer} onDrag={this.sidebar_onDrag} />
|
||||
<NoteList style={styles.noteList} />
|
||||
<VerticalResizer style={styles.verticalResizer} onDrag={this.noteList_onDrag} />
|
||||
<NoteText style={styles.noteText} keyboardMode={keyboardMode} visiblePanes={this.props.noteVisiblePanes} noteDevToolsVisible={this.props.noteDevToolsVisible} />
|
||||
<NoteText style={styles.noteText} keyboardMode={keyboardMode} visiblePanes={this.props.noteVisiblePanes} />
|
||||
|
||||
{pluginDialog}
|
||||
</div>
|
||||
@@ -613,7 +631,6 @@ const mapStateToProps = state => {
|
||||
noteListWidth: state.settings['style.noteList.width'],
|
||||
selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||
plugins: state.plugins,
|
||||
noteDevToolsVisible: state.noteDevToolsVisible,
|
||||
templates: state.templates,
|
||||
};
|
||||
};
|
||||
|
@@ -4,7 +4,7 @@ const { connect } = require('react-redux');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
|
||||
class NavigatorComponent extends Component {
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.route) {
|
||||
const screenInfo = this.props.screens[newProps.route.routeName];
|
||||
let windowTitle = ['Joplin'];
|
||||
|
@@ -87,6 +87,7 @@ class NoteListComponent extends React.Component {
|
||||
const menu = NoteListUtils.makeContextMenu(noteIds, {
|
||||
notes: this.props.notes,
|
||||
dispatch: this.props.dispatch,
|
||||
watchedNoteFiles: this.props.watchedNoteFiles,
|
||||
});
|
||||
|
||||
menu.popup(bridge().window());
|
||||
|
@@ -2,6 +2,7 @@ const React = require('react');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const DialogButtonRow = require('./DialogButtonRow.min');
|
||||
const Datetime = require('react-datetime');
|
||||
const Note = require('lib/models/Note');
|
||||
const formatcoords = require('formatcoords');
|
||||
@@ -11,10 +12,8 @@ class NotePropertiesDialog extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.okButton_click = this.okButton_click.bind(this);
|
||||
this.cancelButton_click = this.cancelButton_click.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.revisionsLink_click = this.revisionsLink_click.bind(this);
|
||||
this.buttonRow_click = this.buttonRow_click.bind(this);
|
||||
this.okButton = React.createRef();
|
||||
|
||||
this.state = {
|
||||
@@ -27,6 +26,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
id: _('ID'),
|
||||
user_created_time: _('Created'),
|
||||
user_updated_time: _('Updated'),
|
||||
todo_completed: _('Completed'),
|
||||
location: _('Location'),
|
||||
source_url: _('URL'),
|
||||
revisionsLink: _('Note History'),
|
||||
@@ -72,6 +72,11 @@ class NotePropertiesDialog extends React.Component {
|
||||
|
||||
formNote.user_updated_time = time.formatMsToLocal(note.user_updated_time);
|
||||
formNote.user_created_time = time.formatMsToLocal(note.user_created_time);
|
||||
|
||||
if (note.todo_completed) {
|
||||
formNote.todo_completed = time.formatMsToLocal(note.todo_completed);
|
||||
}
|
||||
|
||||
formNote.source_url = note.source_url;
|
||||
|
||||
formNote.location = '';
|
||||
@@ -90,6 +95,11 @@ class NotePropertiesDialog extends React.Component {
|
||||
const note = Object.assign({ id: formNote.id }, this.latLongFromLocation(formNote.location));
|
||||
note.user_created_time = time.formatLocalToMs(formNote.user_created_time);
|
||||
note.user_updated_time = time.formatLocalToMs(formNote.user_updated_time);
|
||||
|
||||
if (formNote.todo_completed) {
|
||||
note.todo_completed = time.formatMsToLocal(formNote.todo_completed);
|
||||
}
|
||||
|
||||
note.source_url = formNote.source_url;
|
||||
|
||||
return note;
|
||||
@@ -107,6 +117,9 @@ class NotePropertiesDialog extends React.Component {
|
||||
this.styles_.controlBox = {
|
||||
marginBottom: '1em',
|
||||
color: 'black', // This will apply for the calendar
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
this.styles_.button = {
|
||||
@@ -123,8 +136,11 @@ class NotePropertiesDialog extends React.Component {
|
||||
color: theme.color,
|
||||
textDecoration: 'none',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
padding: '.14em',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '0.5em',
|
||||
};
|
||||
|
||||
this.styles_.input = {
|
||||
@@ -153,12 +169,8 @@ class NotePropertiesDialog extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
okButton_click() {
|
||||
this.closeDialog(true);
|
||||
}
|
||||
|
||||
cancelButton_click() {
|
||||
this.closeDialog(false);
|
||||
buttonRow_click(event) {
|
||||
this.closeDialog(event.buttonName === 'ok');
|
||||
}
|
||||
|
||||
revisionsLink_click() {
|
||||
@@ -166,14 +178,6 @@ class NotePropertiesDialog extends React.Component {
|
||||
if (this.props.onRevisionLinkClick) this.props.onRevisionLinkClick();
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
if (event.keyCode === 13) {
|
||||
this.closeDialog(true);
|
||||
} else if (event.keyCode === 27) {
|
||||
this.closeDialog(false);
|
||||
}
|
||||
}
|
||||
|
||||
editPropertyButtonClick(key, initialValue) {
|
||||
this.setState({
|
||||
editedKey: key,
|
||||
@@ -218,15 +222,12 @@ class NotePropertiesDialog extends React.Component {
|
||||
async cancelProperty() {
|
||||
return new Promise((resolve) => {
|
||||
this.okButton.current.focus();
|
||||
this.setState(
|
||||
{
|
||||
editedKey: null,
|
||||
editedValue: null,
|
||||
},
|
||||
() => {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
this.setState({
|
||||
editedKey: null,
|
||||
editedValue: null,
|
||||
}, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -328,7 +329,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
if (editCompHandler) {
|
||||
editComp = (
|
||||
<a href="#" onClick={editCompHandler} style={styles.editPropertyButton}>
|
||||
<i className={`fa ${editCompIcon}`} aria-hidden="true" style={{ marginLeft: '.5em' }}></i>
|
||||
<i className={`fa ${editCompIcon}`} aria-hidden="true"></i>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -354,7 +355,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
return dms.format('DDMMss', { decimalPlaces: 0 });
|
||||
}
|
||||
|
||||
if (['user_updated_time', 'user_created_time'].indexOf(key) >= 0) {
|
||||
if (['user_updated_time', 'user_created_time', 'todo_completed'].indexOf(key) >= 0) {
|
||||
return time.formatMsToLocal(note[key]);
|
||||
}
|
||||
|
||||
@@ -363,21 +364,8 @@ class NotePropertiesDialog extends React.Component {
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const styles = this.styles(this.props.theme);
|
||||
const formNote = this.state.formNote;
|
||||
|
||||
const buttonComps = [];
|
||||
buttonComps.push(
|
||||
<button key="ok" style={styles.button} onClick={this.okButton_click} ref={this.okButton} onKeyDown={this.onKeyDown}>
|
||||
{_('Apply')}
|
||||
</button>
|
||||
);
|
||||
buttonComps.push(
|
||||
<button key="cancel" style={styles.button} onClick={this.cancelButton_click}>
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
);
|
||||
|
||||
const noteComps = [];
|
||||
|
||||
if (formNote) {
|
||||
@@ -393,7 +381,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
<div style={theme.dialogBox}>
|
||||
<div style={theme.dialogTitle}>{_('Note properties')}</div>
|
||||
<div>{noteComps}</div>
|
||||
<div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>
|
||||
<DialogButtonRow theme={this.props.theme} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -6,16 +6,16 @@ const NoteTextViewer = require('./NoteTextViewer.min');
|
||||
const HelpButton = require('./HelpButton.min');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const Revision = require('lib/models/Revision');
|
||||
const Note = require('lib/models/Note');
|
||||
const urlUtils = require('lib/urlUtils');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const RevisionService = require('lib/services/RevisionService');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const MarkupToHtml = require('lib/renderers/MarkupToHtml');
|
||||
const { MarkupToHtml, assetsToHeaders } = require('joplin-renderer');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const ReactTooltip = require('react-tooltip');
|
||||
const { urlDecode, substrWithEllipsis } = require('lib/string-utils');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const markupLanguageUtils = require('lib/markupLanguageUtils');
|
||||
|
||||
class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
constructor() {
|
||||
@@ -101,7 +101,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
|
||||
async reloadNote() {
|
||||
let noteBody = '';
|
||||
let markupLanguage = Note.MARKUP_LANGUAGE_MARKDOWN;
|
||||
let markupLanguage = MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
|
||||
if (!this.state.revisions.length || !this.state.currentRevId) {
|
||||
noteBody = _('This note has no history');
|
||||
this.setState({ note: null });
|
||||
@@ -116,18 +116,22 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const markupToHtml = new MarkupToHtml({
|
||||
const markupToHtml = markupLanguageUtils.newMarkupToHtml({
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
});
|
||||
|
||||
const result = markupToHtml.render(markupLanguage, noteBody, theme, {
|
||||
const result = await markupToHtml.render(markupLanguage, noteBody, theme, {
|
||||
codeTheme: theme.codeThemeCss,
|
||||
userCss: this.props.customCss ? this.props.customCss : '',
|
||||
resources: await shared.attachedResources(noteBody),
|
||||
postMessageSyntax: 'ipcProxySendToHost',
|
||||
});
|
||||
|
||||
this.viewerRef_.current.wrappedInstance.send('setHtml', result.html, { cssFiles: result.cssFiles });
|
||||
this.viewerRef_.current.wrappedInstance.send('setHtml', result.html, {
|
||||
cssFiles: result.cssFiles,
|
||||
pluginAssets: result.pluginAssets,
|
||||
pluginAssetsHeadersHtml: assetsToHeaders(result.pluginAssets),
|
||||
});
|
||||
}
|
||||
|
||||
async webview_ipcMessage(event) {
|
||||
@@ -196,7 +200,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
</div>
|
||||
);
|
||||
|
||||
const viewer = <NoteTextViewer viewerStyle={{ display: 'flex', flex: 1 }} ref={this.viewerRef_} onDomReady={this.viewer_domReady} onIpcMessage={this.webview_ipcMessage} />;
|
||||
const viewer = <NoteTextViewer viewerStyle={{ display: 'flex', flex: 1, borderLeft: 'none' }} ref={this.viewerRef_} onDomReady={this.viewer_domReady} onIpcMessage={this.webview_ipcMessage} />;
|
||||
|
||||
return (
|
||||
<div style={style.root}>
|
||||
|
@@ -4,9 +4,9 @@ const BaseItem = require('lib/models/BaseItem.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const InteropServiceHelper = require('../InteropServiceHelper.js');
|
||||
const { IconButton } = require('./IconButton.min.js');
|
||||
const { urlDecode, substrWithEllipsis } = require('lib/string-utils');
|
||||
const Toolbar = require('./Toolbar.min.js');
|
||||
@@ -14,7 +14,7 @@ const TagList = require('./TagList.min.js');
|
||||
const { connect } = require('react-redux');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const MarkupToHtml = require('lib/renderers/MarkupToHtml');
|
||||
const { MarkupToHtml, assetsToHeaders } = require('joplin-renderer');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
@@ -40,6 +40,7 @@ const SearchEngine = require('lib/services/SearchEngine');
|
||||
const NoteTextViewer = require('./NoteTextViewer.min');
|
||||
const NoteRevisionViewer = require('./NoteRevisionViewer.min');
|
||||
const TemplateUtils = require('lib/TemplateUtils');
|
||||
const markupLanguageUtils = require('lib/markupLanguageUtils');
|
||||
|
||||
require('brace/mode/markdown');
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
@@ -79,7 +80,7 @@ class CustomMdMode extends ace.acequire('ace/mode/markdown').Mode {
|
||||
}
|
||||
}
|
||||
|
||||
const NOTE_TAG_BAR_FEATURE_ENABLED = false;
|
||||
const NOTE_TAG_BAR_FEATURE_ENABLED = true;
|
||||
|
||||
class NoteTextComponent extends React.Component {
|
||||
constructor() {
|
||||
@@ -110,6 +111,7 @@ class NoteTextComponent extends React.Component {
|
||||
newAndNoTitleChangeNoteId: null,
|
||||
bodyHtml: '',
|
||||
lastRenderCssFiles: [],
|
||||
lastRenderPluginAssets: [],
|
||||
lastKeys: [],
|
||||
showLocalSearch: false,
|
||||
localSearch: Object.assign({}, this.localSearchDefaultState),
|
||||
@@ -400,13 +402,15 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
markupToHtml() {
|
||||
if (this.markupToHtml_) return this.markupToHtml_;
|
||||
this.markupToHtml_ = new MarkupToHtml({
|
||||
|
||||
this.markupToHtml_ = markupLanguageUtils.newMarkupToHtml({
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
});
|
||||
|
||||
return this.markupToHtml_;
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
async UNSAFE_componentWillMount() {
|
||||
let note = null;
|
||||
let noteTags = [];
|
||||
if (this.props.newNote) {
|
||||
@@ -454,19 +458,6 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// if (Setting.value('env') === 'dev' && this.webviewRef()) {
|
||||
// this.webviewRef().openDevTools();
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (this.webviewRef() && this.props.noteDevToolsVisible !== this.webviewRef().isDevToolsOpened()) {
|
||||
if (this.props.noteDevToolsVisible) {
|
||||
this.webviewRef().openDevTools();
|
||||
} else {
|
||||
this.webviewRef().closeDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
const currentNoteId = this.state.note ? this.state.note.id : null;
|
||||
if (this.lastComponentUpdateNoteId_ !== currentNoteId && this.editor_) {
|
||||
this.editor_.editor.getSession().setMode(new CustomMdMode());
|
||||
@@ -547,7 +538,6 @@ class NoteTextComponent extends React.Component {
|
||||
let note = null;
|
||||
let loadingNewNote = true;
|
||||
let parentFolder = null;
|
||||
let noteTags = [];
|
||||
let scrollPercent = 0;
|
||||
|
||||
if (props.newNote) {
|
||||
@@ -562,7 +552,6 @@ class NoteTextComponent extends React.Component {
|
||||
if (!scrollPercent) scrollPercent = 0;
|
||||
|
||||
loadingNewNote = stateNoteId !== noteId;
|
||||
noteTags = await Tag.tagsByNoteId(noteId);
|
||||
this.lastLoadedNoteId_ = noteId;
|
||||
note = noteId ? await Note.load(noteId) : null;
|
||||
if (noteId !== this.lastLoadedNoteId_) return defer(); // Race condition - current note was changed while this one was loading
|
||||
@@ -641,7 +630,6 @@ class NoteTextComponent extends React.Component {
|
||||
webviewReady: webviewReady,
|
||||
folder: parentFolder,
|
||||
lastKeys: [],
|
||||
noteTags: noteTags,
|
||||
showRevisions: false,
|
||||
};
|
||||
|
||||
@@ -662,22 +650,6 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
// https://github.com/laurent22/joplin/pull/893#discussion_r228025210
|
||||
// @Abijeet: Had to add this check. If not, was going into an infinite loop where state was getting updated repeatedly.
|
||||
// Since I'm updating the state, the componentWillReceiveProps was getting triggered again, where nextProps.newNote was still true, causing reloadNote to trigger again and again.
|
||||
// Notes from Laurent: The selected note tags are part of the global Redux state because they need to be updated whenever tags are changed or deleted
|
||||
// anywhere in the app. Thus it's not possible simple to load the tags here (as we won't have a way to know if they're updated afterwards).
|
||||
// Perhaps a better way would be to move that code in the middleware, check for TAGS_DELETE, TAGS_UPDATE, etc. actions and update the
|
||||
// selected note tags accordingly.
|
||||
if (NOTE_TAG_BAR_FEATURE_ENABLED) {
|
||||
if (!this.props.newNote) {
|
||||
this.props.dispatch({
|
||||
type: 'SET_NOTE_TAGS',
|
||||
items: noteTags,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if (newState.note) await shared.refreshAttachedResources(this, newState.note.body);
|
||||
|
||||
await this.updateHtml(newState.note ? newState.note.markup_language : null, newState.note ? newState.note.body : '');
|
||||
@@ -685,7 +657,7 @@ class NoteTextComponent extends React.Component {
|
||||
defer();
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
async UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (this.props.newNote !== nextProps.newNote && nextProps.newNote) {
|
||||
await this.scheduleReloadNote(nextProps);
|
||||
} else if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
||||
@@ -717,18 +689,38 @@ class NoteTextComponent extends React.Component {
|
||||
if (newTags.length !== oldTags.length) return true;
|
||||
|
||||
for (let i = 0; i < newTags.length; ++i) {
|
||||
let found = false;
|
||||
let currNewTag = newTags[i];
|
||||
for (let j = 0; j < oldTags.length; ++j) {
|
||||
let currOldTag = oldTags[j];
|
||||
if (currOldTag.id === currNewTag.id && currOldTag.updated_time !== currNewTag.updated_time) {
|
||||
return true;
|
||||
if (currOldTag.id === currNewTag.id) {
|
||||
found = true;
|
||||
if (currOldTag.updated_time !== currNewTag.updated_time) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
canDisplayTagBar() {
|
||||
if (!NOTE_TAG_BAR_FEATURE_ENABLED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.state.noteTags || this.state.noteTags.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async noteRevisionViewer_onBack() {
|
||||
// When coming back from the revision viewer, the webview has been
|
||||
// unmounted so will need to reload. We set webviewReady to false
|
||||
@@ -855,7 +847,11 @@ class NoteTextComponent extends React.Component {
|
||||
if (item.type_ === BaseModel.TYPE_RESOURCE) {
|
||||
const localState = await Resource.localState(item);
|
||||
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE || !!item.encryption_blob_encrypted) {
|
||||
bridge().showErrorMessageBox(_('This attachment is not downloaded or not decrypted yet.'));
|
||||
if (localState.fetch_status === Resource.FETCH_STATUS_ERROR) {
|
||||
bridge().showErrorMessageBox(`${_('There was an error downloading this attachment:')}\n\n${localState.fetch_error}`);
|
||||
} else {
|
||||
bridge().showErrorMessageBox(_('This attachment is not downloaded or not decrypted yet'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const filePath = Resource.fullPath(item);
|
||||
@@ -942,6 +938,8 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
|
||||
webview_domReady() {
|
||||
|
||||
console.info('webview_domReady', this.webviewRef_.current);
|
||||
if (!this.webviewRef_.current) return;
|
||||
|
||||
this.setState({
|
||||
@@ -1054,10 +1052,10 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
if (bodyToRender === null) {
|
||||
bodyToRender = this.state.note && this.state.note.body ? this.state.note.body : '';
|
||||
markupLanguage = this.state.note ? this.state.note.markup_language : Note.MARKUP_LANGUAGE_MARKDOWN;
|
||||
markupLanguage = this.state.note ? this.state.note.markup_language : MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
|
||||
}
|
||||
|
||||
if (!markupLanguage) markupLanguage = Note.MARKUP_LANGUAGE_MARKDOWN;
|
||||
if (!markupLanguage) markupLanguage = MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
|
||||
|
||||
const resources = await shared.attachedResources(bodyToRender);
|
||||
|
||||
@@ -1078,11 +1076,11 @@ class NoteTextComponent extends React.Component {
|
||||
bodyToRender = `<i>${_('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout'))}</i>`;
|
||||
}
|
||||
|
||||
const result = this.markupToHtml().render(markupLanguage, bodyToRender, theme, mdOptions);
|
||||
const result = await this.markupToHtml().render(markupLanguage, bodyToRender, theme, mdOptions);
|
||||
|
||||
this.setState({
|
||||
bodyHtml: result.html,
|
||||
lastRenderCssFiles: result.cssFiles,
|
||||
lastRenderPluginAssets: result.pluginAssets,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1113,9 +1111,11 @@ class NoteTextComponent extends React.Component {
|
||||
if (!command) return;
|
||||
|
||||
let fn = null;
|
||||
let args = null;
|
||||
|
||||
if (command.name === 'exportPdf') {
|
||||
fn = this.commandSavePdf;
|
||||
args = {noteId: command.noteId};
|
||||
} else if (command.name === 'print') {
|
||||
fn = this.commandPrint;
|
||||
}
|
||||
@@ -1131,6 +1131,8 @@ class NoteTextComponent extends React.Component {
|
||||
fn = this.commandDateTime;
|
||||
} else if (command.name === 'commandStartExternalEditing') {
|
||||
fn = this.commandStartExternalEditing;
|
||||
} else if (command.name === 'commandStopExternalEditing') {
|
||||
fn = this.commandStopExternalEditing;
|
||||
} else if (command.name === 'showLocalSearch') {
|
||||
fn = this.commandShowLocalSearch;
|
||||
} else if (command.name === 'textCode') {
|
||||
@@ -1165,7 +1167,7 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
fn = fn.bind(this);
|
||||
fn();
|
||||
fn(args);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1224,11 +1226,6 @@ class NoteTextComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
// helper function to style the title for printing
|
||||
title_(title) {
|
||||
return `<div style="font-size: 2em; font-weight: bold; border-bottom: 1px solid rgb(230,230,230); padding-bottom: .3em;">${title}</div><br>`;
|
||||
}
|
||||
|
||||
async printTo_(target, options) {
|
||||
if (this.props.selectedNoteIds.length !== 1 || !this.webviewRef_.current) {
|
||||
throw new Error(_('Only one note can be printed or exported to PDF at a time.'));
|
||||
@@ -1240,53 +1237,71 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
|
||||
this.isPrinting_ = true;
|
||||
const previousBody = this.state.note.body;
|
||||
const tempBody = `${this.title_(this.state.note.title)}\n\n${previousBody}`;
|
||||
|
||||
const previousTheme = Setting.value('theme');
|
||||
Setting.setValue('theme', Setting.THEME_LIGHT);
|
||||
this.lastSetHtml_ = '';
|
||||
await this.updateHtml(this.state.note.markup_language, tempBody, { useCustomCss: true });
|
||||
this.forceUpdate();
|
||||
// const previousBody = this.state.note.body;
|
||||
// const tempBody = `${this.state.note.title}\n\n${previousBody}`;
|
||||
|
||||
const restoreSettings = async () => {
|
||||
Setting.setValue('theme', previousTheme);
|
||||
this.lastSetHtml_ = '';
|
||||
await this.updateHtml(this.state.note.markup_language, previousBody);
|
||||
this.forceUpdate();
|
||||
};
|
||||
// const previousTheme = Setting.value('theme');
|
||||
// Setting.setValue('theme', Setting.THEME_LIGHT);
|
||||
// this.lastSetHtml_ = '';
|
||||
// await this.updateHtml(this.state.note.markup_language, tempBody, { useCustomCss: true });
|
||||
// this.forceUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
// const restoreSettings = async () => {
|
||||
// Setting.setValue('theme', previousTheme);
|
||||
// this.lastSetHtml_ = '';
|
||||
// await this.updateHtml(this.state.note.markup_language, previousBody);
|
||||
// this.forceUpdate();
|
||||
// };
|
||||
|
||||
// Need to save because the interop service reloads the note from the database
|
||||
await this.saveIfNeeded();
|
||||
|
||||
setTimeout(async () => {
|
||||
if (target === 'pdf') {
|
||||
this.webviewRef_.current.wrappedInstance.printToPDF({ printBackground: true, pageSize: Setting.value('export.pdfPageSize'), landscape: Setting.value('export.pdfPageOrientation') === 'landscape' }, (error, data) => {
|
||||
restoreSettings();
|
||||
|
||||
if (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
} else {
|
||||
shim.fsDriver().writeFile(options.path, data, 'buffer');
|
||||
}
|
||||
});
|
||||
try {
|
||||
const pdfData = await InteropServiceHelper.exportNoteToPdf(options.noteId, {
|
||||
printBackground: true,
|
||||
pageSize: Setting.value('export.pdfPageSize'),
|
||||
landscape: Setting.value('export.pdfPageOrientation') === 'landscape',
|
||||
customCss: this.props.customCss,
|
||||
});
|
||||
await shim.fsDriver().writeFile(options.path, pdfData, 'buffer');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
} else if (target === 'printer') {
|
||||
this.webviewRef_.current.wrappedInstance.print({ printBackground: true });
|
||||
restoreSettings();
|
||||
try {
|
||||
await InteropServiceHelper.printNote(options.noteId, {
|
||||
printBackground: true,
|
||||
customCss: this.props.customCss,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
|
||||
// restoreSettings();
|
||||
}
|
||||
this.isPrinting_ = false;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async commandSavePdf() {
|
||||
async commandSavePdf(args) {
|
||||
try {
|
||||
if (!this.state.note) throw new Error(_('Only one note can be printed or exported to PDF at a time.'));
|
||||
if (!this.state.note && !args.noteId) throw new Error(_('Only one note can be exported to PDF at a time.'));
|
||||
|
||||
const note = (!args.noteId ? this.state.note : await Note.load(args.noteId));
|
||||
|
||||
const path = bridge().showSaveDialog({
|
||||
filters: [{ name: _('PDF File'), extensions: ['pdf'] }],
|
||||
defaultPath: safeFilename(this.state.note.title),
|
||||
defaultPath: safeFilename(note.title),
|
||||
});
|
||||
|
||||
if (!path) return;
|
||||
|
||||
await this.printTo_('pdf', { path: path });
|
||||
await this.printTo_('pdf', { path: path, noteId: note.id });
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
@@ -1294,25 +1309,23 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
async commandPrint() {
|
||||
try {
|
||||
await this.printTo_('printer');
|
||||
if (!this.state.note) throw new Error(_('Only one note can be printed at a time.'));
|
||||
|
||||
await this.printTo_('printer', { noteId: this.state.note.id });
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async commandStartExternalEditing() {
|
||||
try {
|
||||
await this.saveIfNeeded(true, {
|
||||
autoTitle: false,
|
||||
});
|
||||
await ExternalEditWatcher.instance().openAndWatch(this.state.note);
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
|
||||
}
|
||||
await this.saveIfNeeded(true, {
|
||||
autoTitle: false,
|
||||
});
|
||||
NoteListUtils.startExternalEditing(this.state.note.id);
|
||||
}
|
||||
|
||||
async commandStopExternalEditing() {
|
||||
ExternalEditWatcher.instance().stopWatching(this.state.note.id);
|
||||
NoteListUtils.stopExternalEditing(this.state.note.id);
|
||||
}
|
||||
|
||||
async commandSetTags() {
|
||||
@@ -1627,7 +1640,7 @@ class NoteTextComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (note.markup_language === Note.MARKUP_LANGUAGE_MARKDOWN && editorIsVisible) {
|
||||
if (note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN && editorIsVisible) {
|
||||
toolbarItems.push({
|
||||
tooltip: _('Bold'),
|
||||
iconName: 'fa-bold',
|
||||
@@ -1818,6 +1831,7 @@ class NoteTextComponent extends React.Component {
|
||||
const menu = NoteListUtils.makeContextMenu(this.props.selectedNoteIds, {
|
||||
notes: this.props.notes,
|
||||
dispatch: this.props.dispatch,
|
||||
watchedNoteFiles: this.props.watchedNoteFiles,
|
||||
});
|
||||
|
||||
const buttonStyle = Object.assign({}, theme.buttonStyle, {
|
||||
@@ -1855,7 +1869,7 @@ class NoteTextComponent extends React.Component {
|
||||
const style = this.props.style;
|
||||
const note = this.state.note;
|
||||
const body = note && note.body ? note.body : '';
|
||||
const markupLanguage = note ? note.markup_language : Note.MARKUP_LANGUAGE_MARKDOWN;
|
||||
const markupLanguage = note ? note.markup_language : MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
|
||||
const isTodo = note && !!note.is_todo;
|
||||
@@ -1933,10 +1947,10 @@ class NoteTextComponent extends React.Component {
|
||||
const searchBarHeight = this.state.showLocalSearch ? 35 : 0;
|
||||
|
||||
let bottomRowHeight = 0;
|
||||
if (NOTE_TAG_BAR_FEATURE_ENABLED) {
|
||||
if (this.canDisplayTagBar()) {
|
||||
bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - tagStyle.height - tagStyle.marginBottom;
|
||||
} else {
|
||||
toolbarStyle.marginBottom = 10;
|
||||
toolbarStyle.marginBottom = tagStyle.marginBottom,
|
||||
bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginBottom;
|
||||
}
|
||||
|
||||
@@ -1996,7 +2010,8 @@ class NoteTextComponent extends React.Component {
|
||||
const htmlHasChanged = this.lastSetHtml_ !== html;
|
||||
if (htmlHasChanged) {
|
||||
let options = {
|
||||
cssFiles: this.state.lastRenderCssFiles,
|
||||
pluginAssets: this.state.lastRenderPluginAssets,
|
||||
pluginAssetsHeadersHtml: assetsToHeaders(this.state.lastRenderPluginAssets),
|
||||
downloadResources: Setting.value('sync.resourceDownloadMode'),
|
||||
};
|
||||
this.webviewRef_.current.wrappedInstance.send('setHtml', html, options);
|
||||
@@ -2054,7 +2069,7 @@ class NoteTextComponent extends React.Component {
|
||||
/>
|
||||
);
|
||||
|
||||
const tagList = !NOTE_TAG_BAR_FEATURE_ENABLED ? null : <TagList style={tagStyle} items={this.state.noteTags} />;
|
||||
const tagList = this.canDisplayTagBar() ? <TagList style={tagStyle} items={this.state.noteTags} /> : null;
|
||||
|
||||
const titleBarMenuButton = (
|
||||
<IconButton
|
||||
@@ -2112,6 +2127,8 @@ class NoteTextComponent extends React.Component {
|
||||
onSelectionChange={this.aceEditor_selectionChange}
|
||||
onFocus={this.aceEditor_focus}
|
||||
readOnly={visiblePanes.indexOf('editor') < 0}
|
||||
// Enable/Disable the autoclosing braces
|
||||
setOptions={{ behavioursEnabled: Setting.value('editor.autoMatchingBraces') }}
|
||||
// Disable warning: "Automatically scrolling cursor into view after
|
||||
// selection change this will be disabled in the next version set
|
||||
// editor.$blockScrolling = Infinity to disable this message"
|
||||
|
@@ -13,6 +13,8 @@ class NoteTextViewerComponent extends React.Component {
|
||||
|
||||
this.webview_domReady = this.webview_domReady.bind(this);
|
||||
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
|
||||
this.webview_load = this.webview_load.bind(this);
|
||||
this.webview_message = this.webview_message.bind(this);
|
||||
}
|
||||
|
||||
webview_domReady(event) {
|
||||
@@ -24,6 +26,22 @@ class NoteTextViewerComponent extends React.Component {
|
||||
if (this.props.onIpcMessage) this.props.onIpcMessage(event);
|
||||
}
|
||||
|
||||
webview_load() {
|
||||
this.webview_domReady();
|
||||
}
|
||||
|
||||
webview_message(event) {
|
||||
if (!event.data || event.data.target !== 'main') return;
|
||||
|
||||
const callName = event.data.name;
|
||||
const args = event.data.args;
|
||||
|
||||
if (this.props.onIpcMessage) this.props.onIpcMessage({
|
||||
channel: callName,
|
||||
args: args,
|
||||
});
|
||||
}
|
||||
|
||||
domReady() {
|
||||
return this.domReady_;
|
||||
}
|
||||
@@ -35,6 +53,7 @@ class NoteTextViewerComponent extends React.Component {
|
||||
this.webviewListeners_ = {
|
||||
'dom-ready': this.webview_domReady.bind(this),
|
||||
'ipc-message': this.webview_ipcMessage.bind(this),
|
||||
'load': this.webview_load.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,19 +63,7 @@ class NoteTextViewerComponent extends React.Component {
|
||||
wv.addEventListener(n, fn);
|
||||
}
|
||||
|
||||
let isAlreadyReady = false;
|
||||
try {
|
||||
isAlreadyReady = !this.webviewRef_.current.isLoading();
|
||||
} catch (error) {
|
||||
// Ignore - it means the view has not started loading, and the DOM ready event has not been emitted yet
|
||||
// Error is "The WebView must be attached to the DOM and the dom-ready event emitted before this method can be called."
|
||||
}
|
||||
|
||||
// Edge-case - the webview was already ready by the time initWebview was
|
||||
// called - so manually call the domReady event to notify caller.
|
||||
if (isAlreadyReady) {
|
||||
this.webview_domReady({});
|
||||
}
|
||||
this.webviewRef_.current.contentWindow.addEventListener('message', this.webview_message);
|
||||
}
|
||||
|
||||
destroyWebview() {
|
||||
@@ -69,6 +76,8 @@ class NoteTextViewerComponent extends React.Component {
|
||||
wv.removeEventListener(n, fn);
|
||||
}
|
||||
|
||||
this.webviewRef_.current.contentWindow.removeEventListener('message', this.webview_message);
|
||||
|
||||
this.initialized_ = false;
|
||||
this.domReady_ = false;
|
||||
}
|
||||
@@ -96,16 +105,32 @@ class NoteTextViewerComponent extends React.Component {
|
||||
// Wrap WebView functions
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
send(channel, arg0 = null, arg1 = null, arg2 = null, arg3 = null) {
|
||||
return this.webviewRef_.current.send(channel, arg0, arg1, arg2, arg3);
|
||||
send(channel, arg0 = null, arg1 = null) {
|
||||
const win = this.webviewRef_.current.contentWindow;
|
||||
|
||||
if (channel === 'setHtml') {
|
||||
win.postMessage({ target: 'webview', name: 'setHtml', data: { html: arg0, options: arg1 } }, '*');
|
||||
}
|
||||
|
||||
if (channel === 'scrollToHash') {
|
||||
win.postMessage({ target: 'webview', name: 'scrollToHash', data: { hash: arg0 } }, '*');
|
||||
}
|
||||
|
||||
if (channel === 'setPercentScroll') {
|
||||
win.postMessage({ target: 'webview', name: 'setPercentScroll', data: { percent: arg0 } }, '*');
|
||||
}
|
||||
|
||||
if (channel === 'setMarkers') {
|
||||
win.postMessage({ target: 'webview', name: 'setMarkers', data: { keywords: arg0, options: arg1 } }, '*');
|
||||
}
|
||||
}
|
||||
|
||||
printToPDF(options, callback) {
|
||||
printToPDF() { // options, callback) {
|
||||
// In Electron 4x, printToPDF is broken so need to use this hack:
|
||||
// https://github.com/electron/electron/issues/16171#issuecomment-451090245
|
||||
|
||||
// return this.webviewRef_.current.printToPDF(options, callback);
|
||||
return this.webviewRef_.current.getWebContents().printToPDF(options, callback);
|
||||
// return this.webviewRef_.current.getWebContents().printToPDF(options, callback);
|
||||
}
|
||||
|
||||
print() {
|
||||
@@ -119,15 +144,15 @@ class NoteTextViewerComponent extends React.Component {
|
||||
}
|
||||
|
||||
openDevTools() {
|
||||
return this.webviewRef_.current.openDevTools();
|
||||
// return this.webviewRef_.current.openDevTools();
|
||||
}
|
||||
|
||||
closeDevTools() {
|
||||
return this.webviewRef_.current.closeDevTools();
|
||||
// return this.webviewRef_.current.closeDevTools();
|
||||
}
|
||||
|
||||
isDevToolsOpened() {
|
||||
return this.webviewRef_.current.isDevToolsOpened();
|
||||
// return this.webviewRef_.current.isDevToolsOpened();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
@@ -135,7 +160,8 @@ class NoteTextViewerComponent extends React.Component {
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
render() {
|
||||
return <webview ref={this.webviewRef_} style={this.props.viewerStyle} preload="gui/note-viewer/preload.js" src="gui/note-viewer/index.html" webpreferences="contextIsolation" />;
|
||||
const viewerStyle = Object.assign({}, this.props.viewerStyle, { borderTop: 'none', borderRight: 'none', borderBottom: 'none' });
|
||||
return <iframe className="noteTextViewer" ref={this.webviewRef_} style={viewerStyle} src="gui/note-viewer/index.html"></iframe>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,108 +1,76 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js');
|
||||
|
||||
class OneDriveLoginScreenComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.webview_ = null;
|
||||
this.authCode_ = null;
|
||||
|
||||
this.state = {
|
||||
authLog: [],
|
||||
};
|
||||
}
|
||||
|
||||
refresh_click() {
|
||||
if (!this.webview_) return;
|
||||
this.webview_.src = this.startUrl();
|
||||
}
|
||||
async componentDidMount() {
|
||||
const log = (s) => {
|
||||
this.setState(state => {
|
||||
const authLog = state.authLog.slice();
|
||||
authLog.push({ key: (Date.now() + Math.random()).toString(), text: s });
|
||||
return { authLog: authLog };
|
||||
});
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
webviewUrl: this.startUrl(),
|
||||
webviewReady: false,
|
||||
const syncTargetId = Setting.value('sync.target');
|
||||
const syncTarget = reg.syncTarget(syncTargetId);
|
||||
const oneDriveApiUtils = new OneDriveApiNodeUtils(syncTarget.api());
|
||||
const auth = await oneDriveApiUtils.oauthDance({
|
||||
log: (s) => log(s),
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
|
||||
}
|
||||
Setting.setValue(`sync.${syncTargetId}.auth`, auth ? JSON.stringify(auth) : null);
|
||||
syncTarget.api().setAuth(auth);
|
||||
|
||||
componentWillUnmount() {
|
||||
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
|
||||
}
|
||||
|
||||
webview_domReady() {
|
||||
this.setState({ webviewReady: true });
|
||||
|
||||
this.webview_.addEventListener('did-navigate', async event => {
|
||||
const url = event.url;
|
||||
|
||||
if (this.authCode_) return;
|
||||
|
||||
const urlParse = require('url').parse;
|
||||
const parsedUrl = urlParse(url.trim(), true);
|
||||
|
||||
if (!('code' in parsedUrl.query)) return;
|
||||
|
||||
this.authCode_ = parsedUrl.query.code;
|
||||
|
||||
try {
|
||||
await reg
|
||||
.syncTarget()
|
||||
.api()
|
||||
.execTokenRequest(this.authCode_, this.redirectUrl(), true);
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
reg.scheduleSync(0);
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(`Could not login to OneDrive. Please try again.\n\n${error.message}\n\n${url.match(/.{1,64}/g).join('\n')}`);
|
||||
}
|
||||
|
||||
this.authCode_ = null;
|
||||
});
|
||||
if (!auth) {
|
||||
log(_('Authentication was not completed (did not receive an authentication token).'));
|
||||
} else {
|
||||
reg.scheduleSync(0);
|
||||
}
|
||||
}
|
||||
|
||||
startUrl() {
|
||||
return reg
|
||||
.syncTarget()
|
||||
.api()
|
||||
.authCodeUrl(this.redirectUrl());
|
||||
return reg.syncTarget().api().authCodeUrl(this.redirectUrl());
|
||||
}
|
||||
|
||||
redirectUrl() {
|
||||
return reg
|
||||
.syncTarget()
|
||||
.api()
|
||||
.nativeClientRedirectUrl();
|
||||
return reg.syncTarget().api().nativeClientRedirectUrl();
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const headerStyle = Object.assign({}, theme.headerStyle, { width: style.width });
|
||||
|
||||
const webviewStyle = {
|
||||
width: this.props.style.width,
|
||||
height: this.props.style.height - theme.headerHeight,
|
||||
overflow: 'hidden',
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
|
||||
const headerButtons = [
|
||||
{
|
||||
title: _('Refresh'),
|
||||
onClick: () => this.refresh_click(),
|
||||
iconName: 'fa-refresh',
|
||||
},
|
||||
];
|
||||
const logComps = [];
|
||||
for (const l of this.state.authLog) {
|
||||
if (l.text.indexOf('http:') === 0) {
|
||||
logComps.push(<a key={l.key} style={theme.urlStyle} href="#" onClick={() => { bridge().openExternal(l.text); }}>{l.text}</a>);
|
||||
} else {
|
||||
logComps.push(<p key={l.key} style={theme.textStyle}>{l.text}</p>);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header style={headerStyle} buttons={headerButtons} />
|
||||
<webview src={this.startUrl()} style={webviewStyle} nodeintegration="1" ref={elem => (this.webview_ = elem)} />
|
||||
<Header style={headerStyle}/>
|
||||
<div style={{padding: 10}}>
|
||||
{logComps}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ class PromptDialog extends React.Component {
|
||||
this.answerInput_ = React.createRef();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
visible: false,
|
||||
answer: this.props.defaultValue ? this.props.defaultValue : '',
|
||||
@@ -22,7 +22,7 @@ class PromptDialog extends React.Component {
|
||||
this.focusInput_ = true;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if ('visible' in newProps && newProps.visible !== this.props.visible) {
|
||||
this.setState({ visible: newProps.visible });
|
||||
if (newProps.visible) this.focusInput_ = true;
|
||||
|
219
ElectronClient/app/gui/ShareNoteDialog.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import JoplinServerApi from '../lib/JoplinServerApi';
|
||||
|
||||
const { _, _n } = require('lib/locale.js');
|
||||
const { themeStyle, buildStyle } = require('../theme.js');
|
||||
const DialogButtonRow = require('./DialogButtonRow.min');
|
||||
const Note = require('lib/models/Note');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const BaseItem = require('lib/models/BaseItem');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { clipboard } = require('electron');
|
||||
|
||||
interface ShareNoteDialogProps {
|
||||
theme: number,
|
||||
noteIds: Array<string>,
|
||||
onClose: Function,
|
||||
}
|
||||
|
||||
interface SharesMap {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function styles_(props:ShareNoteDialogProps) {
|
||||
return buildStyle('ShareNoteDialog', props.theme, (theme:any) => {
|
||||
return {
|
||||
noteList: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
note: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
padding: '0.5em',
|
||||
marginBottom: 5,
|
||||
},
|
||||
noteTitle: {
|
||||
...theme.textStyle,
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
color: theme.color,
|
||||
},
|
||||
noteRemoveButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
},
|
||||
noteRemoveButtonIcon: {
|
||||
color: theme.color,
|
||||
fontSize: '1.4em',
|
||||
},
|
||||
copyShareLinkButton: {
|
||||
...theme.buttonStyle,
|
||||
marginBottom: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function ShareNoteDialog(props:ShareNoteDialogProps) {
|
||||
console.info('Render ShareNoteDialog');
|
||||
|
||||
const [notes, setNotes] = useState<any[]>([]);
|
||||
const [sharesState, setSharesState] = useState<string>('unknown');
|
||||
const [shares, setShares] = useState<SharesMap>({});
|
||||
|
||||
const noteCount = notes.length;
|
||||
const theme = themeStyle(props.theme);
|
||||
const styles = styles_(props);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchNotes() {
|
||||
const result = [];
|
||||
for (let noteId of props.noteIds) {
|
||||
result.push(await Note.load(noteId));
|
||||
}
|
||||
setNotes(result);
|
||||
}
|
||||
|
||||
fetchNotes();
|
||||
}, [props.noteIds]);
|
||||
|
||||
const appApi = async () => {
|
||||
return reg.syncTargetNextcloud().appApi();
|
||||
};
|
||||
|
||||
const buttonRow_click = () => {
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const copyLinksToClipboard = (shares:SharesMap) => {
|
||||
const links = [];
|
||||
for (const n in shares) links.push(shares[n]._url);
|
||||
clipboard.writeText(links.join('\n'));
|
||||
};
|
||||
|
||||
const synchronize = async () => {
|
||||
const synchronizer = await reg.syncTarget().synchronizer();
|
||||
await synchronizer.waitForSyncToFinish();
|
||||
await reg.scheduleSync(0);
|
||||
};
|
||||
|
||||
const shareLinkButton_click = async () => {
|
||||
let hasSynced = false;
|
||||
let tryToSync = false;
|
||||
while (true) {
|
||||
try {
|
||||
if (tryToSync) {
|
||||
setSharesState('synchronizing');
|
||||
await synchronize();
|
||||
tryToSync = false;
|
||||
hasSynced = true;
|
||||
}
|
||||
|
||||
setSharesState('creating');
|
||||
|
||||
const api = await appApi();
|
||||
const syncTargetId = api.syncTargetId(Setting.toPlainObject());
|
||||
const newShares = Object.assign({}, shares);
|
||||
let sharedStatusChanged = false;
|
||||
|
||||
for (const note of notes) {
|
||||
const result = await api.exec('POST', 'shares', {
|
||||
syncTargetId: syncTargetId,
|
||||
noteId: note.id,
|
||||
});
|
||||
newShares[note.id] = result;
|
||||
|
||||
const changed = await BaseItem.updateShareStatus(note, true);
|
||||
if (changed) sharedStatusChanged = true;
|
||||
}
|
||||
|
||||
setShares(newShares);
|
||||
|
||||
if (sharedStatusChanged) {
|
||||
setSharesState('synchronizing');
|
||||
await synchronize();
|
||||
setSharesState('creating');
|
||||
}
|
||||
|
||||
copyLinksToClipboard(newShares);
|
||||
|
||||
setSharesState('created');
|
||||
} catch (error) {
|
||||
if (error.code === 404 && !hasSynced) {
|
||||
reg.logger().info('ShareNoteDialog: Note does not exist on server - trying to sync it.', error);
|
||||
tryToSync = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
reg.logger().error('ShareNoteDialog: Cannot share note:', error);
|
||||
|
||||
setSharesState('idle');
|
||||
alert(JoplinServerApi.connectionErrorMessage(error));
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const renderNote = (note:any) => {
|
||||
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>{removeButton}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderNoteList = (notes:any) => {
|
||||
const noteComps = [];
|
||||
for (let noteId of Object.keys(notes)) {
|
||||
noteComps.push(renderNote(notes[noteId]));
|
||||
}
|
||||
return <div style={styles.noteList}>{noteComps}</div>;
|
||||
};
|
||||
|
||||
const statusMessage = (sharesState:string):string => {
|
||||
if (sharesState === 'synchronizing') return _('Synchronising...');
|
||||
if (sharesState === 'creating') return _n('Generating link...', 'Generating links...', noteCount);
|
||||
if (sharesState === 'created') return _n('Link has been copied to clipboard!', 'Links have been copied to clipboard!', noteCount);
|
||||
return '';
|
||||
};
|
||||
|
||||
const encryptionWarningMessage = !Setting.value('encryption.enabled') ? null : <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}</div>;
|
||||
|
||||
const rootStyle = Object.assign({}, theme.dialogBox);
|
||||
rootStyle.width = '50%';
|
||||
|
||||
return (
|
||||
<div style={theme.dialogModalLayer}>
|
||||
<div style={rootStyle}>
|
||||
<div style={theme.dialogTitle}>{_('Share Notes')}</div>
|
||||
{renderNoteList(notes)}
|
||||
<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>
|
||||
{encryptionWarningMessage}
|
||||
<DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -221,6 +221,10 @@ class SideBarComponent extends React.Component {
|
||||
if (item) {
|
||||
const anchorRef = this.anchorItemRefs[item.type][item.id];
|
||||
if (anchorRef) anchorRef.current.focus();
|
||||
} else {
|
||||
const anchorRef = this.firstAnchorItemRef('folder');
|
||||
console.info('anchorRef', anchorRef);
|
||||
if (anchorRef) anchorRef.current.focus();
|
||||
}
|
||||
}
|
||||
} else if (command.name === 'synchronize') {
|
||||
@@ -417,13 +421,6 @@ class SideBarComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
// searchItem_click(search) {
|
||||
// this.props.dispatch({
|
||||
// type: "SEARCH_SELECT",
|
||||
// id: search ? search.id : null,
|
||||
// });
|
||||
// }
|
||||
|
||||
async sync_click() {
|
||||
await shared.synchronize_press(this);
|
||||
}
|
||||
@@ -435,6 +432,18 @@ class SideBarComponent extends React.Component {
|
||||
return this.anchorItemRefs[type][id];
|
||||
}
|
||||
|
||||
firstAnchorItemRef(type) {
|
||||
const refs = this.anchorItemRefs[type];
|
||||
if (!refs) return null;
|
||||
|
||||
const n = `${type}s`;
|
||||
const item = this.props[n] && this.props[n].length ? this.props[n][0] : null;
|
||||
console.info('props', this.props[n], item);
|
||||
if (!item) return null;
|
||||
|
||||
return refs[item.id];
|
||||
}
|
||||
|
||||
noteCountElement(count) {
|
||||
return <div style={this.style().noteCount}>({count})</div>;
|
||||
}
|
||||
@@ -590,8 +599,6 @@ class SideBarComponent extends React.Component {
|
||||
return { type: 'folder', id: this.props.selectedFolderId };
|
||||
} else if (this.props.notesParentType === 'Tag' && this.props.selectedTagId) {
|
||||
return { type: 'tag', id: this.props.selectedTagId };
|
||||
} else if (this.props.notesParentType === 'Search' && this.props.selectedSearchId) {
|
||||
return { type: 'search', id: this.props.selectedSearchId };
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@@ -16,7 +16,7 @@ class StatusScreenComponent extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.resfreshScreen();
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,7 @@ class TagListComponent extends React.Component {
|
||||
style.fontSize = theme.fontSize;
|
||||
|
||||
const tagItems = [];
|
||||
if (tags || tags.length > 0) {
|
||||
if (tags && tags.length > 0) {
|
||||
// Sort by id for now, but probably needs to be changed in the future.
|
||||
tags.sort((a, b) => {
|
||||
return a.title < b.title ? -1 : +1;
|
||||
@@ -31,10 +31,6 @@ class TagListComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (tagItems.length === 0) {
|
||||
style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tag-list" style={style}>
|
||||
{tagItems}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const React = require('react');
|
||||
const electron = require('electron');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
|
||||
class VerticalResizer extends React.PureComponent {
|
||||
constructor() {
|
||||
@@ -32,7 +32,7 @@ class VerticalResizer extends React.PureComponent {
|
||||
|
||||
event.dataTransfer.dropEffect = 'none';
|
||||
|
||||
const cursor = electron.screen.getCursorScreenPoint();
|
||||
const cursor = bridge().screen().getCursorScreenPoint();
|
||||
|
||||
this.setState({
|
||||
drag: {
|
||||
@@ -52,7 +52,7 @@ class VerticalResizer extends React.PureComponent {
|
||||
// const e = event.nativeEvent;
|
||||
// if (!e.buttons || (!e.clientX && !e.clientY && !e.screenX && !e.screenY)) return;
|
||||
|
||||
const cursor = electron.screen.getCursorScreenPoint();
|
||||
const cursor = bridge().screen().getCursorScreenPoint();
|
||||
const newX = cursor.x;
|
||||
const delta = newX - this.state.drag.lastX;
|
||||
if (!delta) return;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<head id="joplin-container-root-head">
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<style>
|
||||
@@ -65,19 +65,7 @@
|
||||
ipc[callName](callData);
|
||||
}
|
||||
}));
|
||||
|
||||
const loadedCssFiles_ = {};
|
||||
function loadCssFiles(cssFiles) {
|
||||
for (let i = 0; i < cssFiles.length; i++) {
|
||||
const f = cssFiles[i];
|
||||
if (loadedCssFiles_[f]) continue;
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = f;
|
||||
document.getElementById('joplin-container-styleContainer').appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
|
||||
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
|
||||
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
|
||||
@@ -154,8 +142,12 @@
|
||||
}, 1);
|
||||
}
|
||||
|
||||
loadCssFiles(event.options.cssFiles);
|
||||
|
||||
if (event.options.pluginAssetsHeadersHtml) {
|
||||
const pluginAssetsContainer = document.createElement('div');
|
||||
pluginAssetsContainer.innerHTML = event.options.pluginAssetsHeadersHtml;
|
||||
document.getElementById('joplin-container-styleContainer').appendChild(pluginAssetsContainer);
|
||||
}
|
||||
|
||||
if (event.options.downloadResources === 'manual') {
|
||||
webviewLib.setupResourceManualDownload();
|
||||
}
|
||||
|
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
|
||||
Atom One Dark With support for ReasonML by Gidi Morris, based off work by Daniel Gamage
|
||||
|
||||
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
|
||||
|
||||
*/
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #abb2bf;
|
||||
background: #282c34;
|
||||
}
|
||||
.hljs-keyword, .hljs-operator {
|
||||
color: #F92672;
|
||||
}
|
||||
.hljs-pattern-match {
|
||||
color: #F92672;
|
||||
}
|
||||
.hljs-pattern-match .hljs-constructor {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-function {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-function .hljs-params {
|
||||
color: #A6E22E;
|
||||
}
|
||||
.hljs-function .hljs-params .hljs-typing {
|
||||
color: #FD971F;
|
||||
}
|
||||
.hljs-module-access .hljs-module {
|
||||
color: #7e57c2;
|
||||
}
|
||||
.hljs-constructor {
|
||||
color: #e2b93d;
|
||||
}
|
||||
.hljs-constructor .hljs-string {
|
||||
color: #9CCC65;
|
||||
}
|
||||
.hljs-comment, .hljs-quote {
|
||||
color: #b18eb1;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-doctag, .hljs-formula {
|
||||
color: #c678dd;
|
||||
}
|
||||
.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst {
|
||||
color: #e06c75;
|
||||
}
|
||||
.hljs-literal {
|
||||
color: #56b6c2;
|
||||
}
|
||||
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string {
|
||||
color: #98c379;
|
||||
}
|
||||
.hljs-built_in, .hljs-class .hljs-title {
|
||||
color: #e6c07b;
|
||||
}
|
||||
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number {
|
||||
color: #d19a66;
|
||||
}
|
||||
.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
|
||||
Atom One Light by Daniel Gamage
|
||||
Original One Light Syntax theme from https://github.com/atom/one-light-syntax
|
||||
|
||||
base: #fafafa
|
||||
mono-1: #383a42
|
||||
mono-2: #686b77
|
||||
mono-3: #a0a1a7
|
||||
hue-1: #0184bb
|
||||
hue-2: #4078f2
|
||||
hue-3: #a626a4
|
||||
hue-4: #50a14f
|
||||
hue-5: #e45649
|
||||
hue-5-2: #c91243
|
||||
hue-6: #986801
|
||||
hue-6-2: #c18401
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #383a42;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #a0a1a7;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-formula {
|
||||
color: #a626a4;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name,
|
||||
.hljs-selector-tag,
|
||||
.hljs-deletion,
|
||||
.hljs-subst {
|
||||
color: #e45649;
|
||||
}
|
||||
|
||||
.hljs-literal {
|
||||
color: #0184bb;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-addition,
|
||||
.hljs-attribute,
|
||||
.hljs-meta-string {
|
||||
color: #50a14f;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-class .hljs-title {
|
||||
color: #c18401;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-number {
|
||||
color: #986801;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-link,
|
||||
.hljs-meta,
|
||||
.hljs-selector-id,
|
||||
.hljs-title {
|
||||
color: #4078f2;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
@@ -7,6 +7,7 @@ const eventManager = require('../../eventManager');
|
||||
const InteropService = require('lib/services/InteropService');
|
||||
const InteropServiceHelper = require('../../InteropServiceHelper.js');
|
||||
const Note = require('lib/models/Note');
|
||||
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
||||
const { substrWithEllipsis } = require('lib/string-utils');
|
||||
|
||||
class NoteListUtils {
|
||||
@@ -49,6 +50,28 @@ class NoteListUtils {
|
||||
})
|
||||
);
|
||||
|
||||
if (props.watchedNoteFiles.indexOf(noteIds[0]) < 0) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Edit in external editor'),
|
||||
enabled: noteIds.length === 1,
|
||||
click: async () => {
|
||||
this.startExternalEditing(noteIds[0]);
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Stop external editing'),
|
||||
enabled: noteIds.length === 1,
|
||||
click: async () => {
|
||||
this.stopExternalEditing(noteIds[0]);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (noteIds.length <= 1) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
@@ -107,6 +130,20 @@ class NoteListUtils {
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Share note...'),
|
||||
click: async () => {
|
||||
console.info('NOTE IDS', noteIds);
|
||||
props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'commandShareNoteDialog',
|
||||
noteIds: noteIds.slice(),
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const exportMenu = new Menu();
|
||||
|
||||
const ioService = new InteropService();
|
||||
@@ -114,6 +151,7 @@ class NoteListUtils {
|
||||
for (let i = 0; i < ioModules.length; i++) {
|
||||
const module = ioModules[i];
|
||||
if (module.type !== 'exporter') continue;
|
||||
if (noteIds.length > 1 && module.canDoMultiExport === false) continue;
|
||||
|
||||
exportMenu.append(
|
||||
new MenuItem({
|
||||
@@ -133,6 +171,7 @@ class NoteListUtils {
|
||||
props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'exportPdf',
|
||||
noteId: noteIds[0],
|
||||
});
|
||||
},
|
||||
})
|
||||
@@ -176,6 +215,20 @@ class NoteListUtils {
|
||||
if (!ok) return;
|
||||
await Note.batchDelete(noteIds);
|
||||
}
|
||||
|
||||
static async startExternalEditing(noteId) {
|
||||
try {
|
||||
const note = await Note.load(noteId);
|
||||
ExternalEditWatcher.instance().openAndWatch(note);
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
|
||||
}
|
||||
}
|
||||
|
||||
static async stopExternalEditing(noteId) {
|
||||
ExternalEditWatcher.instance().stopWatching(noteId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = NoteListUtils;
|
||||
|