You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-27 20:29:45 +02:00
Compare commits
1 Commits
note-drag-
...
linux-fold
Author | SHA1 | Date | |
---|---|---|---|
|
c7299cf679 |
@@ -835,7 +835,6 @@ packages/renderer/index.js
|
||||
packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.js
|
||||
packages/tools/buildServerDocker.js
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/bundleDefaultPlugins.js
|
||||
@@ -846,7 +845,6 @@ packages/tools/convertThemesToCss.js
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/git-changelog.test.js
|
||||
packages/tools/licenseChecker.js
|
||||
packages/tools/release-android.js
|
||||
packages/tools/release-cli.js
|
||||
|
@@ -91,10 +91,6 @@ module.exports = {
|
||||
// Disable because of this: https://github.com/facebook/react/issues/16265
|
||||
// "react-hooks/exhaustive-deps": "warn",
|
||||
|
||||
'jest/require-top-level-describe': ['error', { 'maxNumberOfTopLevelDescribes': 1 }],
|
||||
'jest/no-identical-title': ['error'],
|
||||
'jest/prefer-lowercase-title': ['error', { 'ignoreTopLevelDescribe': true }],
|
||||
|
||||
'promise/prefer-await-to-then': 'error',
|
||||
'no-unneeded-ternary': 'error',
|
||||
|
||||
@@ -159,7 +155,6 @@ module.exports = {
|
||||
// 'react-hooks',
|
||||
'import',
|
||||
'promise',
|
||||
'jest',
|
||||
],
|
||||
'overrides': [
|
||||
{
|
||||
@@ -180,7 +175,9 @@ module.exports = {
|
||||
'project': './tsconfig.eslint.json',
|
||||
},
|
||||
'rules': {
|
||||
'@typescript-eslint/explicit-member-accessibility': ['error'],
|
||||
// Warn only because it would make it difficult to convert JS classes to TypeScript, unless we
|
||||
// make everything public which is not great. New code however should specify member accessibility.
|
||||
'@typescript-eslint/explicit-member-accessibility': ['warn'],
|
||||
'@typescript-eslint/type-annotation-spacing': ['error', { 'before': false, 'after': true }],
|
||||
'@typescript-eslint/no-inferrable-types': ['error', { 'ignoreParameters': true, 'ignoreProperties': true }],
|
||||
'@typescript-eslint/comma-dangle': ['error', {
|
||||
|
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: contributor-assistant/github-action@v2.3.0
|
||||
uses: contributor-assistant/github-action@v2.2.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -823,7 +823,6 @@ packages/renderer/index.js
|
||||
packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.js
|
||||
packages/tools/buildServerDocker.js
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/bundleDefaultPlugins.js
|
||||
@@ -834,7 +833,6 @@ packages/tools/convertThemesToCss.js
|
||||
packages/tools/generate-database-types.js
|
||||
packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/git-changelog.test.js
|
||||
packages/tools/licenseChecker.js
|
||||
packages/tools/release-android.js
|
||||
packages/tools/release-cli.js
|
||||
|
@@ -780,7 +780,6 @@ footer .bottom-links-row p {
|
||||
|
||||
#menu-mobile .social-links .social-link-mastodon,
|
||||
#menu-mobile .social-links .social-link-reddit,
|
||||
#menu-mobile .social-links .social-link-linkedin,
|
||||
#menu-mobile .social-links .social-link-patreon {
|
||||
display: none;
|
||||
}
|
||||
@@ -948,41 +947,6 @@ footer .bottom-links-row p {
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
MORE NARROW VIEW
|
||||
eg for Galaxy S9
|
||||
*****************************************************************/
|
||||
|
||||
@media (max-width: 580px) {
|
||||
|
||||
#nav-section .plans-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
MORE NARROW VIEW
|
||||
eg for Galaxy S9
|
||||
*****************************************************************/
|
||||
|
||||
@media (max-width: 400px) {
|
||||
|
||||
#nav-section .navbar-mobile-content a.sponsor-button .sponsor-button-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#nav-section .navbar-mobile-content a.sponsor-button {
|
||||
padding: 2px 6px;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
#nav-section a {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
VERY NARROW VIEW
|
||||
eg for Galaxy Fold
|
||||
@@ -1004,15 +968,6 @@ footer .bottom-links-row p {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
div.navbar-mobile-content a.sponsor-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#nav-section .button-link {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 238 KiB |
@@ -1,7 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Thu, 02 Mar 2023 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Thu, 02 Mar 2023 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin will participate in JdLL 2023!]]></title><description><![CDATA[<p>On 1 and 2 April 2023, we will have a stand for Joplin at the <a href="https://www.jdll.org/">Journées du Logiciel Libre</a> in Lyon, France. The JdLL has been taking place in Lyon for 24 years and is a popular open source conference in France. We had a stand in 2020 and 2021 but that was cancelled due to Covid, so this year is a first for Joplin!</p>
|
||||
<p>Admission is free, so don't hesitate to come and meet us, exchange ideas and learn more about Joplin!</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230202-jdll.jpg" alt="Joplin at JdLL 2023"></p>
|
||||
]]></description><link>https://joplinapp.org/news/20230302-jdll-2023/</link><guid isPermaLink="false">20230302-jdll-2023</guid><pubDate>Thu, 02 Mar 2023 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Introducing the "GitHub Actions Raw Log Viewer" extension for Chrome]]></title><description><![CDATA[<p>If you've ever used GitHub Actions, you will find that they provide by default a nice coloured output for the log. It looks good and it's even interactive! (You can click to collapse/expand blocks of text) But unfortunately it doesn't scale to large workflows, like we have for Joplin - the log can freeze and it will take forever to search for something. Indeed searching is done in "real time"... which mostly means it will freeze for a minute or two for each letter you type in the search box. Not great.</p>
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 16 Jan 2023 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 16 Jan 2023 00:00:00 GMT</pubDate><item><title><![CDATA[Introducing the "GitHub Actions Raw Log Viewer" extension for Chrome]]></title><description><![CDATA[<p>If you've ever used GitHub Actions, you will find that they provide by default a nice coloured output for the log. It looks good and it's even interactive! (You can click to collapse/expand blocks of text) But unfortunately it doesn't scale to large workflows, like we have for Joplin - the log can freeze and it will take forever to search for something. Indeed searching is done in "real time"... which mostly means it will freeze for a minute or two for each letter you type in the search box. Not great.</p>
|
||||
<p>Thankfully GitHub provides an alternative access: the raw logs. This is much better because they will open as plain text, without any styling or JS magic, which means you can use the browser native search and it will be fast.</p>
|
||||
<p>But now the problem is that raw logs look like this:</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-ga-raw-log.png" alt="Raw log without extension"></p>
|
||||
@@ -297,4 +294,9 @@
|
||||
<p>This release also includes about 30 various bug fixes and improvements.</p>
|
||||
<p>A notable one is a fix for GotoAnything, which recently wasn't working on first try.</p>
|
||||
<p>The plugin screen has also been improved so that search works even when GitHub is down or blocked, as it is in China in particular.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20210929-144036/</link><guid isPermaLink="false">20210929-144036</guid><pubDate>Wed, 29 Sep 2021 14:40:36 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20210929-144036/</link><guid isPermaLink="false">20210929-144036</guid><pubDate>Wed, 29 Sep 2021 14:40:36 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Introducing recommended plugins in the next Joplin version]]></title><description><![CDATA[<p>A common request from new users is how to know which plugin is safe to install or not. In fact probably all of them are safe but as a new user that's not necessarily easy to know. So to help with this, the next version of Joplin will support recommended plugins - those will be plugins that meet our standards of quality and performance, and they will be indicated by a small crown tag inside the plugin box. Recommended plugins will also appear on top when searching.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20210901-113415_0.png" alt=""></p>
|
||||
<p>For now, since we don't have a review process, the recommended plugins are those developed by the Joplin team and frequent contributors, because we know those are safe to use.</p>
|
||||
<p>Later we might have a review process and add more recommended plugins. That being said, in the meantime even if a plugin is not marked as recommended, there's a good chance it is still safe and have good performance too. Often you can search for it on the forum and if it's active with many users commenting, you're most likely good to go.</p>
|
||||
<p>But if there's any doubt, the recommended tag is a good way to be sure.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20210901-113415/</link><guid isPermaLink="false">20210901-113415</guid><pubDate>Wed, 01 Sep 2021 11:34:15 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
|
@@ -24,8 +24,7 @@
|
||||
</div>
|
||||
<div class="col-9 text-right d-block d-md-none navbar-mobile-content">
|
||||
{{> twitterLink}}
|
||||
<a href="{{baseUrl}}/cn/" class="fw500 chinese-page-link">中文</a>
|
||||
{{> joplinCloudButton}}
|
||||
<a href="{{baseUrl}}/cn/" class="fw500">中文</a>
|
||||
{{> supportButton}}
|
||||
|
||||
<span class="pointer"
|
||||
|
@@ -4,7 +4,6 @@
|
||||
<a class="social-link-mastodon" href="https://mastodon.social/@joplinapp" title="Joplin Mastodon feed"><i class="fab fa-mastodon"></i></a>
|
||||
<a class="social-link-patreon" href="https://www.patreon.com/joplin" title="Joplin Patreon"><i class="fab fa-patreon"></i></a>
|
||||
<a class="social-link-discord" href="https://discord.gg/VSj7AFHvpq" title="Joplin Discord chat"><i class="fab fa-discord"></i></a>
|
||||
<a class="social-link-linkedin" href="https://www.linkedin.com/company/joplin" title="Joplin LinkedIn Feed"><i class="fab fa-linkedin"></i></a>
|
||||
<a class="social-link-reddit" href="https://www.reddit.com/r/joplinapp/" title="Joplin Subreddit"><i class="fab fa-reddit"></i></a>
|
||||
<a class="social-link-github" href="https://github.com/laurent22/joplin/" title="Joplin GitHub repository"><i class="fab fa-github"></i></a>
|
||||
</div>
|
||||
|
@@ -1 +1 @@
|
||||
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500 twitter-link"><i class="fab fa-twitter"></i></a>
|
||||
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500"><i class="fab fa-twitter"></i></a>
|
14
README.md
14
README.md
@@ -72,11 +72,14 @@ A community maintained list of these distributions can be found here: [Unofficia
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
| | | | |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) | <img width="50" src="https://avatars2.githubusercontent.com/u/2793530?s=96&v=4"/></br>[CyberXZT](https://github.com/CyberXZT) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/49439044?s=96&v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) | <img width="50" src="https://avatars2.githubusercontent.com/u/3266447?s=96&v=4"/></br>[iamwillbar](https://github.com/iamwillbar) | <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/3061769?s=96&v=4"/></br>[c-nagy](https://github.com/c-nagy) | <img width="50" src="https://avatars2.githubusercontent.com/u/70780798?s=96&v=4"/></br>[cabottech](https://github.com/cabottech) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/4862947?s=96&v=4"/></br>[chrootlogin](https://github.com/chrootlogin) | <img width="50" src="https://avatars2.githubusercontent.com/u/82579431?s=96&v=4"/></br>[clmntsl](https://github.com/clmntsl) | <img width="50" src="https://avatars2.githubusercontent.com/u/808091?s=96&v=4"/></br>[cuongtransc](https://github.com/cuongtransc) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[fbloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/49439044?s=96&v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars2.githubusercontent.com/u/38898566?s=96&v=4"/></br>[h4sh5](https://github.com/h4sh5) | <img width="50" src="https://avatars2.githubusercontent.com/u/3266447?s=96&v=4"/></br>[iamwillbar](https://github.com/iamwillbar) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) | <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) | <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars2.githubusercontent.com/u/77214738?s=96&v=4"/></br>[Polymathic-Company](https://github.com/Polymathic-Company) | <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/54626606?s=96&v=4"/></br>[skyrunner15](https://github.com/skyrunner15) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) | | | |
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
|
||||
<!-- TOC -->
|
||||
@@ -502,7 +505,6 @@ Name | Description
|
||||
[Mastodon feed](https://mastodon.social/@joplinapp) | Follow us on Mastodon
|
||||
[Patreon page](https://www.patreon.com/joplin) |The latest news are often posted there
|
||||
[Discord server](https://discord.gg/VSj7AFHvpq) | Our chat server
|
||||
[LinkedIn](https://www.linkedin.com/company/joplin) | Our LinkedIn page
|
||||
[Sub-reddit](https://www.reddit.com/r/joplinapp/) | Also a good place to get help
|
||||
|
||||
# Contributing
|
||||
|
@@ -71,7 +71,6 @@
|
||||
"eslint": "8.31.0",
|
||||
"eslint-interactive": "10.3.0",
|
||||
"eslint-plugin-import": "2.27.4",
|
||||
"eslint-plugin-jest": "27.2.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-react": "7.32.0",
|
||||
"fs-extra": "11.1.0",
|
||||
@@ -89,7 +88,7 @@
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"http-server": "14.1.1",
|
||||
"node-gyp": "9.3.1",
|
||||
"nodemon": "2.0.21"
|
||||
"nodemon": "2.0.20"
|
||||
},
|
||||
"packageManager": "yarn@3.3.1",
|
||||
"resolutions": {
|
||||
|
@@ -6,14 +6,14 @@ interface LinkStoreEntry {
|
||||
}
|
||||
|
||||
class LinkSelector {
|
||||
private noteId_: string;
|
||||
private scrollTop_: number;
|
||||
private renderedText_: string;
|
||||
private currentLinkIndex_: number;
|
||||
private linkStore_: LinkStoreEntry[];
|
||||
private linkRegex_: RegExp;
|
||||
noteId_: string;
|
||||
scrollTop_: number;
|
||||
renderedText_: string;
|
||||
currentLinkIndex_: number;
|
||||
linkStore_: LinkStoreEntry[];
|
||||
linkRegex_: RegExp;
|
||||
|
||||
public constructor() {
|
||||
constructor() {
|
||||
this.noteId_ = null;
|
||||
this.scrollTop_ = null; // used so 'o' won't open unhighlighted link after scrolling
|
||||
this.renderedText_ = null;
|
||||
@@ -22,22 +22,22 @@ class LinkSelector {
|
||||
this.linkRegex_ = /http:\/\/[0-9.]+:[0-9]+\/[0-9]+/g;
|
||||
}
|
||||
|
||||
public get link(): string | null {
|
||||
get link(): string | null {
|
||||
if (this.currentLinkIndex_ === null) return null;
|
||||
return this.linkStore_[this.currentLinkIndex_].link;
|
||||
}
|
||||
|
||||
public get noteX(): number | null {
|
||||
get noteX(): number | null {
|
||||
if (this.currentLinkIndex_ === null) return null;
|
||||
return this.linkStore_[this.currentLinkIndex_].noteX;
|
||||
}
|
||||
|
||||
public get noteY(): number | null {
|
||||
get noteY(): number | null {
|
||||
if (this.currentLinkIndex_ === null) return null;
|
||||
return this.linkStore_[this.currentLinkIndex_].noteY;
|
||||
}
|
||||
|
||||
public findLinks(renderedText: string): LinkStoreEntry[] {
|
||||
findLinks(renderedText: string): LinkStoreEntry[] {
|
||||
const newLinkStore: LinkStoreEntry[] = [];
|
||||
const lines: string[] = renderedText.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -56,19 +56,19 @@ class LinkSelector {
|
||||
return newLinkStore;
|
||||
}
|
||||
|
||||
public updateText(renderedText: string): void {
|
||||
updateText(renderedText: string): void {
|
||||
this.currentLinkIndex_ = null;
|
||||
this.renderedText_ = renderedText;
|
||||
this.linkStore_ = this.findLinks(this.renderedText_);
|
||||
}
|
||||
|
||||
public updateNote(textWidget: any): void {
|
||||
updateNote(textWidget: any): void {
|
||||
this.noteId_ = textWidget.noteId;
|
||||
this.scrollTop_ = textWidget.scrollTop_;
|
||||
this.updateText(textWidget.renderedText_);
|
||||
}
|
||||
|
||||
public scrollWidget(textWidget: any): void {
|
||||
scrollWidget(textWidget: any): void {
|
||||
if (this.currentLinkIndex_ === null) return;
|
||||
|
||||
const noteY = this.linkStore_[this.currentLinkIndex_].noteY;
|
||||
@@ -93,7 +93,7 @@ class LinkSelector {
|
||||
return;
|
||||
}
|
||||
|
||||
public changeLink(textWidget: any, offset: number): void | null {
|
||||
changeLink(textWidget: any, offset: number): void | null {
|
||||
if (textWidget.noteId !== this.noteId_) {
|
||||
this.updateNote(textWidget);
|
||||
this.changeLink(textWidget, offset);
|
||||
@@ -123,7 +123,7 @@ class LinkSelector {
|
||||
return;
|
||||
}
|
||||
|
||||
public openLink(textWidget: any): void {
|
||||
openLink(textWidget: any): void {
|
||||
if (textWidget.noteId !== this.noteId_) return;
|
||||
if (textWidget.renderedText_ !== this.renderedText_) return;
|
||||
if (textWidget.scrollTop_ !== this.scrollTop_) return;
|
||||
|
@@ -7,80 +7,80 @@ export default class BaseCommand {
|
||||
protected prompt_: any = null;
|
||||
protected dispatcher_: any;
|
||||
|
||||
public usage(): string {
|
||||
usage(): string {
|
||||
throw new Error('Usage not defined');
|
||||
}
|
||||
|
||||
public encryptionCheck(item: any) {
|
||||
encryptionCheck(item: any) {
|
||||
if (item && item.encryption_applied) throw new Error(_('Cannot change encrypted item'));
|
||||
}
|
||||
|
||||
public description() {
|
||||
description() {
|
||||
throw new Error('Description not defined');
|
||||
}
|
||||
|
||||
public async action(_args: any) {
|
||||
async action(_args: any) {
|
||||
throw new Error('Action not defined');
|
||||
}
|
||||
|
||||
public compatibleUis() {
|
||||
compatibleUis() {
|
||||
return ['cli', 'gui'];
|
||||
}
|
||||
|
||||
public supportsUi(ui: string) {
|
||||
supportsUi(ui: string) {
|
||||
return this.compatibleUis().indexOf(ui) >= 0;
|
||||
}
|
||||
|
||||
public options(): any[] {
|
||||
options(): any[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public hidden() {
|
||||
hidden() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public enabled() {
|
||||
enabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public cancellable() {
|
||||
cancellable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async cancel() {}
|
||||
async cancel() {}
|
||||
|
||||
public name() {
|
||||
name() {
|
||||
const r = this.usage().split(' ');
|
||||
return r[0];
|
||||
}
|
||||
|
||||
public setDispatcher(fn: Function) {
|
||||
setDispatcher(fn: Function) {
|
||||
this.dispatcher_ = fn;
|
||||
}
|
||||
|
||||
public dispatch(action: any) {
|
||||
dispatch(action: any) {
|
||||
if (!this.dispatcher_) throw new Error('Dispatcher not defined');
|
||||
return this.dispatcher_(action);
|
||||
}
|
||||
|
||||
public setStdout(fn: Function) {
|
||||
setStdout(fn: Function) {
|
||||
this.stdout_ = fn;
|
||||
}
|
||||
|
||||
public stdout(text: string) {
|
||||
stdout(text: string) {
|
||||
if (this.stdout_) this.stdout_(text);
|
||||
}
|
||||
|
||||
public setPrompt(fn: Function) {
|
||||
setPrompt(fn: Function) {
|
||||
this.prompt_ = fn;
|
||||
}
|
||||
|
||||
public async prompt(message: string, options: any = null) {
|
||||
async prompt(message: string, options: any = null) {
|
||||
if (!this.prompt_) throw new Error('Prompt is undefined');
|
||||
return await this.prompt_(message, options);
|
||||
}
|
||||
|
||||
public metadata() {
|
||||
metadata() {
|
||||
return {
|
||||
name: this.name(),
|
||||
usage: this.usage(),
|
||||
@@ -89,7 +89,7 @@ export default class BaseCommand {
|
||||
};
|
||||
}
|
||||
|
||||
public logger() {
|
||||
logger() {
|
||||
return reg.logger();
|
||||
}
|
||||
}
|
||||
|
@@ -12,15 +12,15 @@ const imageType = require('image-type');
|
||||
const readChunk = require('read-chunk');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public usage() {
|
||||
usage() {
|
||||
return 'e2ee <command> [path]';
|
||||
}
|
||||
|
||||
public description() {
|
||||
description() {
|
||||
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.'); // `generate-ppk`
|
||||
}
|
||||
|
||||
public options() {
|
||||
options() {
|
||||
return [
|
||||
// This is here mostly for testing - shouldn't be used
|
||||
['-p, --password <password>', 'Use this password as master password (For security reasons, it is not recommended to use this option).'],
|
||||
@@ -30,7 +30,7 @@ class Command extends BaseCommand {
|
||||
];
|
||||
}
|
||||
|
||||
public async action(args: any) {
|
||||
async action(args: any) {
|
||||
const options = args.options;
|
||||
|
||||
const askForMasterKey = async (error: any) => {
|
||||
|
@@ -6,22 +6,22 @@ import Folder from '@joplin/lib/models/Folder';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public usage() {
|
||||
usage() {
|
||||
return 'mkbook <new-notebook>';
|
||||
}
|
||||
|
||||
public description() {
|
||||
description() {
|
||||
return _('Creates a new notebook.');
|
||||
}
|
||||
|
||||
public options() {
|
||||
options() {
|
||||
return [
|
||||
['-p, --parent <parent-notebook>', _('Create a new notebook under a parent notebook.')],
|
||||
];
|
||||
}
|
||||
|
||||
// validDestinationFolder check for presents and ambiguous folders
|
||||
public async validDestinationFolder(targetFolder: string) {
|
||||
async validDestinationFolder(targetFolder: string) {
|
||||
|
||||
const destinationFolder = await app().loadItem(BaseModel.TYPE_FOLDER, targetFolder);
|
||||
if (!destinationFolder) {
|
||||
@@ -36,14 +36,14 @@ class Command extends BaseCommand {
|
||||
return destinationFolder;
|
||||
}
|
||||
|
||||
public async saveAndSwitchFolder(newFolder: FolderEntity) {
|
||||
async saveAndSwitchFolder(newFolder: FolderEntity) {
|
||||
|
||||
const folder = await Folder.save(newFolder, { userSideValidation: true });
|
||||
app().switchCurrentFolder(folder);
|
||||
|
||||
}
|
||||
|
||||
public async action(args: any) {
|
||||
async action(args: any) {
|
||||
const targetFolder = args.options.parent;
|
||||
|
||||
const newFolder: FolderEntity = {
|
||||
|
@@ -23,19 +23,19 @@ function settingTypeToSchemaType(type: SettingItemType): string {
|
||||
}
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public usage() {
|
||||
usage() {
|
||||
return 'settingschema <file>';
|
||||
}
|
||||
|
||||
public description() {
|
||||
description() {
|
||||
return 'Build the setting schema file';
|
||||
}
|
||||
|
||||
public enabled() {
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async action(args: any) {
|
||||
async action(args: any) {
|
||||
const schema: Record<string, any> = {
|
||||
title: 'JSON schema for Joplin setting files',
|
||||
'$id': Setting.schemaUrl,
|
||||
|
@@ -21,15 +21,15 @@ class Command extends BaseCommand {
|
||||
private releaseLockFn_: Function = null;
|
||||
private oneDriveApiUtils_: any = null;
|
||||
|
||||
public usage() {
|
||||
usage() {
|
||||
return 'sync';
|
||||
}
|
||||
|
||||
public description() {
|
||||
description() {
|
||||
return _('Synchronises with remote storage.');
|
||||
}
|
||||
|
||||
public options() {
|
||||
options() {
|
||||
return [
|
||||
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
|
||||
['--upgrade', _('Upgrade the sync target to the latest version.')],
|
||||
@@ -45,7 +45,7 @@ class Command extends BaseCommand {
|
||||
return locker.check(filePath);
|
||||
}
|
||||
|
||||
public async doAuth() {
|
||||
async doAuth() {
|
||||
const syncTarget = reg.syncTarget(this.syncTargetId_);
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_);
|
||||
|
||||
@@ -89,18 +89,18 @@ class Command extends BaseCommand {
|
||||
return false;
|
||||
}
|
||||
|
||||
public cancelAuth() {
|
||||
cancelAuth() {
|
||||
if (this.oneDriveApiUtils_) {
|
||||
this.oneDriveApiUtils_.cancelOAuthDance();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public doingAuth() {
|
||||
doingAuth() {
|
||||
return !!this.oneDriveApiUtils_;
|
||||
}
|
||||
|
||||
public async action(args: any) {
|
||||
async action(args: any) {
|
||||
this.releaseLockFn_ = null;
|
||||
|
||||
// Lock is unique per profile/database
|
||||
@@ -238,7 +238,7 @@ class Command extends BaseCommand {
|
||||
cleanUp();
|
||||
}
|
||||
|
||||
public async cancel() {
|
||||
async cancel() {
|
||||
if (this.doingAuth()) {
|
||||
this.cancelAuth();
|
||||
return;
|
||||
@@ -263,7 +263,7 @@ class Command extends BaseCommand {
|
||||
this.syncTargetId_ = null;
|
||||
}
|
||||
|
||||
public cancellable() {
|
||||
cancellable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -18,19 +18,19 @@ function itemCount(args: any) {
|
||||
}
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public usage() {
|
||||
usage() {
|
||||
return 'testing <command> [arg0]';
|
||||
}
|
||||
|
||||
public description() {
|
||||
description() {
|
||||
return 'testing';
|
||||
}
|
||||
|
||||
public enabled() {
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public options(): any[] {
|
||||
options(): any[] {
|
||||
return [
|
||||
['--folder-count <count>', 'Folders to create'],
|
||||
['--note-count <count>', 'Notes to create'],
|
||||
@@ -40,7 +40,7 @@ class Command extends BaseCommand {
|
||||
];
|
||||
}
|
||||
|
||||
public async action(args: any) {
|
||||
async action(args: any) {
|
||||
const { command, options } = args;
|
||||
|
||||
if (command === 'populate') {
|
||||
|
@@ -5,7 +5,7 @@ const stripAnsi = require('strip-ansi');
|
||||
const { handleAutocompletion } = require('../autocompletion.js');
|
||||
|
||||
export default class StatusBarWidget extends BaseWidget {
|
||||
public constructor() {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.promptState_ = null;
|
||||
@@ -14,20 +14,20 @@ export default class StatusBarWidget extends BaseWidget {
|
||||
this.items_ = [];
|
||||
}
|
||||
|
||||
public get name() {
|
||||
get name() {
|
||||
return 'statusBar';
|
||||
}
|
||||
|
||||
public get canHaveFocus() {
|
||||
get canHaveFocus() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public setItemAt(index: number, text: string) {
|
||||
setItemAt(index: number, text: string) {
|
||||
this.items_[index] = stripAnsi(text).trim();
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
public async prompt(initialText = '', promptString: any = null, options: any = null) {
|
||||
async prompt(initialText = '', promptString: any = null, options: any = null) {
|
||||
if (this.promptState_) throw new Error('Another prompt already active');
|
||||
if (promptString === null) promptString = ':';
|
||||
if (options === null) options = {};
|
||||
@@ -53,15 +53,15 @@ export default class StatusBarWidget extends BaseWidget {
|
||||
return this.promptState_.promise;
|
||||
}
|
||||
|
||||
public get promptActive() {
|
||||
get promptActive() {
|
||||
return !!this.promptState_;
|
||||
}
|
||||
|
||||
public get history() {
|
||||
get history() {
|
||||
return this.history_;
|
||||
}
|
||||
|
||||
public resetCursor() {
|
||||
resetCursor() {
|
||||
if (!this.promptActive) return;
|
||||
if (!this.inputEventEmitter_) return;
|
||||
|
||||
@@ -70,7 +70,7 @@ export default class StatusBarWidget extends BaseWidget {
|
||||
this.term.moveTo(this.absoluteInnerX + termutils.textLength(this.promptState_.promptString) + this.inputEventEmitter_.getInput().length, this.absoluteInnerY);
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
super.render();
|
||||
|
||||
const doSaveCursor = !this.promptActive;
|
||||
|
@@ -35,7 +35,7 @@ export default class PluginRunner extends BasePluginRunner {
|
||||
private eventHandlers_: EventHandlers = {};
|
||||
private activeSandboxCalls_: any = {};
|
||||
|
||||
public constructor() {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.eventHandler = this.eventHandler.bind(this);
|
||||
@@ -64,7 +64,7 @@ export default class PluginRunner extends BasePluginRunner {
|
||||
};
|
||||
}
|
||||
|
||||
public async run(plugin: Plugin, sandbox: Global): Promise<void> {
|
||||
async run(plugin: Plugin, sandbox: Global): Promise<void> {
|
||||
return new Promise((resolve: Function, reject: Function) => {
|
||||
const onStarted = () => {
|
||||
plugin.off('started', onStarted);
|
||||
|
@@ -51,7 +51,7 @@
|
||||
"keytar": "7.9.0",
|
||||
"md5": "2.3.0",
|
||||
"node-rsa": "1.1.1",
|
||||
"open": "8.4.2",
|
||||
"open": "8.4.1",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"read-chunk": "2.1.0",
|
||||
"server-destroy": "1.0.1",
|
||||
|
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
|
||||
import KeychainService from '@joplin/lib/services/keychain/KeychainService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
@@ -33,7 +33,7 @@ export default class ElectronAppWrapper {
|
||||
private pluginWindows_: PluginWindows = {};
|
||||
private initialCallbackUrl_: string = null;
|
||||
|
||||
public constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean, initialCallbackUrl: string) {
|
||||
constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean, initialCallbackUrl: string) {
|
||||
this.electronApp_ = electronApp;
|
||||
this.env_ = env;
|
||||
this.isDebugMode_ = isDebugMode;
|
||||
@@ -41,31 +41,31 @@ export default class ElectronAppWrapper {
|
||||
this.initialCallbackUrl_ = initialCallbackUrl;
|
||||
}
|
||||
|
||||
public electronApp() {
|
||||
electronApp() {
|
||||
return this.electronApp_;
|
||||
}
|
||||
|
||||
public setLogger(v: Logger) {
|
||||
setLogger(v: Logger) {
|
||||
this.logger_ = v;
|
||||
}
|
||||
|
||||
public logger() {
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
public window() {
|
||||
window() {
|
||||
return this.win_;
|
||||
}
|
||||
|
||||
public env() {
|
||||
env() {
|
||||
return this.env_;
|
||||
}
|
||||
|
||||
public initialCallbackUrl() {
|
||||
initialCallbackUrl() {
|
||||
return this.initialCallbackUrl_;
|
||||
}
|
||||
|
||||
public createWindow() {
|
||||
createWindow() {
|
||||
// Set to true to view errors if the application does not start
|
||||
const debugEarlyBugs = this.env_ === 'dev' || this.isDebugMode_;
|
||||
|
||||
@@ -236,11 +236,11 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
public registerPluginWindow(pluginId: string, window: any) {
|
||||
registerPluginWindow(pluginId: string, window: any) {
|
||||
this.pluginWindows_[pluginId] = window;
|
||||
}
|
||||
|
||||
public async waitForElectronAppReady() {
|
||||
async waitForElectronAppReady() {
|
||||
if (this.electronApp().isReady()) return Promise.resolve();
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
@@ -253,25 +253,25 @@ export default class ElectronAppWrapper {
|
||||
});
|
||||
}
|
||||
|
||||
public quit() {
|
||||
quit() {
|
||||
this.electronApp_.quit();
|
||||
}
|
||||
|
||||
public exit(errorCode = 0) {
|
||||
exit(errorCode = 0) {
|
||||
this.electronApp_.exit(errorCode);
|
||||
}
|
||||
|
||||
public trayShown() {
|
||||
trayShown() {
|
||||
return !!this.tray_;
|
||||
}
|
||||
|
||||
// This method is used in macOS only to hide the whole app (and not just the main window)
|
||||
// including the menu bar. This follows the macOS way of hiding an app.
|
||||
public hide() {
|
||||
hide() {
|
||||
this.electronApp_.hide();
|
||||
}
|
||||
|
||||
public buildDir() {
|
||||
buildDir() {
|
||||
if (this.buildDir_) return this.buildDir_;
|
||||
let dir = `${__dirname}/build`;
|
||||
if (!fs.pathExistsSync(dir)) {
|
||||
@@ -283,7 +283,7 @@ export default class ElectronAppWrapper {
|
||||
return dir;
|
||||
}
|
||||
|
||||
private trayIconFilename_() {
|
||||
trayIconFilename_() {
|
||||
let output = '';
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
@@ -298,7 +298,7 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
|
||||
// Note: this must be called only after the "ready" event of the app has been dispatched
|
||||
public createTray(contextMenu: any) {
|
||||
createTray(contextMenu: any) {
|
||||
try {
|
||||
this.tray_ = new Tray(`${this.buildDir()}/icons/${this.trayIconFilename_()}`);
|
||||
this.tray_.setToolTip(this.electronApp_.name);
|
||||
@@ -312,13 +312,13 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
public destroyTray() {
|
||||
destroyTray() {
|
||||
if (!this.tray_) return;
|
||||
this.tray_.destroy();
|
||||
this.tray_ = null;
|
||||
}
|
||||
|
||||
public ensureSingleInstance() {
|
||||
ensureSingleInstance() {
|
||||
if (this.env_ === 'dev') return false;
|
||||
|
||||
const gotTheLock = this.electronApp_.requestSingleInstanceLock();
|
||||
@@ -347,7 +347,7 @@ export default class ElectronAppWrapper {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async start() {
|
||||
async start() {
|
||||
// Since we are doing other async things before creating the window, we might miss
|
||||
// the "ready" event. So we use the function below to make sure that the app is ready.
|
||||
await this.waitForElectronAppReady();
|
||||
@@ -375,7 +375,7 @@ export default class ElectronAppWrapper {
|
||||
});
|
||||
}
|
||||
|
||||
public async openCallbackUrl(url: string) {
|
||||
async openCallbackUrl(url: string) {
|
||||
this.win_.webContents.send('asynchronous-message', 'openCallbackUrl', {
|
||||
url: url,
|
||||
});
|
||||
|
@@ -3,7 +3,7 @@ import appReducer, { createAppDefaultState } from './app.reducer';
|
||||
|
||||
describe('app.reducer', () => {
|
||||
|
||||
it('should handle DIALOG_OPEN', async () => {
|
||||
it('DIALOG_OPEN', async () => {
|
||||
const state: AppState = createAppDefaultState({}, {});
|
||||
|
||||
let newState = appReducer(state, {
|
||||
|
@@ -21,7 +21,7 @@ export class Bridge {
|
||||
private electronWrapper_: ElectronAppWrapper;
|
||||
private lastSelectedPaths_: LastSelectedPath;
|
||||
|
||||
public constructor(electronWrapper: ElectronAppWrapper) {
|
||||
constructor(electronWrapper: ElectronAppWrapper) {
|
||||
this.electronWrapper_ = electronWrapper;
|
||||
this.lastSelectedPaths_ = {
|
||||
file: null,
|
||||
@@ -29,11 +29,11 @@ export class Bridge {
|
||||
};
|
||||
}
|
||||
|
||||
public electronApp() {
|
||||
electronApp() {
|
||||
return this.electronWrapper_;
|
||||
}
|
||||
|
||||
public electronIsDev() {
|
||||
electronIsDev() {
|
||||
return !this.electronApp().electronApp().isPackaged;
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ export class Bridge {
|
||||
return `${__dirname}/vendor`;
|
||||
}
|
||||
|
||||
public env() {
|
||||
env() {
|
||||
return this.electronWrapper_.env();
|
||||
}
|
||||
|
||||
public processArgv() {
|
||||
processArgv() {
|
||||
return process.argv;
|
||||
}
|
||||
|
||||
@@ -114,44 +114,44 @@ export class Bridge {
|
||||
});
|
||||
}
|
||||
|
||||
public window() {
|
||||
window() {
|
||||
return this.electronWrapper_.window();
|
||||
}
|
||||
|
||||
public showItemInFolder(fullPath: string) {
|
||||
showItemInFolder(fullPath: string) {
|
||||
return require('electron').shell.showItemInFolder(toSystemSlashes(fullPath));
|
||||
}
|
||||
|
||||
public newBrowserWindow(options: any) {
|
||||
newBrowserWindow(options: any) {
|
||||
return new BrowserWindow(options);
|
||||
}
|
||||
|
||||
public windowContentSize() {
|
||||
windowContentSize() {
|
||||
if (!this.window()) return { width: 0, height: 0 };
|
||||
const s = this.window().getContentSize();
|
||||
return { width: s[0], height: s[1] };
|
||||
}
|
||||
|
||||
public windowSize() {
|
||||
windowSize() {
|
||||
if (!this.window()) return { width: 0, height: 0 };
|
||||
const s = this.window().getSize();
|
||||
return { width: s[0], height: s[1] };
|
||||
}
|
||||
|
||||
public windowSetSize(width: number, height: number) {
|
||||
windowSetSize(width: number, height: number) {
|
||||
if (!this.window()) return;
|
||||
return this.window().setSize(width, height);
|
||||
}
|
||||
|
||||
public openDevTools() {
|
||||
openDevTools() {
|
||||
return this.window().webContents.openDevTools();
|
||||
}
|
||||
|
||||
public closeDevTools() {
|
||||
closeDevTools() {
|
||||
return this.window().webContents.closeDevTools();
|
||||
}
|
||||
|
||||
public async showSaveDialog(options: any) {
|
||||
async showSaveDialog(options: any) {
|
||||
const { dialog } = require('electron');
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPaths_.file) options.defaultPath = this.lastSelectedPaths_.file;
|
||||
@@ -162,7 +162,7 @@ export class Bridge {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public async showOpenDialog(options: OpenDialogOptions = null) {
|
||||
async showOpenDialog(options: OpenDialogOptions = null) {
|
||||
const { dialog } = require('electron');
|
||||
if (!options) options = {};
|
||||
let fileType = 'file';
|
||||
@@ -177,13 +177,13 @@ export class Bridge {
|
||||
}
|
||||
|
||||
// Don't use this directly - call one of the showXxxxxxxMessageBox() instead
|
||||
private showMessageBox_(window: any, options: any): number {
|
||||
showMessageBox_(window: any, options: any): number {
|
||||
const { dialog } = require('electron');
|
||||
if (!window) window = this.window();
|
||||
return dialog.showMessageBoxSync(window, options);
|
||||
}
|
||||
|
||||
public showErrorMessageBox(message: string) {
|
||||
showErrorMessageBox(message: string) {
|
||||
return this.showMessageBox_(this.window(), {
|
||||
type: 'error',
|
||||
message: message,
|
||||
@@ -191,7 +191,7 @@ export class Bridge {
|
||||
});
|
||||
}
|
||||
|
||||
public showConfirmMessageBox(message: string, options: any = null) {
|
||||
showConfirmMessageBox(message: string, options: any = null) {
|
||||
options = {
|
||||
buttons: [_('OK'), _('Cancel')],
|
||||
...options,
|
||||
@@ -208,7 +208,7 @@ export class Bridge {
|
||||
}
|
||||
|
||||
/* returns the index of the clicked button */
|
||||
public showMessageBox(message: string, options: any = null) {
|
||||
showMessageBox(message: string, options: any = null) {
|
||||
if (options === null) options = {};
|
||||
|
||||
const result = this.showMessageBox_(this.window(), Object.assign({}, {
|
||||
@@ -220,7 +220,7 @@ export class Bridge {
|
||||
return result;
|
||||
}
|
||||
|
||||
public showInfoMessageBox(message: string, options: any = {}) {
|
||||
showInfoMessageBox(message: string, options: any = {}) {
|
||||
const result = this.showMessageBox_(this.window(), Object.assign({}, {
|
||||
type: 'info',
|
||||
message: message,
|
||||
@@ -229,35 +229,35 @@ export class Bridge {
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
public setLocale(locale: string) {
|
||||
setLocale(locale: string) {
|
||||
setLocale(locale);
|
||||
}
|
||||
|
||||
public get Menu() {
|
||||
get Menu() {
|
||||
return require('electron').Menu;
|
||||
}
|
||||
|
||||
public get MenuItem() {
|
||||
get MenuItem() {
|
||||
return require('electron').MenuItem;
|
||||
}
|
||||
|
||||
public openExternal(url: string) {
|
||||
openExternal(url: string) {
|
||||
return require('electron').shell.openExternal(url);
|
||||
}
|
||||
|
||||
public async openItem(fullPath: string) {
|
||||
async openItem(fullPath: string) {
|
||||
return require('electron').shell.openPath(toSystemSlashes(fullPath));
|
||||
}
|
||||
|
||||
public screen() {
|
||||
screen() {
|
||||
return require('electron').screen;
|
||||
}
|
||||
|
||||
public shouldUseDarkColors() {
|
||||
shouldUseDarkColors() {
|
||||
return nativeTheme.shouldUseDarkColors;
|
||||
}
|
||||
|
||||
public addEventListener(name: string, fn: Function) {
|
||||
addEventListener(name: string, fn: Function) {
|
||||
if (name === 'nativeThemeUpdated') {
|
||||
nativeTheme.on('updated', fn);
|
||||
} else {
|
||||
@@ -265,7 +265,7 @@ export class Bridge {
|
||||
}
|
||||
}
|
||||
|
||||
public restart(linuxSafeRestart = true) {
|
||||
restart(linuxSafeRestart = true) {
|
||||
// Note that in this case we are not sending the "appClose" event
|
||||
// to notify services and component that the app is about to close
|
||||
// but for the current use-case it's not really needed.
|
||||
|
@@ -2,6 +2,7 @@ const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { clipboard } = require('electron');
|
||||
import ExtensionBadge from './ExtensionBadge';
|
||||
import bridge from '../services/bridge';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import ClipperServer from '@joplin/lib/ClipperServer';
|
||||
@@ -10,29 +11,37 @@ import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import { AppState } from '../app.reducer';
|
||||
|
||||
class ClipperConfigScreenComponent extends React.Component {
|
||||
public constructor() {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.copyToken_click = this.copyToken_click.bind(this);
|
||||
}
|
||||
|
||||
private disableClipperServer_click() {
|
||||
disableClipperServer_click() {
|
||||
Setting.setValue('clipperServer.autoStart', false);
|
||||
void ClipperServer.instance().stop();
|
||||
}
|
||||
|
||||
private enableClipperServer_click() {
|
||||
enableClipperServer_click() {
|
||||
Setting.setValue('clipperServer.autoStart', true);
|
||||
void ClipperServer.instance().start();
|
||||
}
|
||||
|
||||
private copyToken_click() {
|
||||
chromeButton_click() {
|
||||
void bridge().openExternal('https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek');
|
||||
}
|
||||
|
||||
firefoxButton_click() {
|
||||
void bridge().openExternal('https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/');
|
||||
}
|
||||
|
||||
copyToken_click() {
|
||||
clipboard.writeText(this.props.apiToken);
|
||||
|
||||
alert(_('Token has been copied to the clipboard!'));
|
||||
}
|
||||
|
||||
private renewToken_click() {
|
||||
renewToken_click() {
|
||||
if (confirm(_('Are you sure you want to renew the authorisation token?'))) {
|
||||
void EncryptionService.instance()
|
||||
.generateApiToken()
|
||||
@@ -43,7 +52,7 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const containerStyle = Object.assign({}, theme.containerStyle, {
|
||||
|
@@ -26,9 +26,9 @@ const settingKeyToControl: any = {
|
||||
|
||||
class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
private rowStyle_: any = null;
|
||||
rowStyle_: any = null;
|
||||
|
||||
public constructor(props: any) {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
shared.init(this, reg);
|
||||
@@ -55,15 +55,15 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
this.handleSettingButton = this.handleSettingButton.bind(this);
|
||||
}
|
||||
|
||||
private async checkSyncConfig_() {
|
||||
async checkSyncConfig_() {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
componentDidMount() {
|
||||
if (this.props.defaultSection) {
|
||||
this.setState({ selectedSectionName: this.props.defaultSection }, () => {
|
||||
this.switchSection(this.props.defaultSection);
|
||||
@@ -93,7 +93,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
}
|
||||
}
|
||||
|
||||
public sectionByName(name: string) {
|
||||
sectionByName(name: string) {
|
||||
const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings });
|
||||
for (const section of sections) {
|
||||
if (section.name === name) return section;
|
||||
@@ -102,7 +102,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
throw new Error(`Invalid section name: ${name}`);
|
||||
}
|
||||
|
||||
public screenFromName(screenName: string) {
|
||||
screenFromName(screenName: string) {
|
||||
if (screenName === 'encryption') return <EncryptionConfigScreen/>;
|
||||
if (screenName === 'server') return <ClipperConfigScreen themeId={this.props.themeId}/>;
|
||||
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.themeId}/>;
|
||||
@@ -110,7 +110,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
throw new Error(`Invalid screen name: ${screenName}`);
|
||||
}
|
||||
|
||||
public switchSection(name: string) {
|
||||
switchSection(name: string) {
|
||||
const section = this.sectionByName(name);
|
||||
let screenName = '';
|
||||
if (section.isScreen) {
|
||||
@@ -125,11 +125,11 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
this.setState({ selectedSectionName: section.name, screenName: screenName });
|
||||
}
|
||||
|
||||
private sidebar_selectionChange(event: any) {
|
||||
sidebar_selectionChange(event: any) {
|
||||
this.switchSection(event.section.name);
|
||||
}
|
||||
|
||||
public renderSectionDescription(section: any) {
|
||||
renderSectionDescription(section: any) {
|
||||
const description = Setting.sectionDescription(section.name);
|
||||
if (!description) return null;
|
||||
|
||||
@@ -141,7 +141,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
);
|
||||
}
|
||||
|
||||
public sectionToComponent(key: string, section: any, settings: any, selected: boolean) {
|
||||
sectionToComponent(key: string, section: any, settings: any, selected: boolean) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const createSettingComponents = (advanced: boolean) => {
|
||||
@@ -284,7 +284,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
return description ? <div style={this.descriptionStyle(themeId)}>{description}</div> : null;
|
||||
}
|
||||
|
||||
public settingToComponent(key: string, value: any) {
|
||||
settingToComponent(key: string, value: any) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const output: any = null;
|
||||
@@ -657,26 +657,26 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
}
|
||||
}
|
||||
|
||||
public async onApplyClick() {
|
||||
async onApplyClick() {
|
||||
shared.saveSettings(this);
|
||||
await this.checkNeedRestart();
|
||||
}
|
||||
|
||||
public async onSaveClick() {
|
||||
async onSaveClick() {
|
||||
shared.saveSettings(this);
|
||||
await this.checkNeedRestart();
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
}
|
||||
|
||||
public onCancelClick() {
|
||||
onCancelClick() {
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
}
|
||||
|
||||
public hasChanges() {
|
||||
hasChanges() {
|
||||
return !!this.state.changedSettingKeys.length;
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const style = Object.assign({},
|
||||
|
@@ -13,19 +13,19 @@ interface Props {
|
||||
|
||||
class DropboxLoginScreenComponent extends React.Component<any, any> {
|
||||
|
||||
private shared_: any;
|
||||
shared_: any;
|
||||
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.shared_ = new Shared(this, (msg: string) => bridge().showInfoMessageBox(msg), (msg: string) => bridge().showErrorMessageBox(msg));
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.shared_.refreshUrl();
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
|
@@ -32,7 +32,7 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
|
||||
public state: State = { error: null, errorInfo: null, pluginInfos: [], plugins: {} };
|
||||
|
||||
public componentDidCatch(error: any, errorInfo: ErrorInfo) {
|
||||
componentDidCatch(error: any, errorInfo: ErrorInfo) {
|
||||
if (typeof error === 'string') error = { message: error };
|
||||
|
||||
const pluginInfos: PluginInfo[] = [];
|
||||
@@ -58,7 +58,7 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
this.setState({ error, errorInfo, pluginInfos, plugins });
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
componentDidMount() {
|
||||
const onAppClose = () => {
|
||||
ipcRenderer.send('asynchronous-message', 'appCloseReply', {
|
||||
canClose: true,
|
||||
@@ -68,12 +68,12 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
ipcRenderer.on('appClose', onAppClose);
|
||||
}
|
||||
|
||||
public renderMessage() {
|
||||
renderMessage() {
|
||||
const message = this.props.message || 'Joplin encountered a fatal error and could not continue.';
|
||||
return <p>{message}</p>;
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
const safeMode_click = async () => {
|
||||
Setting.setValue('isSafeMode', true);
|
||||
|
@@ -11,17 +11,17 @@ interface Props {
|
||||
}
|
||||
|
||||
class HelpButtonComponent extends React.Component<Props> {
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
public onClick() {
|
||||
onClick() {
|
||||
if (this.props.onClick) this.props.onClick();
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = Object.assign({}, this.props.style, { color: theme.color, textDecoration: 'none' });
|
||||
const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 };
|
||||
|
@@ -9,7 +9,7 @@ interface Props {
|
||||
}
|
||||
|
||||
class IconButton extends React.Component<Props> {
|
||||
public render() {
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const iconStyle = {
|
||||
|
@@ -24,7 +24,7 @@ interface State {
|
||||
}
|
||||
|
||||
class ImportScreenComponent extends React.Component<Props, State> {
|
||||
public UNSAFE_componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
doImport: true,
|
||||
filePath: this.props.filePath,
|
||||
@@ -32,7 +32,7 @@ class ImportScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
if (newProps.filePath) {
|
||||
this.setState(
|
||||
{
|
||||
@@ -47,13 +47,13 @@ class ImportScreenComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
componentDidMount() {
|
||||
if (this.state.filePath && this.state.doImport) {
|
||||
void this.doImport();
|
||||
}
|
||||
}
|
||||
|
||||
public addMessage(key: string, text: string) {
|
||||
addMessage(key: string, text: string) {
|
||||
const messages = this.state.messages.slice();
|
||||
|
||||
messages.push({ key: key, text: text });
|
||||
@@ -61,7 +61,7 @@ class ImportScreenComponent extends React.Component<Props, State> {
|
||||
this.setState({ messages: messages });
|
||||
}
|
||||
|
||||
public uniqueMessages() {
|
||||
uniqueMessages() {
|
||||
const output = [];
|
||||
const messages = this.state.messages.slice();
|
||||
const foundKeys = [];
|
||||
@@ -74,7 +74,7 @@ class ImportScreenComponent extends React.Component<Props, State> {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async doImport() {
|
||||
async doImport() {
|
||||
const filePath = this.props.filePath;
|
||||
const folderTitle = await Folder.findUniqueItemTitle(filename(filePath));
|
||||
|
||||
@@ -109,7 +109,7 @@ class ImportScreenComponent extends React.Component<Props, State> {
|
||||
this.setState({ doImport: false });
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const messages = this.uniqueMessages();
|
||||
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const logger = Logger.create('ItemList');
|
||||
|
||||
interface Props {
|
||||
style: any;
|
||||
@@ -24,7 +21,7 @@ class ItemList extends React.Component<Props, State> {
|
||||
private scrollTop_: number;
|
||||
private listRef: any;
|
||||
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.scrollTop_ = 0;
|
||||
@@ -36,12 +33,12 @@ class ItemList extends React.Component<Props, State> {
|
||||
this.onDrop = this.onDrop.bind(this);
|
||||
}
|
||||
|
||||
public visibleItemCount(props: Props = undefined) {
|
||||
visibleItemCount(props: Props = undefined) {
|
||||
if (typeof props === 'undefined') props = this.props;
|
||||
return Math.ceil(props.style.height / props.itemHeight);
|
||||
}
|
||||
|
||||
public updateStateItemIndexes(props: Props = undefined) {
|
||||
updateStateItemIndexes(props: Props = undefined) {
|
||||
if (typeof props === 'undefined') props = this.props;
|
||||
|
||||
const topItemIndex = Math.floor(this.scrollTop_ / props.itemHeight);
|
||||
@@ -50,66 +47,42 @@ class ItemList extends React.Component<Props, State> {
|
||||
let bottomItemIndex = topItemIndex + (visibleItemCount - 1);
|
||||
if (bottomItemIndex >= props.items.length) bottomItemIndex = props.items.length - 1;
|
||||
|
||||
// EDGE CASE:
|
||||
// ref: https://github.com/laurent22/joplin/issues/4124
|
||||
// when the note list is hidden, visibleItemCount is negative, and scroll top is positive when a note is selected
|
||||
if (visibleItemCount < 0 && this.scrollTop_ > 0) {
|
||||
logger.warn('Resetting scrollTop to 0. visibleItemCount is negative, scrollTop is positive.');
|
||||
// we will reset the scroll top so that there is no blank space at the top of note list
|
||||
this.scrollTop_ = 0;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
topItemIndex: topItemIndex,
|
||||
bottomItemIndex: bottomItemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
public offsetTop() {
|
||||
offsetTop() {
|
||||
return this.listRef.current ? this.listRef.current.offsetTop : 0;
|
||||
}
|
||||
|
||||
public offsetScroll() {
|
||||
offsetScroll() {
|
||||
return this.scrollTop_;
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
this.updateStateItemIndexes(newProps);
|
||||
}
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
// EDGE CASE
|
||||
// scroll top is not updated when item list visibility is toggled
|
||||
// if the user was at the bottom of the item list before hiding, blank spaces are added at the bottom of the item list
|
||||
if (this.offsetScroll() !== this.listRef.current?.scrollTop) {
|
||||
logger.warn(`scrollTop mismatch. Updating scrollTop with current listRef scrollTop(${this.listRef.current.scrollTop})`);
|
||||
// update scroll postion once if there is a mismatch in scroll position after showing item list
|
||||
this.onScroll({
|
||||
target: {
|
||||
scrollTop: this.listRef.current.scrollTop,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onScroll(event: any) {
|
||||
onScroll(event: any) {
|
||||
this.scrollTop_ = event.target.scrollTop;
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
public onKeyDown(event: any) {
|
||||
onKeyDown(event: any) {
|
||||
if (this.props.onKeyDown) this.props.onKeyDown(event);
|
||||
}
|
||||
|
||||
public onDrop(event: any) {
|
||||
onDrop(event: any) {
|
||||
if (this.props.onNoteDrop) this.props.onNoteDrop(event);
|
||||
}
|
||||
|
||||
public makeItemIndexVisible(itemIndex: number) {
|
||||
makeItemIndexVisible(itemIndex: number) {
|
||||
const top = Math.min(this.props.items.length - 1, this.state.topItemIndex);
|
||||
const bottom = Math.max(0, this.state.bottomItemIndex);
|
||||
|
||||
@@ -146,7 +119,7 @@ class ItemList extends React.Component<Props, State> {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const items = this.props.items;
|
||||
const style = Object.assign({}, this.props.style, {
|
||||
overflowX: 'hidden',
|
||||
|
@@ -123,7 +123,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
private styles_: any;
|
||||
private promptOnClose_: Function;
|
||||
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@@ -250,11 +250,11 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
return this.updateLayoutPluginViews(output, plugins);
|
||||
}
|
||||
|
||||
private window_resize() {
|
||||
window_resize() {
|
||||
this.updateRootLayoutSize();
|
||||
}
|
||||
|
||||
public setupAppCloseHandling() {
|
||||
setupAppCloseHandling() {
|
||||
this.waitForNotesSavedIID_ = null;
|
||||
|
||||
// This event is dispached from the main process when the app is about
|
||||
@@ -289,11 +289,11 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
private notePropertiesDialog_close() {
|
||||
notePropertiesDialog_close() {
|
||||
this.setState({ notePropertiesDialogOptions: {} });
|
||||
}
|
||||
|
||||
private noteContentPropertiesDialog_close() {
|
||||
noteContentPropertiesDialog_close() {
|
||||
this.setState({ noteContentPropertiesDialogOptions: {} });
|
||||
}
|
||||
|
||||
@@ -305,14 +305,14 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
this.setState({ shareFolderDialogOptions: { visible: false, folderId: '' } });
|
||||
}
|
||||
|
||||
public updateMainLayout(layout: LayoutItem) {
|
||||
updateMainLayout(layout: LayoutItem) {
|
||||
this.props.dispatch({
|
||||
type: 'MAIN_LAYOUT_SET',
|
||||
value: layout,
|
||||
});
|
||||
}
|
||||
|
||||
public updateRootLayoutSize() {
|
||||
updateRootLayoutSize() {
|
||||
this.updateMainLayout(produce(this.props.mainLayout, (draft: any) => {
|
||||
const s = this.rootLayoutSize();
|
||||
draft.width = s.width;
|
||||
@@ -320,7 +320,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
}));
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (prevProps.style.width !== this.props.style.width ||
|
||||
prevProps.style.height !== this.props.style.height ||
|
||||
this.messageBoxVisible(prevProps) !== this.messageBoxVisible(this.props)
|
||||
@@ -383,24 +383,24 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
public layoutModeListenerKeyDown(event: any) {
|
||||
layoutModeListenerKeyDown(event: any) {
|
||||
if (event.key !== 'Escape') return;
|
||||
if (!this.props.layoutMoveMode) return;
|
||||
void CommandService.instance().execute('toggleLayoutMoveMode');
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
componentDidMount() {
|
||||
window.addEventListener('keydown', this.layoutModeListenerKeyDown);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
componentWillUnmount() {
|
||||
this.unregisterCommands();
|
||||
|
||||
window.removeEventListener('resize', this.window_resize);
|
||||
window.removeEventListener('keydown', this.layoutModeListenerKeyDown);
|
||||
}
|
||||
|
||||
public async waitForNoteToSaved(noteId: string) {
|
||||
async waitForNoteToSaved(noteId: string) {
|
||||
while (noteId && this.props.editorNoteStatuses[noteId] === 'saving') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Waiting for note to be saved...', this.props.editorNoteStatuses);
|
||||
@@ -408,7 +408,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
public async printTo_(target: string, options: any) {
|
||||
async printTo_(target: string, options: any) {
|
||||
// Concurrent print calls are disallowed to avoid incorrect settings being restored upon completion
|
||||
if (this.isPrinting_) {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -449,23 +449,23 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
this.isPrinting_ = false;
|
||||
}
|
||||
|
||||
public rootLayoutSize() {
|
||||
rootLayoutSize() {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: this.rowHeight(),
|
||||
};
|
||||
}
|
||||
|
||||
public rowHeight() {
|
||||
rowHeight() {
|
||||
if (!this.props) return 0;
|
||||
return this.props.style.height - (this.messageBoxVisible() ? this.messageBoxHeight() : 0);
|
||||
}
|
||||
|
||||
public messageBoxHeight() {
|
||||
messageBoxHeight() {
|
||||
return 50;
|
||||
}
|
||||
|
||||
public styles(themeId: number, width: number, height: number, messageBoxVisible: boolean) {
|
||||
styles(themeId: number, width: number, height: number, messageBoxVisible: boolean) {
|
||||
const styleKey = [themeId, width, height, messageBoxVisible].join('_');
|
||||
if (styleKey === this.styleKey_) return this.styles_;
|
||||
|
||||
@@ -539,7 +539,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
public renderNotification(theme: any, styles: any) {
|
||||
renderNotification(theme: any, styles: any) {
|
||||
if (!this.messageBoxVisible()) return null;
|
||||
|
||||
const onViewStatusScreen = () => {
|
||||
@@ -658,33 +658,33 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
public messageBoxVisible(props: Props = null) {
|
||||
messageBoxVisible(props: Props = null) {
|
||||
if (!props) props = this.props;
|
||||
return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode || this.showShareInvitationNotification(props) || this.props.needApiAuth || this.props.showInstallTemplatesPlugin;
|
||||
}
|
||||
|
||||
public registerCommands() {
|
||||
registerCommands() {
|
||||
for (const command of commands) {
|
||||
CommandService.instance().registerRuntime(command.declaration.name, command.runtime(this));
|
||||
}
|
||||
}
|
||||
|
||||
public unregisterCommands() {
|
||||
unregisterCommands() {
|
||||
for (const command of commands) {
|
||||
CommandService.instance().unregisterRuntime(command.declaration.name);
|
||||
}
|
||||
}
|
||||
|
||||
private resizableLayout_resize(event: any) {
|
||||
resizableLayout_resize(event: any) {
|
||||
this.updateMainLayout(event.layout);
|
||||
}
|
||||
|
||||
private resizableLayout_moveButtonClick(event: MoveButtonClickEvent) {
|
||||
resizableLayout_moveButtonClick(event: MoveButtonClickEvent) {
|
||||
const newLayout = move(this.props.mainLayout, event.itemKey, event.direction);
|
||||
this.updateMainLayout(newLayout);
|
||||
}
|
||||
|
||||
private resizableLayout_renderItem(key: string, event: any) {
|
||||
resizableLayout_renderItem(key: string, event: any) {
|
||||
// Key should never be undefined but somehow it can happen, also not
|
||||
// clear how. For now in this case render nothing so that the app
|
||||
// doesn't crash.
|
||||
@@ -770,7 +770,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
public renderPluginDialogs() {
|
||||
renderPluginDialogs() {
|
||||
const output = [];
|
||||
const infos = pluginUtils.viewInfosByType(this.props.plugins, 'webview');
|
||||
|
||||
@@ -801,7 +801,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = Object.assign(
|
||||
{
|
||||
|
@@ -9,7 +9,7 @@ interface Props {
|
||||
}
|
||||
|
||||
class NavigatorComponent extends React.Component<Props> {
|
||||
public UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
if (newProps.route) {
|
||||
const screenInfo = this.props.screens[newProps.route.routeName];
|
||||
const devMarker = Setting.value('env') === 'dev' ? ` (DEV - ${Setting.value('profileDir')})` : '';
|
||||
@@ -21,7 +21,7 @@ class NavigatorComponent extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
public updateWindowTitle(title: string) {
|
||||
updateWindowTitle(title: string) {
|
||||
try {
|
||||
if (bridge().window()) bridge().window().setTitle(title);
|
||||
} catch (error) {
|
||||
@@ -29,7 +29,7 @@ class NavigatorComponent extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
if (!this.props.route) throw new Error('Route must not be null');
|
||||
|
||||
const route = this.props.route;
|
||||
|
@@ -77,6 +77,16 @@ function stripMarkup(markupLanguage: number, markup: string, options: any = null
|
||||
return markupToHtml_.stripMarkup(markupLanguage, markup, options);
|
||||
}
|
||||
|
||||
function createSyntheticClipboardEventWithoutHTML(): ClipboardEvent {
|
||||
const clipboardData = new DataTransfer();
|
||||
for (const format of clipboard.availableFormats()) {
|
||||
if (format !== 'text/html') {
|
||||
clipboardData.setData(format, clipboard.read(format));
|
||||
}
|
||||
}
|
||||
return new ClipboardEvent('paste', { clipboardData });
|
||||
}
|
||||
|
||||
interface TinyMceCommand {
|
||||
name: string;
|
||||
value?: any;
|
||||
@@ -1066,24 +1076,24 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
async function onKeyDown(event: any) {
|
||||
function onKeyDown(event: any) {
|
||||
// It seems "paste as text" is handled automatically on Windows and Linux,
|
||||
// so we need to run the below code only on macOS. If we were to run this
|
||||
// on Windows/Linux, we would have this double-paste issue:
|
||||
// https://github.com/laurent22/joplin/issues/4243
|
||||
|
||||
// While "paste as text" functionality is handled by Windows and Linux, if we
|
||||
// want to allow the user to customize the shortcut we need to prevent when it
|
||||
// has the default value so it doesn't paste the content twice
|
||||
// (one by the system and the other by our code)
|
||||
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
|
||||
event.preventDefault();
|
||||
pasteAsPlainText(null);
|
||||
// Handle "paste as text". Note that when pressing CtrlOrCmd+Shift+V it's going
|
||||
// to trigger the "keydown" event but not the "paste" event, so it's ok to process
|
||||
// it here and we don't need to do anything special in onPaste
|
||||
if (!shim.isWindows() && !shim.isLinux()) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
|
||||
pasteAsPlainText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onPasteAsText() {
|
||||
pasteAsPlainText(null);
|
||||
async function onPasteAsText() {
|
||||
await onPaste(createSyntheticClipboardEventWithoutHTML());
|
||||
}
|
||||
|
||||
editor.on(TinyMceEditorEvents.KeyUp, onKeyUp);
|
||||
|
@@ -62,7 +62,6 @@ export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
postMessageSyntax: 'ipcProxySendToHost',
|
||||
splitted: true,
|
||||
externalAssetsOnly: true,
|
||||
codeHighlightCacheKey: 'useMarkupToHtml',
|
||||
}, options));
|
||||
|
||||
return result;
|
||||
|
@@ -241,7 +241,6 @@ const NoteListComponent = (props: Props) => {
|
||||
event.dataTransfer.setDragImage(new Image(), 1, 1);
|
||||
event.dataTransfer.clearData();
|
||||
event.dataTransfer.setData('text/x-jop-note-ids', JSON.stringify(noteIds));
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
const renderItem = useCallback((item: any, index: number) => {
|
||||
@@ -295,7 +294,7 @@ const NoteListComponent = (props: Props) => {
|
||||
useEffect(() => {
|
||||
if (previousSelectedNoteIds !== props.selectedNoteIds && props.selectedNoteIds.length === 1) {
|
||||
const id = props.selectedNoteIds[0];
|
||||
const doRefocus = props.notes.length < previousNotes.length && !props.focusedField;
|
||||
const doRefocus = props.notes.length < previousNotes.length;
|
||||
|
||||
for (let i = 0; i < props.notes.length; i++) {
|
||||
if (props.notes[i].id === id) {
|
||||
@@ -312,7 +311,8 @@ const NoteListComponent = (props: Props) => {
|
||||
if (previousVisible !== props.visible) {
|
||||
updateSizeState();
|
||||
}
|
||||
}, [previousSelectedNoteIds, previousNotes, previousVisible, props.selectedNoteIds, props.notes, props.focusedField, props.visible]);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [previousSelectedNoteIds, previousNotes, previousVisible, props.selectedNoteIds, props.notes]);
|
||||
|
||||
const scrollNoteIndex_ = (keyCode: any, ctrlKey: any, metaKey: any, noteIndex: any) => {
|
||||
|
||||
@@ -559,7 +559,6 @@ const mapStateToProps = (state: AppState) => {
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customCss,
|
||||
focusedField: state.focusedField,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -23,5 +23,4 @@ export interface Props {
|
||||
highlightedWords: string[];
|
||||
provisionalNoteIds: string[];
|
||||
visible: boolean;
|
||||
focusedField: string;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { AppState } from '../../app.reducer';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useMemo, useState } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import SearchBar from '../SearchBar/SearchBar';
|
||||
import Button, { ButtonLevel, ButtonSize, buttonSizePx } from '../Button/Button';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -11,13 +11,6 @@ import { _ } from '@joplin/lib/locale';
|
||||
const { connect } = require('react-redux');
|
||||
const styled = require('styled-components').default;
|
||||
|
||||
enum BaseBreakpoint {
|
||||
Sm = 160,
|
||||
Md = 190,
|
||||
Lg = 40,
|
||||
Xl = 474,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
showNewNoteButtons: boolean;
|
||||
sortOrderButtonsVisible: boolean;
|
||||
@@ -25,15 +18,6 @@ interface Props {
|
||||
sortOrderReverse: boolean;
|
||||
notesParentType: string;
|
||||
height: number;
|
||||
width: number;
|
||||
onContentHeightChange: (sameRow: boolean)=> void;
|
||||
}
|
||||
|
||||
interface Breakpoints {
|
||||
Sm: number;
|
||||
Md: number;
|
||||
Lg: number;
|
||||
Xl: number;
|
||||
}
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
@@ -50,7 +34,7 @@ const StyledButton = styled(Button)`
|
||||
width: auto;
|
||||
height: 26px;
|
||||
min-height: 26px;
|
||||
max-width: none;
|
||||
flex: 1 0 auto;
|
||||
|
||||
.fa, .fas {
|
||||
font-size: 11px;
|
||||
@@ -70,13 +54,7 @@ const StyledPairButtonR = styled(Button)`
|
||||
width: auto;
|
||||
`;
|
||||
|
||||
const TopRow = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const BottomRow = styled.div`
|
||||
const RowContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1 1 auto;
|
||||
@@ -90,102 +68,7 @@ const SortOrderButtonsContainer = styled.div`
|
||||
`;
|
||||
|
||||
function NoteListControls(props: Props) {
|
||||
const [dynamicBreakpoints, setDynamicBreakpoints] = useState<Breakpoints>({ Sm: BaseBreakpoint.Sm, Md: BaseBreakpoint.Md, Lg: BaseBreakpoint.Lg, Xl: BaseBreakpoint.Xl });
|
||||
|
||||
const searchBarRef = useRef(null);
|
||||
const newNoteRef = useRef(null);
|
||||
const newTodoRef = useRef(null);
|
||||
const noteControlsRef = useRef(null);
|
||||
const searchAndSortRef = useRef(null);
|
||||
|
||||
const getTextWidth = (text: string): number => {
|
||||
const canvas = document.createElement('canvas');
|
||||
if (!canvas) throw new Error('Failed to create canvas element');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to get context');
|
||||
const fontWeight = getComputedStyle(newNoteRef.current).getPropertyValue('font-weight');
|
||||
const fontSize = getComputedStyle(newNoteRef.current).getPropertyValue('font-size');
|
||||
const fontFamily = getComputedStyle(newNoteRef.current).getPropertyValue('font-family');
|
||||
ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`;
|
||||
|
||||
return ctx.measureText(text).width;
|
||||
};
|
||||
|
||||
// Initialize language-specific breakpoints
|
||||
useEffect(() => {
|
||||
// Use the longest string to calculate the amount of extra width needed
|
||||
const smAdditional = getTextWidth(_('note')) > getTextWidth(_('to-do')) ? getTextWidth(_('note')) : getTextWidth(_('to-do'));
|
||||
const mdAdditional = getTextWidth(_('New note')) > getTextWidth(_('New to-do')) ? getTextWidth(_('New note')) : getTextWidth(_('New to-do'));
|
||||
|
||||
const Sm = BaseBreakpoint.Sm + smAdditional * 2;
|
||||
const Md = BaseBreakpoint.Md + mdAdditional * 2;
|
||||
const Lg = BaseBreakpoint.Lg + Md;
|
||||
const Xl = BaseBreakpoint.Xl;
|
||||
|
||||
setDynamicBreakpoints({ Sm, Md, Lg, Xl });
|
||||
}, []);
|
||||
|
||||
const breakpoint = useMemo(() => {
|
||||
// Find largest breakpoint that width is less than
|
||||
const index = Object.values(dynamicBreakpoints).findIndex(x => props.width < x);
|
||||
|
||||
return index === -1 ? dynamicBreakpoints.Xl : Object.values(dynamicBreakpoints)[index];
|
||||
}, [props.width, dynamicBreakpoints]);
|
||||
|
||||
const noteButtonText = useMemo(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Sm) {
|
||||
return '';
|
||||
} else if (breakpoint === dynamicBreakpoints.Md) {
|
||||
return _('note');
|
||||
} else {
|
||||
return _('New note');
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints]);
|
||||
|
||||
const todoButtonText = useMemo(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Sm) {
|
||||
return '';
|
||||
} else if (breakpoint === dynamicBreakpoints.Md) {
|
||||
return _('to-do');
|
||||
} else {
|
||||
return _('New to-do');
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints]);
|
||||
|
||||
const noteIcon = useMemo(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Sm) {
|
||||
return 'icon-note';
|
||||
} else {
|
||||
return 'fas fa-plus';
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints]);
|
||||
|
||||
const todoIcon = useMemo(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Sm) {
|
||||
return 'far fa-check-square';
|
||||
} else {
|
||||
return 'fas fa-plus';
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints]);
|
||||
|
||||
const showTooltip = useMemo(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Sm) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints.Sm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (breakpoint === dynamicBreakpoints.Xl) {
|
||||
noteControlsRef.current.style.flexDirection = 'row';
|
||||
searchAndSortRef.current.style.flex = '2 1 auto';
|
||||
props.onContentHeightChange(true);
|
||||
} else {
|
||||
noteControlsRef.current.style.flexDirection = 'column';
|
||||
props.onContentHeightChange(false);
|
||||
}
|
||||
}, [breakpoint, dynamicBreakpoints, props.onContentHeightChange]);
|
||||
|
||||
useEffect(() => {
|
||||
CommandService.instance().registerRuntime('focusSearch', focusSearchRuntime(searchBarRef));
|
||||
@@ -244,63 +127,64 @@ function NoteListControls(props: Props) {
|
||||
if (!props.showNewNoteButtons) return null;
|
||||
|
||||
return (
|
||||
<TopRow className="new-note-todo-buttons">
|
||||
<StyledButton ref={newNoteRef}
|
||||
<RowContainer>
|
||||
<StyledButton
|
||||
className="new-note-button"
|
||||
tooltip={ showTooltip ? CommandService.instance().label('newNote') : '' }
|
||||
iconName={noteIcon}
|
||||
title={_('%s', noteButtonText)}
|
||||
tooltip={CommandService.instance().label('newNote')}
|
||||
iconName="fas fa-plus"
|
||||
title={_('%s', 'New note')}
|
||||
level={ButtonLevel.Primary}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onNewNoteButtonClick}
|
||||
/>
|
||||
<StyledButton ref={newTodoRef}
|
||||
<StyledButton
|
||||
className="new-todo-button"
|
||||
tooltip={ showTooltip ? CommandService.instance().label('newTodo') : '' }
|
||||
iconName={todoIcon}
|
||||
title={_('%s', todoButtonText)}
|
||||
tooltip={CommandService.instance().label('newTodo')}
|
||||
iconName="fas fa-plus"
|
||||
title={_('%s', 'New to-do')}
|
||||
level={ButtonLevel.Secondary}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onNewTodoButtonClick}
|
||||
/>
|
||||
</TopRow>
|
||||
</RowContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledRoot ref={noteControlsRef}>
|
||||
<StyledRoot>
|
||||
{renderNewNoteButtons()}
|
||||
<BottomRow ref={searchAndSortRef} className="search-and-sort">
|
||||
<RowContainer>
|
||||
<SearchBar inputRef={searchBarRef}/>
|
||||
{showsSortOrderButtons() &&
|
||||
<SortOrderButtonsContainer>
|
||||
<StyledPairButtonL
|
||||
className="sort-order-field-button"
|
||||
tooltip={sortOrderFieldTooltip()}
|
||||
iconName={sortOrderFieldIcon()}
|
||||
level={ButtonLevel.Secondary}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onSortOrderFieldButtonClick}
|
||||
/>
|
||||
<StyledPairButtonR
|
||||
className="sort-order-reverse-button"
|
||||
tooltip={CommandService.instance().label('toggleNotesSortOrderReverse')}
|
||||
iconName={sortOrderReverseIcon()}
|
||||
level={ButtonLevel.Secondary}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onSortOrderReverseButtonClick}
|
||||
/>
|
||||
</SortOrderButtonsContainer>
|
||||
}
|
||||
</BottomRow>
|
||||
<SortOrderButtonsContainer>
|
||||
{showsSortOrderButtons() &&
|
||||
<StyledPairButtonL
|
||||
className="sort-order-field-button"
|
||||
tooltip={sortOrderFieldTooltip()}
|
||||
iconName={sortOrderFieldIcon()}
|
||||
level={ButtonLevel.Secondary}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onSortOrderFieldButtonClick}
|
||||
/>
|
||||
}
|
||||
{showsSortOrderButtons() &&
|
||||
<StyledPairButtonR
|
||||
className="sort-order-reverse-button"
|
||||
tooltip={CommandService.instance().label('toggleNotesSortOrderReverse')}
|
||||
iconName={sortOrderReverseIcon()}
|
||||
level={ButtonLevel.Secondary}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onSortOrderReverseButtonClick}
|
||||
/>
|
||||
}
|
||||
</SortOrderButtonsContainer>
|
||||
</RowContainer>
|
||||
</StyledRoot>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
// TODO: showNewNoteButtons and the logic associated is not needed anymore.
|
||||
showNewNoteButtons: true,
|
||||
showNewNoteButtons: state.focusedField !== 'globalSearch',
|
||||
sortOrderButtonsVisible: state.settings['notes.sortOrder.buttonsVisible'],
|
||||
sortOrderField: state.settings['notes.sortOrder.field'],
|
||||
sortOrderReverse: state.settings['notes.sortOrder.reverse'],
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import * as React from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import NoteList from '../NoteList/NoteList';
|
||||
import NoteListControls from '../NoteListControls/NoteListControls';
|
||||
import { Size } from '../ResizableLayout/utils/types';
|
||||
@@ -22,15 +22,7 @@ const StyledRoot = styled.div`
|
||||
|
||||
export default function NoteListWrapper(props: Props) {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const [controlHeight, setControlHeight] = useState(theme.topRowHeight);
|
||||
|
||||
const onContentHeightChange = (sameRow: boolean) => {
|
||||
if (sameRow) {
|
||||
setControlHeight(theme.topRowHeight);
|
||||
} else {
|
||||
setControlHeight(theme.topRowHeight * 2);
|
||||
}
|
||||
};
|
||||
const controlHeight = theme.topRowHeight;
|
||||
|
||||
const noteListSize = useMemo(() => {
|
||||
return {
|
||||
@@ -41,7 +33,7 @@ export default function NoteListWrapper(props: Props) {
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<NoteListControls height={controlHeight} width={noteListSize.width} onContentHeightChange={onContentHeightChange}/>
|
||||
<NoteListControls height={controlHeight}/>
|
||||
<NoteList resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
|
||||
</StyledRoot>
|
||||
);
|
||||
|
@@ -31,7 +31,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
private styleKey_: number;
|
||||
private styles_: any;
|
||||
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.revisionsLink_click = this.revisionsLink_click.bind(this);
|
||||
@@ -56,17 +56,17 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
componentDidMount() {
|
||||
void this.loadNote(this.props.noteId);
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
componentDidUpdate() {
|
||||
if (this.state.editedKey === null) {
|
||||
this.okButton.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public async loadNote(noteId: string) {
|
||||
async loadNote(noteId: string) {
|
||||
if (!noteId) {
|
||||
this.setState({ formNote: null });
|
||||
} else {
|
||||
@@ -76,7 +76,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
public latLongFromLocation(location: string) {
|
||||
latLongFromLocation(location: string) {
|
||||
const o: any = {};
|
||||
const l = location.split(',');
|
||||
if (l.length === 2) {
|
||||
@@ -89,7 +89,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
return o;
|
||||
}
|
||||
|
||||
public noteToFormNote(note: NoteEntity) {
|
||||
noteToFormNote(note: NoteEntity) {
|
||||
const formNote: any = {};
|
||||
|
||||
formNote.user_updated_time = time.formatMsToLocal(note.user_updated_time);
|
||||
@@ -113,7 +113,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
return formNote;
|
||||
}
|
||||
|
||||
public formNoteToNote(formNote: any) {
|
||||
formNoteToNote(formNote: any) {
|
||||
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);
|
||||
@@ -127,7 +127,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
return note;
|
||||
}
|
||||
|
||||
public styles(themeId: number) {
|
||||
styles(themeId: number) {
|
||||
const styleKey = themeId;
|
||||
if (styleKey === this.styleKey_) return this.styles_;
|
||||
|
||||
@@ -168,7 +168,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
return this.styles_;
|
||||
}
|
||||
|
||||
public async closeDialog(applyChanges: boolean) {
|
||||
async closeDialog(applyChanges: boolean) {
|
||||
if (applyChanges) {
|
||||
await this.saveProperty();
|
||||
const note = this.formNoteToNote(this.state.formNote);
|
||||
@@ -183,16 +183,16 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private buttonRow_click(event: any) {
|
||||
buttonRow_click(event: any) {
|
||||
void this.closeDialog(event.buttonName === 'ok');
|
||||
}
|
||||
|
||||
private revisionsLink_click() {
|
||||
revisionsLink_click() {
|
||||
void this.closeDialog(false);
|
||||
if (this.props.onRevisionLinkClick) this.props.onRevisionLinkClick();
|
||||
}
|
||||
|
||||
public editPropertyButtonClick(key: string, initialValue: any) {
|
||||
editPropertyButtonClick(key: string, initialValue: any) {
|
||||
this.setState({
|
||||
editedKey: key,
|
||||
editedValue: initialValue,
|
||||
@@ -207,7 +207,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public async saveProperty() {
|
||||
async saveProperty() {
|
||||
if (!this.state.editedKey) return;
|
||||
|
||||
return new Promise((resolve: Function) => {
|
||||
@@ -233,7 +233,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
public async cancelProperty() {
|
||||
async cancelProperty() {
|
||||
return new Promise((resolve: Function) => {
|
||||
this.okButton.current.focus();
|
||||
this.setState({
|
||||
@@ -245,7 +245,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
public createNoteField(key: string, value: any) {
|
||||
createNoteField(key: string, value: any) {
|
||||
const styles = this.styles(this.props.themeId);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const labelComp = <label style={Object.assign({}, theme.textStyle, theme.controlBoxLabel)}>{this.formatLabel(key)}</label>;
|
||||
@@ -364,12 +364,12 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
public formatLabel(key: string) {
|
||||
formatLabel(key: string) {
|
||||
if (this.keyToLabel_[key]) return this.keyToLabel_[key];
|
||||
return key;
|
||||
}
|
||||
|
||||
public formatValue(key: string, note: NoteEntity) {
|
||||
formatValue(key: string, note: NoteEntity) {
|
||||
if (key === 'location') {
|
||||
if (!Number(note.latitude) && !Number(note.longitude)) return null;
|
||||
const dms = formatcoords(Number(note.latitude), Number(note.longitude));
|
||||
@@ -383,7 +383,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
return (note as any)[key];
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const formNote = this.state.formNote;
|
||||
|
||||
|
@@ -38,7 +38,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
private viewerRef_: any;
|
||||
private helpButton_onClick: Function;
|
||||
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@@ -57,7 +57,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
|
||||
}
|
||||
|
||||
public style() {
|
||||
style() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const style = {
|
||||
@@ -74,7 +74,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
return style;
|
||||
}
|
||||
|
||||
private async viewer_domReady() {
|
||||
async viewer_domReady() {
|
||||
// this.viewerRef_.current.openDevTools();
|
||||
|
||||
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, this.props.noteId);
|
||||
@@ -90,7 +90,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
private async importButton_onClick() {
|
||||
async importButton_onClick() {
|
||||
if (!this.state.note) return;
|
||||
this.setState({ restoring: true });
|
||||
await RevisionService.instance().importRevisionNote(this.state.note);
|
||||
@@ -98,11 +98,11 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
alert(RevisionService.instance().restoreSuccessMessage(this.state.note));
|
||||
}
|
||||
|
||||
private backButton_click() {
|
||||
backButton_click() {
|
||||
if (this.props.onBack) this.props.onBack();
|
||||
}
|
||||
|
||||
private revisionList_onChange(event: any) {
|
||||
revisionList_onChange(event: any) {
|
||||
const value = event.target.value;
|
||||
|
||||
if (!value) {
|
||||
@@ -119,7 +119,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
public async reloadNote() {
|
||||
async reloadNote() {
|
||||
let noteBody = '';
|
||||
let markupLanguage = MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
|
||||
if (!this.state.revisions.length || !this.state.currentRevId) {
|
||||
@@ -153,7 +153,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
private async webview_ipcMessage(event: any) {
|
||||
async webview_ipcMessage(event: any) {
|
||||
// For the revision view, we only suppport a minimal subset of the IPC messages.
|
||||
// For example, we don't need interactive checkboxes or sync between viewer and editor view.
|
||||
// We try to get most links work though, except for internal (joplin://) links.
|
||||
@@ -183,7 +183,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = this.style();
|
||||
|
||||
|
@@ -20,7 +20,7 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
|
||||
private backgroundColor: any;
|
||||
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.searchInput_change = this.searchInput_change.bind(this);
|
||||
@@ -33,7 +33,7 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
this.backgroundColor = undefined;
|
||||
}
|
||||
|
||||
public style() {
|
||||
style() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const style = {
|
||||
@@ -46,7 +46,7 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
return style;
|
||||
}
|
||||
|
||||
public buttonIconComponent(iconName: string, clickHandler: any, isEnabled: boolean) {
|
||||
buttonIconComponent(iconName: string, clickHandler: any, isEnabled: boolean) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const searchButton = {
|
||||
@@ -74,12 +74,12 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
private searchInput_change(event: any) {
|
||||
searchInput_change(event: any) {
|
||||
const query = event.currentTarget.value;
|
||||
this.triggerOnChange(query);
|
||||
}
|
||||
|
||||
private searchInput_keyDown(event: any) {
|
||||
searchInput_keyDown(event: any) {
|
||||
if (event.keyCode === 13) {
|
||||
// ENTER
|
||||
event.preventDefault();
|
||||
@@ -106,28 +106,28 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
private previousButton_click() {
|
||||
previousButton_click() {
|
||||
if (this.props.onPrevious) this.props.onPrevious();
|
||||
}
|
||||
|
||||
private nextButton_click() {
|
||||
nextButton_click() {
|
||||
if (this.props.onNext) this.props.onNext();
|
||||
}
|
||||
|
||||
private closeButton_click() {
|
||||
closeButton_click() {
|
||||
if (this.props.onClose) this.props.onClose();
|
||||
}
|
||||
|
||||
public triggerOnChange(query: string) {
|
||||
triggerOnChange(query: string) {
|
||||
if (this.props.onChange) this.props.onChange(query);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
focus() {
|
||||
(this.refs.searchInput as any).focus();
|
||||
(this.refs.searchInput as any).select();
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const query = this.props.query ? this.props.query : '';
|
||||
|
||||
// backgroundColor needs to cached to a local variable to prevent the
|
||||
|
@@ -11,7 +11,7 @@ interface Props {
|
||||
}
|
||||
|
||||
class NoteStatusBarComponent extends React.Component<Props> {
|
||||
public style() {
|
||||
style() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const style = {
|
||||
@@ -24,7 +24,7 @@ class NoteStatusBarComponent extends React.Component<Props> {
|
||||
return style;
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const note = this.props.note;
|
||||
return <div style={this.style().root}>{time.formatMsToLocal(note.user_updated_time)}</div>;
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
private webviewRef_: any;
|
||||
private webviewListeners_: any = null;
|
||||
|
||||
public constructor(props: any) {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.webviewRef_ = React.createRef();
|
||||
@@ -41,20 +41,20 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
this.webview_message = this.webview_message.bind(this);
|
||||
}
|
||||
|
||||
private webview_domReady(event: any) {
|
||||
webview_domReady(event: any) {
|
||||
this.domReady_ = true;
|
||||
if (this.props.onDomReady) this.props.onDomReady(event);
|
||||
}
|
||||
|
||||
private webview_ipcMessage(event: any) {
|
||||
webview_ipcMessage(event: any) {
|
||||
if (this.props.onIpcMessage) this.props.onIpcMessage(event);
|
||||
}
|
||||
|
||||
private webview_load() {
|
||||
webview_load() {
|
||||
this.webview_domReady({});
|
||||
}
|
||||
|
||||
private webview_message(event: any) {
|
||||
webview_message(event: any) {
|
||||
if (!event.data || event.data.target !== 'main') return;
|
||||
|
||||
const callName = event.data.name;
|
||||
@@ -68,11 +68,11 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
}
|
||||
}
|
||||
|
||||
public domReady() {
|
||||
domReady() {
|
||||
return this.domReady_;
|
||||
}
|
||||
|
||||
public initWebview() {
|
||||
initWebview() {
|
||||
const wv = this.webviewRef_.current;
|
||||
|
||||
if (!this.webviewListeners_) {
|
||||
@@ -92,7 +92,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
this.webviewRef_.current.contentWindow.addEventListener('message', this.webview_message);
|
||||
}
|
||||
|
||||
public destroyWebview() {
|
||||
destroyWebview() {
|
||||
const wv = this.webviewRef_.current;
|
||||
if (!wv || !this.initialized_) return;
|
||||
|
||||
@@ -115,28 +115,28 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
this.domReady_ = false;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
focus() {
|
||||
if (this.webviewRef_.current) {
|
||||
this.webviewRef_.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public tryInit() {
|
||||
tryInit() {
|
||||
if (!this.initialized_ && this.webviewRef_.current) {
|
||||
this.initWebview();
|
||||
this.initialized_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
componentDidMount() {
|
||||
this.tryInit();
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
componentDidUpdate() {
|
||||
this.tryInit();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
componentWillUnmount() {
|
||||
this.destroyWebview();
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
// Wrap WebView functions
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
public send(channel: string, arg0: any = null, arg1: any = null) {
|
||||
send(channel: string, arg0: any = null, arg1: any = null) {
|
||||
const win = this.webviewRef_.current.contentWindow;
|
||||
|
||||
if (channel === 'focus') {
|
||||
@@ -172,7 +172,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
// Wrap WebView functions (END)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const viewerStyle = Object.assign({}, { border: 'none' }, this.props.viewerStyle);
|
||||
return <iframe className="noteTextViewer" ref={this.webviewRef_} style={viewerStyle} src="gui/note-viewer/index.html"></iframe>;
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ interface Props {
|
||||
}
|
||||
|
||||
class OneDriveLoginScreenComponent extends React.Component<any, any> {
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@@ -22,7 +22,7 @@ class OneDriveLoginScreenComponent extends React.Component<any, any> {
|
||||
};
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
async componentDidMount() {
|
||||
const log = (s: any) => {
|
||||
this.setState((state: any) => {
|
||||
const authLog = state.authLog.slice();
|
||||
@@ -48,15 +48,15 @@ class OneDriveLoginScreenComponent extends React.Component<any, any> {
|
||||
}
|
||||
}
|
||||
|
||||
public startUrl() {
|
||||
startUrl() {
|
||||
return reg.syncTarget().api().authCodeUrl(this.redirectUrl());
|
||||
}
|
||||
|
||||
public redirectUrl() {
|
||||
redirectUrl() {
|
||||
return reg.syncTarget().api().nativeClientRedirectUrl();
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const logComps = [];
|
||||
|
@@ -27,13 +27,13 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
private styles_: any;
|
||||
private styleKey_: string;
|
||||
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.answerInput_ = React.createRef();
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
visible: false,
|
||||
answer: this.props.defaultValue ? this.props.defaultValue : '',
|
||||
@@ -41,7 +41,7 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
this.focusInput_ = true;
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
if ('visible' in newProps && newProps.visible !== this.props.visible) {
|
||||
this.setState({ visible: newProps.visible });
|
||||
if (newProps.visible) this.focusInput_ = true;
|
||||
@@ -52,12 +52,12 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
componentDidUpdate() {
|
||||
if (this.focusInput_ && this.answerInput_.current) this.answerInput_.current.focus();
|
||||
this.focusInput_ = false;
|
||||
}
|
||||
|
||||
public styles(themeId: number, width: number, height: number, visible: boolean) {
|
||||
styles(themeId: number, width: number, height: number, visible: boolean) {
|
||||
const styleKey = `${themeId}_${width}_${height}_${visible}`;
|
||||
if (styleKey === this.styleKey_) return this.styles_;
|
||||
|
||||
@@ -181,7 +181,7 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
return this.styles_;
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel'];
|
||||
|
@@ -147,7 +147,7 @@ describe('movements', () => {
|
||||
expect(canMove(MoveDirection.Right, findItemByKey(layout, 'col2'), findItemByKey(layout, 'root'))).toBe(false);
|
||||
});
|
||||
|
||||
test('container with only one child should take the width of its parent', () => {
|
||||
test('Container with only one child should take the width of its parent', () => {
|
||||
let layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 100,
|
||||
@@ -170,7 +170,7 @@ describe('movements', () => {
|
||||
expect(layout.children[0].children[0].width).toBe(undefined);
|
||||
});
|
||||
|
||||
test('temp container should take the width of the child it replaces', () => {
|
||||
test('Temp container should take the width of the child it replaces', () => {
|
||||
let layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 100,
|
||||
@@ -198,7 +198,7 @@ describe('movements', () => {
|
||||
expect(layout.children[0].children[1].width).toBe(undefined);
|
||||
});
|
||||
|
||||
test('last child should have flexible width if all siblings have fixed width', () => {
|
||||
test('Last child should have flexible width if all siblings have fixed width', () => {
|
||||
let layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
width: 100,
|
||||
|
@@ -260,6 +260,10 @@ describe('useLayoutItemSizes', () => {
|
||||
expect(sizes.col4.width).toBe(50);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('calculateMaxSizeAvailableForItem', () => {
|
||||
|
||||
test('should give maximum available space this item can take up during resizing', () => {
|
||||
const layout: LayoutItem = validateLayout({
|
||||
key: 'root',
|
||||
|
@@ -135,7 +135,7 @@ const getNextSortingOrderType = (s: SortingType): SortingType => {
|
||||
const MAX_RESOURCES = 10000;
|
||||
|
||||
class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
resources: undefined,
|
||||
@@ -147,7 +147,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
};
|
||||
}
|
||||
|
||||
public async reloadResources(sorting: ActiveSorting) {
|
||||
async reloadResources(sorting: ActiveSorting) {
|
||||
this.setState({ isLoading: true });
|
||||
const resources = await Resource.all({
|
||||
order: [{
|
||||
@@ -161,11 +161,11 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
this.setState({ resources, isLoading: false });
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
componentDidMount() {
|
||||
void this.reloadResources(this.state.sorting);
|
||||
}
|
||||
|
||||
public onResourceDelete(resource: InnerResource) {
|
||||
onResourceDelete(resource: InnerResource) {
|
||||
const ok = bridge().showConfirmMessageBox(_('Delete attachment "%s"?', resource.title), {
|
||||
buttons: [_('Delete'), _('Cancel')],
|
||||
defaultId: 1,
|
||||
@@ -184,7 +184,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
public openResource(resource: InnerResource) {
|
||||
openResource(resource: InnerResource) {
|
||||
const resourcePath = Resource.fullPath(resource);
|
||||
const ok = bridge().openExternal(`file://${resourcePath}`);
|
||||
if (!ok) {
|
||||
@@ -192,7 +192,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
public onToggleSortOrder(sortOrder: SortingOrder) {
|
||||
onToggleSortOrder(sortOrder: SortingOrder) {
|
||||
let newSorting = { ...this.state.sorting };
|
||||
if (sortOrder === this.state.sorting.order) {
|
||||
newSorting.type = getNextSortingOrderType(newSorting.type);
|
||||
@@ -206,7 +206,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
void this.reloadResources(newSorting);
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
|
@@ -5,7 +5,7 @@ import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { AppState } from '../app.reducer';
|
||||
|
||||
class TagItemComponent extends React.Component {
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = Object.assign({}, theme.tagStyle);
|
||||
const { title, id } = this.props;
|
||||
|
@@ -13,7 +13,7 @@ interface Props {
|
||||
|
||||
class ToolbarBaseComponent extends React.Component<Props, any> {
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const style: any = Object.assign({
|
||||
|
@@ -6,7 +6,7 @@ interface Props {
|
||||
}
|
||||
|
||||
class ToolbarSpace extends React.Component<Props> {
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = Object.assign({}, theme.toolbarStyle);
|
||||
style.minWidth = style.height / 2;
|
||||
|
@@ -7,11 +7,11 @@ const smalltalk = require('smalltalk');
|
||||
const logger = Logger.create('dialogs');
|
||||
|
||||
class Dialogs {
|
||||
public async alert(message: string, title = '') {
|
||||
async alert(message: string, title = '') {
|
||||
await smalltalk.alert(title, message);
|
||||
}
|
||||
|
||||
public async confirm(message: string, title = '', options: any = {}) {
|
||||
async confirm(message: string, title = '', options: any = {}) {
|
||||
try {
|
||||
await smalltalk.confirm(title, message, options);
|
||||
return true;
|
||||
@@ -21,7 +21,7 @@ class Dialogs {
|
||||
}
|
||||
}
|
||||
|
||||
public async prompt(message: string, title = '', defaultValue = '', options: any = null) {
|
||||
async prompt(message: string, title = '', defaultValue = '', options: any = null) {
|
||||
if (options === null) options = {};
|
||||
|
||||
try {
|
||||
|
@@ -26,7 +26,7 @@ interface ContextMenuProps {
|
||||
}
|
||||
|
||||
export default class NoteListUtils {
|
||||
public static makeContextMenu(noteIds: string[], props: ContextMenuProps) {
|
||||
static makeContextMenu(noteIds: string[], props: ContextMenuProps) {
|
||||
const cmdService = CommandService.instance();
|
||||
|
||||
const menuUtils = new MenuUtils(cmdService);
|
||||
@@ -212,7 +212,7 @@ export default class NoteListUtils {
|
||||
return menu;
|
||||
}
|
||||
|
||||
public static async confirmDeleteNotes(noteIds: string[]) {
|
||||
static async confirmDeleteNotes(noteIds: string[]) {
|
||||
if (!noteIds.length) return;
|
||||
|
||||
const msg = await Note.deleteMessage(noteIds);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.10.10",
|
||||
"version": "2.10.8",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -171,7 +171,7 @@
|
||||
"roboto-fontface": "0.10.0",
|
||||
"smalltalk": "2.5.1",
|
||||
"sqlite3": "5.1.4",
|
||||
"styled-components": "5.3.8",
|
||||
"styled-components": "5.3.6",
|
||||
"styled-system": "5.1.5",
|
||||
"taboverride": "4.0.3",
|
||||
"tinymce": "5.10.6"
|
||||
|
@@ -65,7 +65,7 @@ class GotoAnything {
|
||||
public static Dialog: any;
|
||||
public static manifest: any;
|
||||
|
||||
public onTrigger(event: any) {
|
||||
onTrigger(event: any) {
|
||||
this.dispatch({
|
||||
type: 'PLUGINLEGACY_DIALOG_SET',
|
||||
open: true,
|
||||
@@ -85,7 +85,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
private markupToHtml_: MarkupToHtml;
|
||||
private userCallback_: any = null;
|
||||
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const startString = props?.userData?.startString ? props?.userData?.startString : '';
|
||||
@@ -119,7 +119,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
if (startString) this.scheduleListUpdate();
|
||||
}
|
||||
|
||||
public style() {
|
||||
style() {
|
||||
const styleKey = [this.props.themeId, this.state.listType, this.state.resultsInBody ? '1' : '0'].join('-');
|
||||
|
||||
if (this.styles_[styleKey]) return this.styles_[styleKey];
|
||||
@@ -184,7 +184,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
return this.styles_[styleKey];
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
|
||||
this.props.dispatch({
|
||||
@@ -193,7 +193,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
componentWillUnmount() {
|
||||
if (this.listUpdateIID_) shim.clearTimeout(this.listUpdateIID_);
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
|
||||
@@ -203,7 +203,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
public onKeyDown(event: any) {
|
||||
onKeyDown(event: any) {
|
||||
if (event.keyCode === 27) { // ESCAPE
|
||||
this.props.dispatch({
|
||||
pluginName: PLUGIN_NAME,
|
||||
@@ -213,7 +213,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private modalLayer_onClick(event: any) {
|
||||
modalLayer_onClick(event: any) {
|
||||
if (event.currentTarget === event.target) {
|
||||
this.props.dispatch({
|
||||
pluginName: PLUGIN_NAME,
|
||||
@@ -223,17 +223,17 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private helpButton_onClick() {
|
||||
helpButton_onClick() {
|
||||
this.setState({ showHelp: !this.state.showHelp });
|
||||
}
|
||||
|
||||
private input_onChange(event: any) {
|
||||
input_onChange(event: any) {
|
||||
this.setState({ query: event.target.value });
|
||||
|
||||
this.scheduleListUpdate();
|
||||
}
|
||||
|
||||
public scheduleListUpdate() {
|
||||
scheduleListUpdate() {
|
||||
if (this.listUpdateIID_) shim.clearTimeout(this.listUpdateIID_);
|
||||
|
||||
this.listUpdateIID_ = shim.setTimeout(async () => {
|
||||
@@ -242,12 +242,12 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public async keywords(searchQuery: string) {
|
||||
async keywords(searchQuery: string) {
|
||||
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery);
|
||||
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
|
||||
public markupToHtml() {
|
||||
markupToHtml() {
|
||||
if (this.markupToHtml_) return this.markupToHtml_;
|
||||
this.markupToHtml_ = markupLanguageUtils.newMarkupToHtml();
|
||||
return this.markupToHtml_;
|
||||
@@ -262,7 +262,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
};
|
||||
}
|
||||
|
||||
public async updateList() {
|
||||
async updateList() {
|
||||
let resultsInBody = false;
|
||||
|
||||
if (!this.state.query) {
|
||||
@@ -402,7 +402,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
this.itemListRef.current.makeItemIndexVisible(index);
|
||||
}
|
||||
|
||||
public async gotoItem(item: any) {
|
||||
async gotoItem(item: any) {
|
||||
this.props.dispatch({
|
||||
pluginName: PLUGIN_NAME,
|
||||
type: 'PLUGINLEGACY_DIALOG_SET',
|
||||
@@ -465,7 +465,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private listItem_onClick(event: any) {
|
||||
listItem_onClick(event: any) {
|
||||
const itemId = event.currentTarget.getAttribute('data-id');
|
||||
const parentId = event.currentTarget.getAttribute('data-parent-id');
|
||||
const itemType = Number(event.currentTarget.getAttribute('data-type'));
|
||||
@@ -478,7 +478,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
public renderItem(item: SearchResult) {
|
||||
renderItem(item: SearchResult) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = this.style();
|
||||
const isSelected = item.id === this.state.selectedItemId;
|
||||
@@ -502,7 +502,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
public selectedItemIndex(results: any[] = undefined, itemId: string = undefined) {
|
||||
selectedItemIndex(results: any[] = undefined, itemId: string = undefined) {
|
||||
if (typeof results === 'undefined') results = this.state.results;
|
||||
if (typeof itemId === 'undefined') itemId = this.state.selectedItemId;
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
@@ -512,13 +512,13 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public selectedItem() {
|
||||
selectedItem() {
|
||||
const index = this.selectedItemIndex();
|
||||
if (index < 0) return null;
|
||||
return { ...this.state.results[index], commandArgs: this.state.commandArgs };
|
||||
}
|
||||
|
||||
private input_onKeyDown(event: any) {
|
||||
input_onKeyDown(event: any) {
|
||||
const keyCode = event.keyCode;
|
||||
|
||||
if (this.state.results.length > 0 && (keyCode === 40 || keyCode === 38)) { // DOWN / UP
|
||||
@@ -554,7 +554,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
return maxItemCount * itemHeight;
|
||||
}
|
||||
|
||||
public renderList() {
|
||||
renderList() {
|
||||
const style = this.style();
|
||||
|
||||
const itemListStyle = {
|
||||
@@ -573,7 +573,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = this.style();
|
||||
const helpComp = !this.state.showHelp ? null : <div className="help-text" style={style.help}>{_('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.')}</div>;
|
||||
|
@@ -6,14 +6,14 @@ const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
const folderId1 = 'aa012345678901234567890123456789';
|
||||
const folderId2 = 'bb012345678901234567890123456789';
|
||||
|
||||
describe('PerFolderSortOrderService', () => {
|
||||
beforeAll(async () => {
|
||||
shimInit();
|
||||
Setting.autoSaveEnabled = false;
|
||||
PerFolderSortOrderService.initialize();
|
||||
Setting.setValue('notes.perFolderSortOrderEnabled', true);
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
shimInit();
|
||||
Setting.autoSaveEnabled = false;
|
||||
PerFolderSortOrderService.initialize();
|
||||
Setting.setValue('notes.perFolderSortOrderEnabled', true);
|
||||
});
|
||||
describe('PerFolderSortOrderService', () => {
|
||||
|
||||
test('get(), isSet() and set()', async () => {
|
||||
// Clear all per-folder sort order
|
||||
|
@@ -2,12 +2,12 @@ import { notesSortOrderFieldArray, notesSortOrderNextField, setNotesSortOrder }
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
|
||||
describe('notesSortOrderUtils', () => {
|
||||
beforeAll(() => {
|
||||
shimInit();
|
||||
Setting.autoSaveEnabled = false;
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
shimInit();
|
||||
Setting.autoSaveEnabled = false;
|
||||
});
|
||||
describe('notesSortOrderUtils', () => {
|
||||
|
||||
it('should always provide the same ordered fields', async () => {
|
||||
const expected = ['user_updated_time', 'user_created_time', 'title', 'order'];
|
||||
|
@@ -6,24 +6,24 @@ import KvStore from '@joplin/lib/services/KvStore';
|
||||
|
||||
export default class PluginAssetsLoader {
|
||||
|
||||
private static instance_: PluginAssetsLoader = null;
|
||||
private logger_: any = null;
|
||||
static instance_: PluginAssetsLoader = null;
|
||||
logger_: any = null;
|
||||
|
||||
public static instance() {
|
||||
static instance() {
|
||||
if (PluginAssetsLoader.instance_) return PluginAssetsLoader.instance_;
|
||||
PluginAssetsLoader.instance_ = new PluginAssetsLoader();
|
||||
return PluginAssetsLoader.instance_;
|
||||
}
|
||||
|
||||
public setLogger(logger: any) {
|
||||
setLogger(logger: any) {
|
||||
this.logger_ = logger;
|
||||
}
|
||||
|
||||
public logger() {
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
public async importAssets() {
|
||||
async importAssets() {
|
||||
const destDir = `${Setting.value('resourceDir')}/pluginAssets`;
|
||||
await shim.fsDriver().mkdir(destDir);
|
||||
|
||||
|
@@ -2,7 +2,7 @@ const { BackButtonService } = require('../services/back-button.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
|
||||
export default class BackButtonDialogBox extends DialogBox {
|
||||
public constructor(props: any) {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.backHandler_ = () => {
|
||||
@@ -14,7 +14,7 @@ export default class BackButtonDialogBox extends DialogBox {
|
||||
};
|
||||
}
|
||||
|
||||
public async componentDidUpdate() {
|
||||
async componentDidUpdate() {
|
||||
if (this.state.isVisible) {
|
||||
BackButtonService.addHandler(this.backHandler_);
|
||||
} else {
|
||||
|
@@ -14,7 +14,7 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
Icon.loadFont().catch((error: any) => { console.info(error); });
|
||||
|
||||
class CameraView extends Component {
|
||||
public constructor() {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const dimensions = Dimensions.get('window');
|
||||
@@ -34,18 +34,18 @@ class CameraView extends Component {
|
||||
this.onLayout = this.onLayout.bind(this);
|
||||
}
|
||||
|
||||
public onLayout(event: any) {
|
||||
onLayout(event: any) {
|
||||
this.setState({
|
||||
screenWidth: event.nativeEvent.layout.width,
|
||||
screenHeight: event.nativeEvent.layout.height,
|
||||
});
|
||||
}
|
||||
|
||||
private back_onPress() {
|
||||
back_onPress() {
|
||||
if (this.props.onCancel) this.props.onCancel();
|
||||
}
|
||||
|
||||
private reverse_onPress() {
|
||||
reverse_onPress() {
|
||||
if (this.props.cameraType === RNCamera.Constants.Type.back) {
|
||||
Setting.setValue('camera.type', RNCamera.Constants.Type.front);
|
||||
} else {
|
||||
@@ -53,7 +53,7 @@ class CameraView extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
private ratio_onPress() {
|
||||
ratio_onPress() {
|
||||
if (this.state.ratios.length <= 1) return;
|
||||
|
||||
let index = this.state.ratios.indexOf(this.props.cameraRatio);
|
||||
@@ -62,7 +62,7 @@ class CameraView extends Component {
|
||||
Setting.setValue('camera.ratio', this.state.ratios[index]);
|
||||
}
|
||||
|
||||
private async photo_onPress() {
|
||||
async photo_onPress() {
|
||||
if (!this.camera || !this.props.onPhoto) return;
|
||||
|
||||
this.setState({ snapping: true });
|
||||
@@ -79,14 +79,14 @@ class CameraView extends Component {
|
||||
|
||||
}
|
||||
|
||||
public async onCameraReady() {
|
||||
async onCameraReady() {
|
||||
if (this.supportsRatios()) {
|
||||
const ratios = await this.camera.getSupportedRatiosAsync();
|
||||
this.setState({ ratios: ratios });
|
||||
}
|
||||
}
|
||||
|
||||
public renderButton(onPress: Function, iconNameOrIcon: any, style: any) {
|
||||
renderButton(onPress: Function, iconNameOrIcon: any, style: any) {
|
||||
let icon = null;
|
||||
|
||||
if (typeof iconNameOrIcon === 'string') {
|
||||
@@ -112,7 +112,7 @@ class CameraView extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
public fitRectIntoBounds(rect: any, bounds: any) {
|
||||
fitRectIntoBounds(rect: any, bounds: any) {
|
||||
const rectRatio = rect.width / rect.height;
|
||||
const boundsRatio = bounds.width / bounds.height;
|
||||
|
||||
@@ -130,7 +130,7 @@ class CameraView extends Component {
|
||||
return newDimensions;
|
||||
}
|
||||
|
||||
public cameraRect(ratio: string) {
|
||||
cameraRect(ratio: string) {
|
||||
// To keep the calculations simpler, it's assumed that the phone is in
|
||||
// portrait orientation. Then at the end we swap the values if needed.
|
||||
const splitted = ratio.split(':');
|
||||
@@ -152,11 +152,11 @@ class CameraView extends Component {
|
||||
return output;
|
||||
}
|
||||
|
||||
public supportsRatios() {
|
||||
supportsRatios() {
|
||||
return shim.mobilePlatform() === 'android';
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const photoIcon = this.state.snapping ? 'md-checkmark' : 'md-camera';
|
||||
|
||||
const displayRatios = this.supportsRatios() && this.state.ratios.length > 1;
|
||||
|
@@ -239,5 +239,17 @@ describe('markdownCommands', () => {
|
||||
expect(sel.from).toBe('> Testing...> \n> \n'.length);
|
||||
expect(sel.to).toBe(editor.state.doc.length);
|
||||
});
|
||||
|
||||
it('toggling inline code should both create and navigate out of an inline code region', async () => {
|
||||
const initialDocText = 'Testing...\n\n';
|
||||
const editor = await createEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []);
|
||||
|
||||
toggleCode(editor);
|
||||
editor.dispatch(editor.state.replaceSelection('f(x) = ...'));
|
||||
toggleCode(editor);
|
||||
|
||||
editor.dispatch(editor.state.replaceSelection(' is a function.'));
|
||||
expect(editor.state.doc.toString()).toBe('Testing...\n\n`f(x) = ...` is a function.');
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -101,56 +101,56 @@ describe('markdownCommands.toggleList', () => {
|
||||
// );
|
||||
// });
|
||||
|
||||
// it('should not preserve indentation when removing sublists', async () => {
|
||||
// const preSubListText = '# List test\n * This\n * is\n';
|
||||
// const initialDocText = `${preSubListText}\t1. a\n\t2. test\n * of list toggling`;
|
||||
it('should not preserve indentation when removing sublists', async () => {
|
||||
const preSubListText = '# List test\n * This\n * is\n';
|
||||
const initialDocText = `${preSubListText}\t1. a\n\t2. test\n * of list toggling`;
|
||||
|
||||
// const editor = await createEditor(
|
||||
// initialDocText,
|
||||
// EditorSelection.range(preSubListText.length, `${preSubListText}\t1. a\n\t2. test`.length),
|
||||
// ['ATXHeading1', 'BulletList', 'OrderedList']
|
||||
// );
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.range(preSubListText.length, `${preSubListText}\t1. a\n\t2. test`.length),
|
||||
['ATXHeading1', 'BulletList', 'OrderedList']
|
||||
);
|
||||
|
||||
// // Indentation should not be preserved when removing lists
|
||||
// toggleList(ListType.OrderedList)(editor);
|
||||
// expect(editor.state.selection.main.from).toBe(preSubListText.length);
|
||||
// expect(editor.state.doc.toString()).toBe(
|
||||
// '# List test\n * This\n * is\na\ntest\n * of list toggling'
|
||||
// );
|
||||
// Indentation should not be preserved when removing lists
|
||||
toggleList(ListType.OrderedList)(editor);
|
||||
expect(editor.state.selection.main.from).toBe(preSubListText.length);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'# List test\n * This\n * is\na\ntest\n * of list toggling'
|
||||
);
|
||||
|
||||
// // Put the cursor in the middle of the list
|
||||
// editor.dispatch({ selection: EditorSelection.cursor(preSubListText.length) });
|
||||
// Put the cursor in the middle of the list
|
||||
editor.dispatch({ selection: EditorSelection.cursor(preSubListText.length) });
|
||||
|
||||
// // Sublists should be changed
|
||||
// toggleList(ListType.CheckList)(editor);
|
||||
// const expectedChecklistPart =
|
||||
// '# List test\n - [ ] This\n - [ ] is\n - [ ] a\n - [ ] test\n - [ ] of list toggling';
|
||||
// expect(editor.state.doc.toString()).toBe(
|
||||
// expectedChecklistPart
|
||||
// );
|
||||
// Sublists should be changed
|
||||
toggleList(ListType.CheckList)(editor);
|
||||
const expectedChecklistPart =
|
||||
'# List test\n - [ ] This\n - [ ] is\n - [ ] a\n - [ ] test\n - [ ] of list toggling';
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
expectedChecklistPart
|
||||
);
|
||||
|
||||
// editor.dispatch({ selection: EditorSelection.cursor(editor.state.doc.length) });
|
||||
// editor.dispatch(editor.state.replaceSelection('\n\n\n'));
|
||||
editor.dispatch({ selection: EditorSelection.cursor(editor.state.doc.length) });
|
||||
editor.dispatch(editor.state.replaceSelection('\n\n\n'));
|
||||
|
||||
// // toggleList should also create a new list if the cursor is on an empty line.
|
||||
// toggleList(ListType.OrderedList)(editor);
|
||||
// editor.dispatch(editor.state.replaceSelection('Test.\n2. Test2\n3. Test3'));
|
||||
// toggleList should also create a new list if the cursor is on an empty line.
|
||||
toggleList(ListType.OrderedList)(editor);
|
||||
editor.dispatch(editor.state.replaceSelection('Test.\n2. Test2\n3. Test3'));
|
||||
|
||||
// expect(editor.state.doc.toString()).toBe(
|
||||
// `${expectedChecklistPart}\n\n\n1. Test.\n2. Test2\n3. Test3`
|
||||
// );
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
`${expectedChecklistPart}\n\n\n1. Test.\n2. Test2\n3. Test3`
|
||||
);
|
||||
|
||||
// toggleList(ListType.CheckList)(editor);
|
||||
// expect(editor.state.doc.toString()).toBe(
|
||||
// `${expectedChecklistPart}\n\n\n- [ ] Test.\n- [ ] Test2\n- [ ] Test3`
|
||||
// );
|
||||
toggleList(ListType.CheckList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
`${expectedChecklistPart}\n\n\n- [ ] Test.\n- [ ] Test2\n- [ ] Test3`
|
||||
);
|
||||
|
||||
// // The entire checklist should have been selected (and thus will now be indented)
|
||||
// increaseIndent(editor);
|
||||
// expect(editor.state.doc.toString()).toBe(
|
||||
// `${expectedChecklistPart}\n\n\n\t- [ ] Test.\n\t- [ ] Test2\n\t- [ ] Test3`
|
||||
// );
|
||||
// });
|
||||
// The entire checklist should have been selected (and thus will now be indented)
|
||||
increaseIndent(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
`${expectedChecklistPart}\n\n\n\t- [ ] Test.\n\t- [ ] Test2\n\t- [ ] Test3`
|
||||
);
|
||||
});
|
||||
|
||||
it('should toggle a numbered list without changing its sublists', async () => {
|
||||
const initialDocText = '1. Foo\n2. Bar\n3. Baz\n\t- Test\n\t- of\n\t- sublists\n4. Foo';
|
||||
|
@@ -52,7 +52,7 @@ const styles = StyleSheet.create({
|
||||
|
||||
export default class SelectDateTimeDialog extends React.PureComponent<any, any> {
|
||||
|
||||
public constructor(props: any) {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@@ -67,37 +67,37 @@ export default class SelectDateTimeDialog extends React.PureComponent<any, any>
|
||||
this.onSetDate = this.onSetDate.bind(this);
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillReceiveProps(newProps: any) {
|
||||
UNSAFE_componentWillReceiveProps(newProps: any) {
|
||||
if (newProps.date !== this.state.date) {
|
||||
this.setState({ date: newProps.date });
|
||||
}
|
||||
}
|
||||
|
||||
public onAccept() {
|
||||
onAccept() {
|
||||
if (this.props.onAccept) this.props.onAccept(this.state.date);
|
||||
}
|
||||
|
||||
public onReject() {
|
||||
onReject() {
|
||||
if (this.props.onReject) this.props.onReject();
|
||||
}
|
||||
|
||||
public onClear() {
|
||||
onClear() {
|
||||
if (this.props.onAccept) this.props.onAccept(null);
|
||||
}
|
||||
|
||||
public onPickerConfirm(selectedDate: Date) {
|
||||
onPickerConfirm(selectedDate: Date) {
|
||||
this.setState({ date: selectedDate, showPicker: false });
|
||||
}
|
||||
|
||||
public onPickerCancel() {
|
||||
onPickerCancel() {
|
||||
this.setState({ showPicker: false });
|
||||
}
|
||||
|
||||
public onSetDate() {
|
||||
onSetDate() {
|
||||
this.setState({ showPicker: true });
|
||||
}
|
||||
|
||||
public renderContent() {
|
||||
renderContent() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
return (
|
||||
@@ -118,7 +118,7 @@ export default class SelectDateTimeDialog extends React.PureComponent<any, any>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const modalVisible = this.props.shown;
|
||||
|
||||
if (!modalVisible) return null;
|
||||
|
@@ -4,7 +4,7 @@ import { Dimensions } from 'react-native';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
|
||||
class SideMenuComponent extends SideMenu_ {
|
||||
public onLayoutChange(e: any) {
|
||||
onLayoutChange(e: any) {
|
||||
const { width, height } = e.nativeEvent.layout;
|
||||
const openMenuOffsetPercentage = this.props.openMenuOffset / Dimensions.get('window').width;
|
||||
const openMenuOffset = width * openMenuOffsetPercentage;
|
||||
|
@@ -25,7 +25,7 @@ class AppNavComponent extends Component<Props, State> {
|
||||
private keyboardDidHideListener: EmitterSubscription|null = null;
|
||||
private keyboardWillChangeFrameListener: EmitterSubscription|null = null;
|
||||
|
||||
public constructor(props: Props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.previousRouteName_ = null;
|
||||
@@ -35,7 +35,7 @@ class AppNavComponent extends Component<Props, State> {
|
||||
};
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
if (Platform.OS === 'ios') {
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide.bind(this));
|
||||
@@ -43,7 +43,7 @@ class AppNavComponent extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
componentWillUnmount() {
|
||||
this.keyboardDidShowListener?.remove();
|
||||
this.keyboardDidHideListener?.remove();
|
||||
this.keyboardWillChangeFrameListener?.remove();
|
||||
@@ -53,15 +53,15 @@ class AppNavComponent extends Component<Props, State> {
|
||||
this.keyboardWillChangeFrameListener = null;
|
||||
}
|
||||
|
||||
public keyboardDidShow() {
|
||||
keyboardDidShow() {
|
||||
this.setState({ autoCompletionBarExtraHeight: 30 });
|
||||
}
|
||||
|
||||
public keyboardDidHide() {
|
||||
keyboardDidHide() {
|
||||
this.setState({ autoCompletionBarExtraHeight: 0 });
|
||||
}
|
||||
|
||||
private keyboardWillChangeFrame = (evt: KeyboardEvent) => {
|
||||
keyboardWillChangeFrame = (evt: KeyboardEvent) => {
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
|
||||
// If the keyboard isn't as wide as the window, the floating keyboard is diabled.
|
||||
@@ -71,7 +71,7 @@ class AppNavComponent extends Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
if (!this.props.route) throw new Error('Route must not be null');
|
||||
|
||||
// Note: certain screens are kept into memory, in particular Notes and Search
|
||||
|
@@ -25,12 +25,12 @@ const testCases: testCase[] = [
|
||||
|
||||
describe('getResponsiveValue', () => {
|
||||
|
||||
test('should throw exception if value map is an empty object', () => {
|
||||
test('Should throw exception if value map is an empty object', () => {
|
||||
const input = {};
|
||||
expect(() => getResponsiveValue(input)).toThrow('valueMap cannot be an empty object!');
|
||||
});
|
||||
|
||||
test('should return correct values', () => {
|
||||
test('Should return correct values', () => {
|
||||
const mockReturnValues = [
|
||||
{ width: 400 },
|
||||
{ width: 480 },
|
||||
|
@@ -25,7 +25,7 @@ import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent {
|
||||
public static navigationOptions(): any {
|
||||
static navigationOptions(): any {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
};
|
||||
}
|
||||
|
||||
public async checkFilesystemPermission() {
|
||||
async checkFilesystemPermission() {
|
||||
if (Platform.OS !== 'android') {
|
||||
// Not implemented yet
|
||||
return true;
|
||||
@@ -212,11 +212,11 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
});
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
public styles() {
|
||||
styles() {
|
||||
const themeId = this.props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
@@ -376,7 +376,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
BackButtonService.removeHandler(this.handleBackButtonPress);
|
||||
}
|
||||
|
||||
public renderHeader(key: string, title: string) {
|
||||
renderHeader(key: string, title: string) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
return (
|
||||
<View key={key} style={this.styles().headerWrapperStyle} onLayout={(event: any) => this.onHeaderLayout(key, event)}>
|
||||
@@ -410,7 +410,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
);
|
||||
}
|
||||
|
||||
public sectionToComponent(key: string, section: any, settings: any) {
|
||||
sectionToComponent(key: string, section: any, settings: any) {
|
||||
const settingComps = [];
|
||||
|
||||
for (let i = 0; i < section.metadatas.length; i++) {
|
||||
@@ -474,7 +474,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
return !hasDescription ? this.styles().settingContainer : this.styles().settingContainerNoBottomBorder;
|
||||
}
|
||||
|
||||
public settingToComponent(key: string, value: any) {
|
||||
settingToComponent(key: string, value: any) {
|
||||
const themeId = this.props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
const output: any = null;
|
||||
@@ -599,7 +599,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
return output;
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const settings = this.state.settings;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
@@ -51,11 +51,11 @@ const emptyArray: any[] = [];
|
||||
const logger = Logger.create('screens/Note');
|
||||
|
||||
class NoteScreenComponent extends BaseScreenComponent {
|
||||
public static navigationOptions(): any {
|
||||
static navigationOptions(): any {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
note: Note.new(),
|
||||
@@ -280,7 +280,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
});
|
||||
}
|
||||
|
||||
private screenHeader_undoButtonPress() {
|
||||
screenHeader_undoButtonPress() {
|
||||
if (this.useEditorBeta()) {
|
||||
this.editorRef.current.undo();
|
||||
} else {
|
||||
@@ -288,7 +288,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private screenHeader_redoButtonPress() {
|
||||
screenHeader_redoButtonPress() {
|
||||
if (this.useEditorBeta()) {
|
||||
this.editorRef.current.redo();
|
||||
} else {
|
||||
@@ -296,13 +296,13 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public undoState(noteBody: string = null) {
|
||||
undoState(noteBody: string = null) {
|
||||
return {
|
||||
body: noteBody === null ? this.state.note.body : noteBody,
|
||||
};
|
||||
}
|
||||
|
||||
public styles() {
|
||||
styles() {
|
||||
const themeId = this.props.themeId;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
@@ -392,11 +392,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return this.styles_[cacheKey];
|
||||
}
|
||||
|
||||
public isModified() {
|
||||
isModified() {
|
||||
return shared.isModified(this);
|
||||
}
|
||||
|
||||
public async requestGeoLocationPermissions() {
|
||||
async requestGeoLocationPermissions() {
|
||||
if (!Setting.value('trackLocation')) return;
|
||||
|
||||
const response = await checkPermissions(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, {
|
||||
@@ -413,7 +413,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
async componentDidMount() {
|
||||
BackButtonService.addHandler(this.backHandler);
|
||||
NavService.addHandler(this.navHandler);
|
||||
|
||||
@@ -436,11 +436,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
void this.requestGeoLocationPermissions();
|
||||
}
|
||||
|
||||
public onMarkForDownload(event: any) {
|
||||
onMarkForDownload(event: any) {
|
||||
void ResourceFetcher.instance().markForDownload(event.resourceId);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: any) {
|
||||
componentDidUpdate(prevProps: any) {
|
||||
if (this.doFocusUpdate_) {
|
||||
this.doFocusUpdate_ = false;
|
||||
this.focusUpdate();
|
||||
@@ -454,7 +454,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
componentWillUnmount() {
|
||||
BackButtonService.removeHandler(this.backHandler);
|
||||
NavService.removeHandler(this.navHandler);
|
||||
|
||||
@@ -467,13 +467,13 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
if (this.undoRedoService_) this.undoRedoService_.off('stackChange', this.undoRedoService_stackChange);
|
||||
}
|
||||
|
||||
private title_changeText(text: string) {
|
||||
title_changeText(text: string) {
|
||||
shared.noteComponent_change(this, 'title', text);
|
||||
this.setState({ newAndNoTitleChangeNoteId: null });
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
private body_changeText(text: string) {
|
||||
body_changeText(text: string) {
|
||||
if (!this.undoRedoService_.canUndo) {
|
||||
this.undoRedoService_.push(this.undoState());
|
||||
} else {
|
||||
@@ -484,7 +484,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
private body_selectionChange(event: any) {
|
||||
body_selectionChange(event: any) {
|
||||
if (this.useEditorBeta()) {
|
||||
this.selection = event.selection;
|
||||
} else {
|
||||
@@ -492,34 +492,34 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public makeSaveAction() {
|
||||
makeSaveAction() {
|
||||
return async () => {
|
||||
return shared.saveNoteButton_press(this, null, null);
|
||||
};
|
||||
}
|
||||
|
||||
public saveActionQueue(noteId: string) {
|
||||
saveActionQueue(noteId: string) {
|
||||
if (!this.saveActionQueues_[noteId]) {
|
||||
this.saveActionQueues_[noteId] = new AsyncActionQueue(500);
|
||||
}
|
||||
return this.saveActionQueues_[noteId];
|
||||
}
|
||||
|
||||
public scheduleSave() {
|
||||
scheduleSave() {
|
||||
this.saveActionQueue(this.state.note.id).push(this.makeSaveAction());
|
||||
}
|
||||
|
||||
private async saveNoteButton_press(folderId: string = null) {
|
||||
async saveNoteButton_press(folderId: string = null) {
|
||||
await shared.saveNoteButton_press(this, folderId, null);
|
||||
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
|
||||
public async saveOneProperty(name: string, value: any) {
|
||||
async saveOneProperty(name: string, value: any) {
|
||||
await shared.saveOneProperty(this, name, value);
|
||||
}
|
||||
|
||||
private async deleteNote_onPress() {
|
||||
async deleteNote_onPress() {
|
||||
const note = this.state.note;
|
||||
if (!note.id) return;
|
||||
|
||||
@@ -546,7 +546,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return result;
|
||||
}
|
||||
|
||||
public async imageDimensions(uri: string) {
|
||||
async imageDimensions(uri: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Image.getSize(
|
||||
uri,
|
||||
@@ -560,7 +560,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
});
|
||||
}
|
||||
|
||||
public async resizeImage(localFilePath: string, targetPath: string, mimeType: string) {
|
||||
async resizeImage(localFilePath: string, targetPath: string, mimeType: string) {
|
||||
const maxSize = Resource.IMAGE_MAX_DIMENSION;
|
||||
|
||||
const dimensions: any = await this.imageDimensions(localFilePath);
|
||||
@@ -609,7 +609,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async attachFile(pickerResponse: any, fileType: string) {
|
||||
async attachFile(pickerResponse: any, fileType: string) {
|
||||
if (!pickerResponse) {
|
||||
// User has cancelled
|
||||
return;
|
||||
@@ -727,11 +727,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private takePhoto_onPress() {
|
||||
takePhoto_onPress() {
|
||||
this.setState({ showCamera: true });
|
||||
}
|
||||
|
||||
private cameraView_onPhoto(data: any) {
|
||||
cameraView_onPhoto(data: any) {
|
||||
void this.attachFile(
|
||||
{
|
||||
uri: data.uri,
|
||||
@@ -743,7 +743,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ showCamera: false });
|
||||
}
|
||||
|
||||
private cameraView_onCancel() {
|
||||
cameraView_onCancel() {
|
||||
this.setState({ showCamera: false });
|
||||
}
|
||||
|
||||
@@ -754,30 +754,34 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private toggleIsTodo_onPress() {
|
||||
toggleIsTodo_onPress() {
|
||||
shared.toggleIsTodo_onPress(this);
|
||||
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
private tags_onPress() {
|
||||
tags_onPress() {
|
||||
if (!this.state.note || !this.state.note.id) return;
|
||||
|
||||
this.setState({ noteTagDialogShown: true });
|
||||
}
|
||||
|
||||
private async share_onPress() {
|
||||
async share_onPress() {
|
||||
await Share.share({
|
||||
message: `${this.state.note.title}\n\n${this.state.note.body}`,
|
||||
title: this.state.note.title,
|
||||
});
|
||||
}
|
||||
|
||||
private properties_onPress() {
|
||||
properties_onPress() {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_OPEN' });
|
||||
}
|
||||
|
||||
public async onAlarmDialogAccept(date: Date) {
|
||||
setAlarm_onPress() {
|
||||
this.setState({ alarmDialogShown: true });
|
||||
}
|
||||
|
||||
async onAlarmDialogAccept(date: Date) {
|
||||
const newNote = Object.assign({}, this.state.note);
|
||||
newNote.todo_due = date ? date.getTime() : 0;
|
||||
|
||||
@@ -786,11 +790,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ alarmDialogShown: false });
|
||||
}
|
||||
|
||||
public onAlarmDialogReject() {
|
||||
onAlarmDialogReject() {
|
||||
this.setState({ alarmDialogShown: false });
|
||||
}
|
||||
|
||||
private async showOnMap_onPress() {
|
||||
async showOnMap_onPress() {
|
||||
if (!this.state.note.id) return;
|
||||
|
||||
const note = await Note.load(this.state.note.id);
|
||||
@@ -803,7 +807,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private async showSource_onPress() {
|
||||
async showSource_onPress() {
|
||||
if (!this.state.note.id) return;
|
||||
|
||||
const note = await Note.load(this.state.note.id);
|
||||
@@ -814,12 +818,12 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private copyMarkdownLink_onPress() {
|
||||
copyMarkdownLink_onPress() {
|
||||
const note = this.state.note;
|
||||
Clipboard.setString(Note.markdownTag(note));
|
||||
}
|
||||
|
||||
public sideMenuOptions() {
|
||||
sideMenuOptions() {
|
||||
const note = this.state.note;
|
||||
if (!note) return [];
|
||||
|
||||
@@ -850,7 +854,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async showAttachMenu() {
|
||||
async showAttachMenu() {
|
||||
const buttons = [];
|
||||
|
||||
// On iOS, it will show "local files", which means certain files saved from the browser
|
||||
@@ -871,7 +875,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
if (buttonId === 'attachPhoto') void this.attachPhoto_onPress();
|
||||
}
|
||||
|
||||
public menuOptions() {
|
||||
menuOptions() {
|
||||
const note = this.state.note;
|
||||
const isTodo = note && !!note.is_todo;
|
||||
const isSaved = note && note.id;
|
||||
@@ -955,11 +959,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return output;
|
||||
}
|
||||
|
||||
private async todoCheckbox_change(checked: boolean) {
|
||||
async todoCheckbox_change(checked: boolean) {
|
||||
await this.saveOneProperty('todo_completed', checked ? time.unixMs() : 0);
|
||||
}
|
||||
|
||||
public scheduleFocusUpdate() {
|
||||
scheduleFocusUpdate() {
|
||||
if (this.focusUpdateIID_) shim.clearTimeout(this.focusUpdateIID_);
|
||||
|
||||
this.focusUpdateIID_ = shim.setTimeout(() => {
|
||||
@@ -968,7 +972,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public focusUpdate() {
|
||||
focusUpdate() {
|
||||
if (this.focusUpdateIID_) shim.clearTimeout(this.focusUpdateIID_);
|
||||
this.focusUpdateIID_ = null;
|
||||
|
||||
@@ -986,7 +990,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
// }
|
||||
}
|
||||
|
||||
private async folderPickerOptions_valueChanged(itemValue: any) {
|
||||
async folderPickerOptions_valueChanged(itemValue: any) {
|
||||
const note = this.state.note;
|
||||
const isProvisionalNote = this.props.provisionalNoteIds.includes(note.id);
|
||||
|
||||
@@ -1007,7 +1011,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
});
|
||||
}
|
||||
|
||||
public folderPickerOptions() {
|
||||
folderPickerOptions() {
|
||||
const options = {
|
||||
enabled: true,
|
||||
selectedFolderId: this.state.folder ? this.state.folder.id : null,
|
||||
@@ -1020,7 +1024,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return this.folderPickerOptions_;
|
||||
}
|
||||
|
||||
public onBodyViewerLoadEnd() {
|
||||
onBodyViewerLoadEnd() {
|
||||
shim.setTimeout(() => {
|
||||
this.setState({ HACK_webviewLoadingState: 1 });
|
||||
shim.setTimeout(() => {
|
||||
@@ -1029,11 +1033,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}, 5);
|
||||
}
|
||||
|
||||
public onBodyViewerCheckboxChange(newBody: string) {
|
||||
onBodyViewerCheckboxChange(newBody: string) {
|
||||
void this.saveOneProperty('body', newBody);
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
if (this.state.isLoading) {
|
||||
return (
|
||||
<View style={this.styles().screen}>
|
||||
|
@@ -21,7 +21,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
|
||||
private onAppStateChangeSub_: NativeEventSubscription = null;
|
||||
|
||||
public constructor() {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onAppStateChange_ = async () => {
|
||||
@@ -78,7 +78,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
};
|
||||
}
|
||||
|
||||
public styles() {
|
||||
styles() {
|
||||
if (!this.styles_) this.styles_ = {};
|
||||
const themeId = this.props.themeId;
|
||||
const cacheKey = themeId;
|
||||
@@ -96,24 +96,24 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
return this.styles_[cacheKey];
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
async componentDidMount() {
|
||||
BackButtonService.addHandler(this.backHandler);
|
||||
await this.refreshNotes();
|
||||
this.onAppStateChangeSub_ = RNAppState.addEventListener('change', this.onAppStateChange_);
|
||||
}
|
||||
|
||||
public async componentWillUnmount() {
|
||||
async componentWillUnmount() {
|
||||
if (this.onAppStateChangeSub_) this.onAppStateChangeSub_.remove();
|
||||
BackButtonService.removeHandler(this.backHandler);
|
||||
}
|
||||
|
||||
public async componentDidUpdate(prevProps: any) {
|
||||
async componentDidUpdate(prevProps: any) {
|
||||
if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId !== this.props.selectedFolderId || prevProps.selectedTagId !== this.props.selectedTagId || prevProps.selectedSmartFilterId !== this.props.selectedSmartFilterId || prevProps.notesParentType !== this.props.notesParentType) {
|
||||
await this.refreshNotes(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
public async refreshNotes(props: any = null) {
|
||||
async refreshNotes(props: any = null) {
|
||||
if (props === null) props = this.props;
|
||||
|
||||
const options = {
|
||||
@@ -149,7 +149,36 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
});
|
||||
}
|
||||
|
||||
public newNoteNavigate = async (folderId: string, isTodo: boolean) => {
|
||||
deleteFolder_onPress(folderId: string) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
dialogs.confirm(this, _('Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.')).then((ok: boolean) => {
|
||||
if (!ok) return;
|
||||
|
||||
Folder.delete(folderId)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then(() => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Notes',
|
||||
smartFilterId: 'c3176726992c11e9ac940492261af972',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.catch(error => {
|
||||
alert(error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
editFolder_onPress(folderId: string) {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Folder',
|
||||
folderId: folderId,
|
||||
});
|
||||
}
|
||||
|
||||
newNoteNavigate = async (folderId: string, isTodo: boolean) => {
|
||||
const newNote = await Note.save({
|
||||
parent_id: folderId,
|
||||
is_todo: isTodo ? 1 : 0,
|
||||
@@ -162,7 +191,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
});
|
||||
};
|
||||
|
||||
public parentItem(props: any = null) {
|
||||
parentItem(props: any = null) {
|
||||
if (!props) props = this.props;
|
||||
|
||||
let output = null;
|
||||
@@ -179,7 +208,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
return output;
|
||||
}
|
||||
|
||||
public folderPickerOptions() {
|
||||
folderPickerOptions() {
|
||||
const options = {
|
||||
enabled: this.props.noteSelectionEnabled,
|
||||
mustSelect: true,
|
||||
@@ -191,7 +220,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
return this.folderPickerOptions_;
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
const parent = this.parentItem();
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
|
@@ -27,11 +27,11 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
private styles_: any = {};
|
||||
private scheduleSearchTimer_: any = null;
|
||||
|
||||
public static navigationOptions() {
|
||||
static navigationOptions() {
|
||||
return { header: null } as any;
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
query: '',
|
||||
@@ -39,7 +39,7 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
};
|
||||
}
|
||||
|
||||
public styles() {
|
||||
styles() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
if (this.styles_[this.props.themeId]) return this.styles_[this.props.themeId];
|
||||
@@ -72,17 +72,17 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
return this.styles_[this.props.themeId];
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
componentDidMount() {
|
||||
this.setState({ query: this.props.query });
|
||||
void this.refreshSearch(this.props.query);
|
||||
this.isMounted_ = true;
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
componentWillUnmount() {
|
||||
this.isMounted_ = false;
|
||||
}
|
||||
|
||||
private clearButton_press() {
|
||||
clearButton_press() {
|
||||
this.props.dispatch({
|
||||
type: 'SEARCH_QUERY',
|
||||
query: '',
|
||||
@@ -92,7 +92,7 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
void this.refreshSearch('');
|
||||
}
|
||||
|
||||
public async refreshSearch(query: string = null) {
|
||||
async refreshSearch(query: string = null) {
|
||||
if (!this.props.visible) return;
|
||||
|
||||
query = gotoAnythingStyleQuery(query);
|
||||
@@ -130,7 +130,7 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ notes: notes });
|
||||
}
|
||||
|
||||
public scheduleSearch() {
|
||||
scheduleSearch() {
|
||||
if (this.scheduleSearchTimer_) clearTimeout(this.scheduleSearchTimer_);
|
||||
|
||||
this.scheduleSearchTimer_ = setTimeout(() => {
|
||||
@@ -139,7 +139,7 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private searchTextInput_changeText(text: string) {
|
||||
searchTextInput_changeText(text: string) {
|
||||
this.setState({ query: text });
|
||||
|
||||
this.props.dispatch({
|
||||
@@ -150,7 +150,7 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
this.scheduleSearch();
|
||||
}
|
||||
|
||||
public render() {
|
||||
render() {
|
||||
if (!this.isMounted_) return null;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
@@ -432,7 +432,7 @@ PODS:
|
||||
- React
|
||||
- RNSecureRandom (1.0.1):
|
||||
- React
|
||||
- RNShare (8.2.0):
|
||||
- RNShare (8.1.0):
|
||||
- React-Core
|
||||
- RNVectorIcons (9.2.0):
|
||||
- React-Core
|
||||
@@ -751,7 +751,7 @@ SPEC CHECKSUMS:
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
||||
RNShare: b089c33619bbfb0a32bc4069c858b9274e694187
|
||||
RNShare: 48b3113cd089a2be8ff0515c3ae7a46a4db8a76b
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
|
||||
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc
|
||||
|
@@ -51,7 +51,7 @@
|
||||
"react-native-fingerprint-scanner": "6.0.0",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-get-random-values": "1.8.0",
|
||||
"react-native-image-picker": "5.1.0",
|
||||
"react-native-image-picker": "5.0.2",
|
||||
"react-native-image-resizer": "1.4.5",
|
||||
"react-native-modal-datetime-picker": "14.0.1",
|
||||
"react-native-paper": "5.2.0",
|
||||
@@ -60,7 +60,7 @@
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "4.5.0",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "8.2.1",
|
||||
"react-native-share": "8.2.0",
|
||||
"react-native-side-menu-updated": "1.3.2",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
@@ -108,7 +108,7 @@
|
||||
"jsdom": "21.0.0",
|
||||
"md5-file": "5.0.0",
|
||||
"metro-react-native-babel-preset": "0.72.3",
|
||||
"nodemon": "2.0.21",
|
||||
"nodemon": "2.0.20",
|
||||
"ts-jest": "29.0.5",
|
||||
"ts-loader": "9.4.2",
|
||||
"ts-node": "10.9.1",
|
||||
|
@@ -8,7 +8,7 @@ export default class AlarmServiceDriver {
|
||||
private inAppNotificationHandler_: any = null;
|
||||
private logger_: Logger;
|
||||
|
||||
public constructor(logger: Logger) {
|
||||
constructor(logger: Logger) {
|
||||
this.logger_ = logger;
|
||||
PushNotificationIOS.addEventListener('localNotification', (instance: any) => {
|
||||
if (!this.inAppNotificationHandler_) return;
|
||||
@@ -23,19 +23,19 @@ export default class AlarmServiceDriver {
|
||||
});
|
||||
}
|
||||
|
||||
public hasPersistentNotifications() {
|
||||
hasPersistentNotifications() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public notificationIsSet() {
|
||||
notificationIsSet() {
|
||||
throw new Error('Available only for non-persistent alarms');
|
||||
}
|
||||
|
||||
public setInAppNotificationHandler(v: any) {
|
||||
setInAppNotificationHandler(v: any) {
|
||||
this.inAppNotificationHandler_ = v;
|
||||
}
|
||||
|
||||
public async hasPermissions(perm: any = null) {
|
||||
async hasPermissions(perm: any = null) {
|
||||
if (perm !== null) return perm.alert && perm.badge && perm.sound;
|
||||
|
||||
if (this.hasPermission_ !== null) return this.hasPermission_;
|
||||
@@ -49,7 +49,7 @@ export default class AlarmServiceDriver {
|
||||
});
|
||||
}
|
||||
|
||||
public async requestPermissions() {
|
||||
async requestPermissions() {
|
||||
const options: any = {
|
||||
alert: 1,
|
||||
badge: 1,
|
||||
@@ -60,11 +60,11 @@ export default class AlarmServiceDriver {
|
||||
return this.hasPermissions(newPerm);
|
||||
}
|
||||
|
||||
public async clearNotification(id: number) {
|
||||
async clearNotification(id: number) {
|
||||
PushNotificationIOS.cancelLocalNotifications({ id: `${id}` });
|
||||
}
|
||||
|
||||
public async scheduleNotification(notification: Notification) {
|
||||
async scheduleNotification(notification: Notification) {
|
||||
if (!(await this.hasPermissions())) {
|
||||
const ok = await this.requestPermissions();
|
||||
if (!ok) return;
|
||||
|
@@ -25,7 +25,7 @@
|
||||
"dependencies": {
|
||||
"chalk": "2.4.2",
|
||||
"slugify": "1.6.5",
|
||||
"yeoman-generator": "5.8.0",
|
||||
"yeoman-generator": "5.7.1",
|
||||
"yosay": "2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -192,12 +192,6 @@ export default class BaseApplication {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--safe-mode') {
|
||||
matched.isSafeMode = true;
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--open-dev-tools') {
|
||||
Setting.setConstant('flagOpenDevTools', true);
|
||||
argv.splice(0, 1);
|
||||
@@ -424,7 +418,7 @@ export default class BaseApplication {
|
||||
ResourceFetcher.instance().scheduleAutoAddResources();
|
||||
}
|
||||
|
||||
public reducerActionToString(action: any) {
|
||||
reducerActionToString(action: any) {
|
||||
const o = [action.type];
|
||||
if ('id' in action) o.push(action.id);
|
||||
if ('noteId' in action) o.push(action.noteId);
|
||||
@@ -835,10 +829,6 @@ export default class BaseApplication {
|
||||
|
||||
appLogger.info(`Client ID: ${Setting.value('clientId')}`);
|
||||
|
||||
if (initArgs?.isSafeMode) {
|
||||
Setting.setValue('isSafeMode', true);
|
||||
}
|
||||
|
||||
if (Setting.value('firstStart')) {
|
||||
// If it's a sub-profile, the locale must come from the root
|
||||
// profile.
|
||||
|
@@ -85,19 +85,19 @@ class BaseModel {
|
||||
|
||||
private static db_: JoplinDatabase;
|
||||
|
||||
public static modelType(): ModelType {
|
||||
static modelType(): ModelType {
|
||||
throw new Error('Must be overriden');
|
||||
}
|
||||
|
||||
public static tableName(): string {
|
||||
static tableName(): string {
|
||||
throw new Error('Must be overriden');
|
||||
}
|
||||
|
||||
public static setDb(db: any) {
|
||||
static setDb(db: any) {
|
||||
this.db_ = db;
|
||||
}
|
||||
|
||||
public static addModelMd(model: any): any {
|
||||
static addModelMd(model: any): any {
|
||||
if (!model) return model;
|
||||
|
||||
if (Array.isArray(model)) {
|
||||
@@ -113,22 +113,22 @@ class BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
public static logger() {
|
||||
static logger() {
|
||||
return this.db().logger();
|
||||
}
|
||||
|
||||
public static useUuid() {
|
||||
static useUuid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static byId(items: any[], id: string) {
|
||||
static byId(items: any[], id: string) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].id === id) return items[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static defaultValues(fieldNames: string[]) {
|
||||
static defaultValues(fieldNames: string[]) {
|
||||
const output: any = {};
|
||||
for (const n of fieldNames) {
|
||||
output[n] = this.db().fieldDefaultValue(this.tableName(), n);
|
||||
@@ -136,14 +136,14 @@ class BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static modelIndexById(items: any[], id: string) {
|
||||
static modelIndexById(items: any[], id: string) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].id === id) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static modelsByIds(items: any[], ids: string[]) {
|
||||
static modelsByIds(items: any[], ids: string[]) {
|
||||
const output = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (ids.indexOf(items[i].id) >= 0) {
|
||||
@@ -155,14 +155,14 @@ class BaseModel {
|
||||
|
||||
// Prefer the use of this function to compare IDs as it handles the case where
|
||||
// one ID is null and the other is "", in which case they are actually considered to be the same.
|
||||
public static idsEqual(id1: string, id2: string) {
|
||||
static idsEqual(id1: string, id2: string) {
|
||||
if (!id1 && !id2) return true;
|
||||
if (!id1 && !!id2) return false;
|
||||
if (!!id1 && !id2) return false;
|
||||
return id1 === id2;
|
||||
}
|
||||
|
||||
public static modelTypeToName(type: number) {
|
||||
static modelTypeToName(type: number) {
|
||||
for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
|
||||
const e = BaseModel.typeEnum_[i];
|
||||
if (e[1] === type) return e[0].substr(5).toLowerCase();
|
||||
@@ -170,7 +170,7 @@ class BaseModel {
|
||||
throw new Error(`Unknown model type: ${type}`);
|
||||
}
|
||||
|
||||
public static modelNameToType(name: string) {
|
||||
static modelNameToType(name: string) {
|
||||
for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
|
||||
const e = BaseModel.typeEnum_[i];
|
||||
const eName = e[0].substr(5).toLowerCase();
|
||||
@@ -179,12 +179,12 @@ class BaseModel {
|
||||
throw new Error(`Unknown model name: ${name}`);
|
||||
}
|
||||
|
||||
public static hasField(name: string) {
|
||||
static hasField(name: string) {
|
||||
const fields = this.fieldNames();
|
||||
return fields.indexOf(name) >= 0;
|
||||
}
|
||||
|
||||
public static fieldNames(withPrefix: boolean = false) {
|
||||
static fieldNames(withPrefix: boolean = false) {
|
||||
const output = this.db().tableFieldNames(this.tableName());
|
||||
if (!withPrefix) return output;
|
||||
|
||||
@@ -197,7 +197,7 @@ class BaseModel {
|
||||
return temp;
|
||||
}
|
||||
|
||||
public static fieldType(name: string, defaultValue: any = null) {
|
||||
static fieldType(name: string, defaultValue: any = null) {
|
||||
const fields = this.fields();
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
if (fields[i].name === name) return fields[i].type;
|
||||
@@ -206,11 +206,11 @@ class BaseModel {
|
||||
throw new Error(`Unknown field: ${name}`);
|
||||
}
|
||||
|
||||
public static fields(): TableField[] {
|
||||
static fields(): TableField[] {
|
||||
return this.db().tableFields(this.tableName());
|
||||
}
|
||||
|
||||
public static removeUnknownFields(model: any) {
|
||||
static removeUnknownFields(model: any) {
|
||||
const newModel: any = {};
|
||||
for (const n in model) {
|
||||
if (!model.hasOwnProperty(n)) continue;
|
||||
@@ -220,7 +220,7 @@ class BaseModel {
|
||||
return newModel;
|
||||
}
|
||||
|
||||
public static new() {
|
||||
static new() {
|
||||
const fields = this.fields();
|
||||
const output: any = {};
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
@@ -230,7 +230,7 @@ class BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static modOptions(options: any) {
|
||||
static modOptions(options: any) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
} else {
|
||||
@@ -242,7 +242,7 @@ class BaseModel {
|
||||
return options;
|
||||
}
|
||||
|
||||
public static count(options: any = null) {
|
||||
static count(options: any = null) {
|
||||
if (!options) options = {};
|
||||
let sql = `SELECT count(*) as total FROM \`${this.tableName()}\``;
|
||||
if (options.where) sql += ` WHERE ${options.where}`;
|
||||
@@ -254,19 +254,19 @@ class BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
public static load(id: string, options: any = null) {
|
||||
static load(id: string, options: any = null) {
|
||||
return this.loadByField('id', id, options);
|
||||
}
|
||||
|
||||
public static shortId(id: string) {
|
||||
static shortId(id: string) {
|
||||
return id.substr(0, 5);
|
||||
}
|
||||
|
||||
public static loadByPartialId(partialId: string) {
|
||||
static loadByPartialId(partialId: string) {
|
||||
return this.modelSelectAll(`SELECT * FROM \`${this.tableName()}\` WHERE \`id\` LIKE ?`, [`${partialId}%`]);
|
||||
}
|
||||
|
||||
public static applySqlOptions(options: any, sql: string, params: any[] = null) {
|
||||
static applySqlOptions(options: any, sql: string, params: any[] = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.order && options.order.length) {
|
||||
@@ -278,13 +278,13 @@ class BaseModel {
|
||||
return { sql: sql, params: params };
|
||||
}
|
||||
|
||||
public static async allIds(options: any = null) {
|
||||
static async allIds(options: any = null) {
|
||||
const q = this.applySqlOptions(options, `SELECT id FROM \`${this.tableName()}\``);
|
||||
const rows = await this.db().selectAll(q.sql, q.params);
|
||||
return rows.map((r: any) => r.id);
|
||||
}
|
||||
|
||||
public static async all(options: any = null) {
|
||||
static async all(options: any = null) {
|
||||
if (!options) options = {};
|
||||
if (!options.fields) options.fields = '*';
|
||||
|
||||
@@ -299,7 +299,7 @@ class BaseModel {
|
||||
return this.modelSelectAll(q.sql, q.params);
|
||||
}
|
||||
|
||||
public static async byIds(ids: string[], options: any = null) {
|
||||
static async byIds(ids: string[], options: any = null) {
|
||||
if (!ids.length) return [];
|
||||
if (!options) options = {};
|
||||
if (!options.fields) options.fields = '*';
|
||||
@@ -310,7 +310,7 @@ class BaseModel {
|
||||
return this.modelSelectAll(q.sql);
|
||||
}
|
||||
|
||||
public static async search(options: any = null) {
|
||||
static async search(options: any = null) {
|
||||
if (!options) options = {};
|
||||
if (!options.fields) options.fields = '*';
|
||||
|
||||
@@ -332,7 +332,7 @@ class BaseModel {
|
||||
return this.modelSelectAll(query.sql, query.params);
|
||||
}
|
||||
|
||||
public static modelSelectOne(sql: string, params: any[] = null) {
|
||||
static modelSelectOne(sql: string, params: any[] = null) {
|
||||
if (params === null) params = [];
|
||||
return this.db()
|
||||
.selectOne(sql, params)
|
||||
@@ -342,7 +342,7 @@ class BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
public static modelSelectAll(sql: string, params: any[] = null) {
|
||||
static modelSelectAll(sql: string, params: any[] = null) {
|
||||
if (params === null) params = [];
|
||||
return this.db()
|
||||
.selectAll(sql, params)
|
||||
@@ -352,7 +352,7 @@ class BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
public static loadByField(fieldName: string, fieldValue: any, options: any = null) {
|
||||
static loadByField(fieldName: string, fieldValue: any, options: any = null) {
|
||||
if (!options) options = {};
|
||||
if (!('caseInsensitive' in options)) options.caseInsensitive = false;
|
||||
if (!options.fields) options.fields = '*';
|
||||
@@ -361,7 +361,7 @@ class BaseModel {
|
||||
return this.modelSelectOne(sql, [fieldValue]);
|
||||
}
|
||||
|
||||
public static loadByFields(fields: any, options: any = null) {
|
||||
static loadByFields(fields: any, options: any = null) {
|
||||
if (!options) options = {};
|
||||
if (!('caseInsensitive' in options)) options.caseInsensitive = false;
|
||||
if (!options.fields) options.fields = '*';
|
||||
@@ -376,11 +376,11 @@ class BaseModel {
|
||||
return this.modelSelectOne(sql, params);
|
||||
}
|
||||
|
||||
public static loadByTitle(fieldValue: any) {
|
||||
static loadByTitle(fieldValue: any) {
|
||||
return this.modelSelectOne(`SELECT * FROM \`${this.tableName()}\` WHERE \`title\` = ?`, [fieldValue]);
|
||||
}
|
||||
|
||||
public static diffObjects(oldModel: any, newModel: any) {
|
||||
static diffObjects(oldModel: any, newModel: any) {
|
||||
const output: any = {};
|
||||
const fields = this.diffObjectsFields(oldModel, newModel);
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
@@ -390,7 +390,7 @@ class BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static diffObjectsFields(oldModel: any, newModel: any) {
|
||||
static diffObjectsFields(oldModel: any, newModel: any) {
|
||||
const output = [];
|
||||
for (const n in newModel) {
|
||||
if (!newModel.hasOwnProperty(n)) continue;
|
||||
@@ -402,13 +402,13 @@ class BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static modelsAreSame(oldModel: any, newModel: any) {
|
||||
static modelsAreSame(oldModel: any, newModel: any) {
|
||||
const diff = this.diffObjects(oldModel, newModel);
|
||||
delete diff.type_;
|
||||
return !Object.getOwnPropertyNames(diff).length;
|
||||
}
|
||||
|
||||
public static saveMutex(modelOrId: any) {
|
||||
static saveMutex(modelOrId: any) {
|
||||
const noLockMutex = {
|
||||
acquire: function(): any {
|
||||
return null;
|
||||
@@ -429,7 +429,7 @@ class BaseModel {
|
||||
return mutex;
|
||||
}
|
||||
|
||||
public static releaseSaveMutex(modelOrId: any, release: Function) {
|
||||
static releaseSaveMutex(modelOrId: any, release: Function) {
|
||||
if (!release) return;
|
||||
if (!modelOrId) return release();
|
||||
|
||||
@@ -444,7 +444,7 @@ class BaseModel {
|
||||
release();
|
||||
}
|
||||
|
||||
public static saveQuery(o: any, options: any) {
|
||||
static saveQuery(o: any, options: any) {
|
||||
let temp: any = {};
|
||||
const fieldNames = this.fieldNames();
|
||||
for (let i = 0; i < fieldNames.length; i++) {
|
||||
@@ -521,7 +521,7 @@ class BaseModel {
|
||||
return query;
|
||||
}
|
||||
|
||||
public static userSideValidation(o: any) {
|
||||
static userSideValidation(o: any) {
|
||||
if (o.id && !o.id.match(/^[a-f0-9]{32}$/)) {
|
||||
throw new Error('Validation error: ID must a 32-characters lowercase hexadecimal string');
|
||||
}
|
||||
@@ -532,7 +532,7 @@ class BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
public static async save(o: any, options: any = null) {
|
||||
static async save(o: any, options: any = null) {
|
||||
// When saving, there's a mutex per model ID. This is because the model returned from this function
|
||||
// is basically its input `o` (instead of being read from the database, for performance reasons).
|
||||
// This works well in general except if that model is saved simultaneously in two places. In that
|
||||
@@ -602,7 +602,7 @@ class BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static isNew(object: any, options: any) {
|
||||
static isNew(object: any, options: any) {
|
||||
if (options && 'isNew' in options) {
|
||||
// options.isNew can be "auto" too
|
||||
if (options.isNew === true) return true;
|
||||
@@ -612,7 +612,7 @@ class BaseModel {
|
||||
return !object.id;
|
||||
}
|
||||
|
||||
public static filterArray(models: any[]) {
|
||||
static filterArray(models: any[]) {
|
||||
const output = [];
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
output.push(this.filter(models[i]));
|
||||
@@ -620,7 +620,7 @@ class BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static filter(model: any) {
|
||||
static filter(model: any) {
|
||||
if (!model) return model;
|
||||
|
||||
const output = Object.assign({}, model);
|
||||
@@ -643,12 +643,12 @@ class BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static delete(id: string) {
|
||||
static delete(id: string) {
|
||||
if (!id) throw new Error('Cannot delete object without an ID');
|
||||
return this.db().exec(`DELETE FROM ${this.tableName()} WHERE id = ?`, [id]);
|
||||
}
|
||||
|
||||
public static async batchDelete(ids: string[], options: DeleteOptions = null) {
|
||||
static async batchDelete(ids: string[], options: DeleteOptions = null) {
|
||||
if (!ids.length) return;
|
||||
options = this.modOptions(options);
|
||||
const idFieldName = options.idFieldName ? options.idFieldName : 'id';
|
||||
@@ -656,7 +656,7 @@ class BaseModel {
|
||||
await this.db().exec(sql);
|
||||
}
|
||||
|
||||
public static db() {
|
||||
static db() {
|
||||
if (!this.db_) throw new Error('Accessing database before it has been initialised');
|
||||
return this.db_;
|
||||
}
|
||||
|
@@ -24,11 +24,11 @@ export default class ClipperServer {
|
||||
|
||||
private static instance_: ClipperServer = null;
|
||||
|
||||
public constructor() {
|
||||
constructor() {
|
||||
this.logger_ = new Logger();
|
||||
}
|
||||
|
||||
public static instance() {
|
||||
static instance() {
|
||||
if (this.instance_) return this.instance_;
|
||||
this.instance_ = new ClipperServer();
|
||||
return this.instance_;
|
||||
@@ -38,30 +38,30 @@ export default class ClipperServer {
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
public initialize(actionApi: any = null) {
|
||||
initialize(actionApi: any = null) {
|
||||
this.api_ = new Api(() => {
|
||||
return Setting.value('api.token');
|
||||
}, (action: any) => { this.dispatch(action); }, actionApi);
|
||||
}
|
||||
|
||||
public setLogger(l: Logger) {
|
||||
setLogger(l: Logger) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
public logger() {
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
public setDispatch(d: Function) {
|
||||
setDispatch(d: Function) {
|
||||
this.dispatch_ = d;
|
||||
}
|
||||
|
||||
public dispatch(action: any) {
|
||||
dispatch(action: any) {
|
||||
if (!this.dispatch_) throw new Error('dispatch not set!');
|
||||
this.dispatch_(action);
|
||||
}
|
||||
|
||||
public setStartState(v: StartState) {
|
||||
setStartState(v: StartState) {
|
||||
if (this.startState_ === v) return;
|
||||
this.startState_ = v;
|
||||
this.dispatch({
|
||||
@@ -70,7 +70,7 @@ export default class ClipperServer {
|
||||
});
|
||||
}
|
||||
|
||||
public setPort(v: number) {
|
||||
setPort(v: number) {
|
||||
if (this.port_ === v) return;
|
||||
this.port_ = v;
|
||||
this.dispatch({
|
||||
@@ -79,7 +79,7 @@ export default class ClipperServer {
|
||||
});
|
||||
}
|
||||
|
||||
public async findAvailablePort() {
|
||||
async findAvailablePort() {
|
||||
const tcpPortUsed = require('tcp-port-used');
|
||||
|
||||
let state = null;
|
||||
@@ -92,14 +92,14 @@ export default class ClipperServer {
|
||||
throw new Error('All potential ports are in use or not available.');
|
||||
}
|
||||
|
||||
public async isRunning() {
|
||||
async isRunning() {
|
||||
const tcpPortUsed = require('tcp-port-used');
|
||||
const port = Setting.value('api.port') ? Setting.value('api.port') : startPort(Setting.value('env'));
|
||||
const inUse = await tcpPortUsed.check(port);
|
||||
return inUse ? port : 0;
|
||||
}
|
||||
|
||||
public async start() {
|
||||
async start() {
|
||||
this.setPort(null);
|
||||
|
||||
this.setStartState(StartState.Starting);
|
||||
@@ -237,7 +237,7 @@ export default class ClipperServer {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
async stop() {
|
||||
this.server_.destroy();
|
||||
this.server_ = null;
|
||||
this.setStartState(StartState.Idle);
|
||||
|
@@ -29,7 +29,7 @@ export default class Cache {
|
||||
private expirableKeys_: ExpirableKeys = {};
|
||||
private recordKeyHistory_: string[] = [];
|
||||
|
||||
public constructor(maxRecords: number = 50) {
|
||||
constructor(maxRecords: number = 50) {
|
||||
this.maxRecords_ = maxRecords;
|
||||
}
|
||||
|
||||
|
@@ -63,7 +63,7 @@ class Logger {
|
||||
private lastDbCleanup_: number = time.unixMs();
|
||||
private enabled_: boolean = true;
|
||||
|
||||
public static fsDriver() {
|
||||
static fsDriver() {
|
||||
if (!Logger.fsDriver_) Logger.fsDriver_ = new FsDriverDummy();
|
||||
return Logger.fsDriver_;
|
||||
}
|
||||
@@ -103,7 +103,7 @@ class Logger {
|
||||
return this.globalLogger_;
|
||||
}
|
||||
|
||||
public static create(prefix: string): LoggerWrapper {
|
||||
static create(prefix: string): LoggerWrapper {
|
||||
return {
|
||||
debug: (...object: any[]) => this.globalLogger.log(LogLevel.Debug, prefix, ...object),
|
||||
info: (...object: any[]) => this.globalLogger.log(LogLevel.Info, prefix, ...object),
|
||||
@@ -118,15 +118,15 @@ class Logger {
|
||||
return previous;
|
||||
}
|
||||
|
||||
public level() {
|
||||
level() {
|
||||
return this.level_;
|
||||
}
|
||||
|
||||
public targets() {
|
||||
targets() {
|
||||
return this.targets_;
|
||||
}
|
||||
|
||||
public addTarget(type: TargetType, options: TargetOptions = null) {
|
||||
addTarget(type: TargetType, options: TargetOptions = null) {
|
||||
const target = { type: type };
|
||||
for (const n in options) {
|
||||
if (!options.hasOwnProperty(n)) continue;
|
||||
@@ -136,7 +136,7 @@ class Logger {
|
||||
this.targets_.push(target);
|
||||
}
|
||||
|
||||
public objectToString(object: any) {
|
||||
objectToString(object: any) {
|
||||
let output = '';
|
||||
|
||||
if (typeof object === 'object') {
|
||||
@@ -157,7 +157,7 @@ class Logger {
|
||||
return output;
|
||||
}
|
||||
|
||||
public objectsToString(...object: any[]) {
|
||||
objectsToString(...object: any[]) {
|
||||
const output = [];
|
||||
for (let i = 0; i < object.length; i++) {
|
||||
output.push(`"${this.objectToString(object[i])}"`);
|
||||
@@ -165,7 +165,7 @@ class Logger {
|
||||
return output.join(', ');
|
||||
}
|
||||
|
||||
public static databaseCreateTableSql() {
|
||||
static databaseCreateTableSql() {
|
||||
const output = `
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
@@ -179,7 +179,7 @@ class Logger {
|
||||
}
|
||||
|
||||
// Only for database at the moment
|
||||
public async lastEntries(limit: number = 100, options: any = null) {
|
||||
async lastEntries(limit: number = 100, options: any = null) {
|
||||
if (options === null) options = {};
|
||||
if (!options.levels) options.levels = [LogLevel.Debug, LogLevel.Info, LogLevel.Warn, LogLevel.Error];
|
||||
if (!options.levels.length) return [];
|
||||
@@ -195,7 +195,7 @@ class Logger {
|
||||
return [];
|
||||
}
|
||||
|
||||
public targetLevel(target: Target) {
|
||||
targetLevel(target: Target) {
|
||||
if ('level' in target) return target.level;
|
||||
return this.level();
|
||||
}
|
||||
@@ -287,20 +287,20 @@ class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
public error(...object: any[]) {
|
||||
error(...object: any[]) {
|
||||
return this.log(LogLevel.Error, null, ...object);
|
||||
}
|
||||
public warn(...object: any[]) {
|
||||
warn(...object: any[]) {
|
||||
return this.log(LogLevel.Warn, null, ...object);
|
||||
}
|
||||
public info(...object: any[]) {
|
||||
info(...object: any[]) {
|
||||
return this.log(LogLevel.Info, null, ...object);
|
||||
}
|
||||
public debug(...object: any[]) {
|
||||
debug(...object: any[]) {
|
||||
return this.log(LogLevel.Debug, null, ...object);
|
||||
}
|
||||
|
||||
public static levelStringToId(s: string) {
|
||||
static levelStringToId(s: string) {
|
||||
if (s === 'none') return LogLevel.None;
|
||||
if (s === 'error') return LogLevel.Error;
|
||||
if (s === 'warn') return LogLevel.Warn;
|
||||
@@ -309,7 +309,7 @@ class Logger {
|
||||
throw new Error(`Unknown log level: ${s}`);
|
||||
}
|
||||
|
||||
public static levelIdToString(id: LogLevel) {
|
||||
static levelIdToString(id: LogLevel) {
|
||||
if (id === LogLevel.None) return 'none';
|
||||
if (id === LogLevel.Error) return 'error';
|
||||
if (id === LogLevel.Warn) return 'warn';
|
||||
@@ -318,7 +318,7 @@ class Logger {
|
||||
throw new Error(`Unknown level ID: ${id}`);
|
||||
}
|
||||
|
||||
public static levelIds() {
|
||||
static levelIds() {
|
||||
return [LogLevel.None, LogLevel.Error, LogLevel.Warn, LogLevel.Info, LogLevel.Debug];
|
||||
}
|
||||
|
||||
|
@@ -12,20 +12,20 @@ export default class SyncTargetOneDrive extends BaseSyncTarget {
|
||||
|
||||
private api_: any;
|
||||
|
||||
public static id() {
|
||||
static id() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public constructor(db: any, options: any = null) {
|
||||
constructor(db: any, options: any = null) {
|
||||
super(db, options);
|
||||
this.api_ = null;
|
||||
}
|
||||
|
||||
public static targetName() {
|
||||
static targetName() {
|
||||
return 'onedrive';
|
||||
}
|
||||
|
||||
public static label() {
|
||||
static label() {
|
||||
return _('OneDrive');
|
||||
}
|
||||
|
||||
@@ -37,30 +37,30 @@ export default class SyncTargetOneDrive extends BaseSyncTarget {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async isAuthenticated() {
|
||||
async isAuthenticated() {
|
||||
return !!this.api().auth();
|
||||
}
|
||||
|
||||
public syncTargetId() {
|
||||
syncTargetId() {
|
||||
return SyncTargetOneDrive.id();
|
||||
}
|
||||
|
||||
public isTesting() {
|
||||
isTesting() {
|
||||
const p = parameters();
|
||||
return !!p.oneDriveTest;
|
||||
}
|
||||
|
||||
public oneDriveParameters() {
|
||||
oneDriveParameters() {
|
||||
const p = parameters();
|
||||
if (p.oneDriveTest) return p.oneDriveTest;
|
||||
return p.oneDrive;
|
||||
}
|
||||
|
||||
public authRouteName() {
|
||||
authRouteName() {
|
||||
return 'OneDriveLogin';
|
||||
}
|
||||
|
||||
public api() {
|
||||
api() {
|
||||
if (this.isTesting()) {
|
||||
return this.fileApi_.driver().api();
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export default class SyncTargetOneDrive extends BaseSyncTarget {
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
public async initFileApi() {
|
||||
async initFileApi() {
|
||||
let context = Setting.value(`sync.${this.syncTargetId()}.context`);
|
||||
context = context === '' ? null : JSON.parse(context);
|
||||
let accountProperties = context ? context.accountProperties : null;
|
||||
@@ -103,17 +103,13 @@ export default class SyncTargetOneDrive extends BaseSyncTarget {
|
||||
}
|
||||
this.api_.setAccountProperties(accountProperties);
|
||||
const appDir = await this.api().appDirectory();
|
||||
// the appDir might contain non-ASCII characters
|
||||
// /[^\u0021-\u00ff]/ is used in Node.js to detect the unescaped characters.
|
||||
// See https://github.com/nodejs/node/blob/bbbf97b6dae63697371082475dc8651a6a220336/lib/_http_client.js#L176
|
||||
const baseDir = RegExp(/[^\u0021-\u00ff]/).exec(appDir) !== null ? encodeURI(appDir) : appDir;
|
||||
const fileApi = new FileApi(baseDir, new FileApiDriverOneDrive(this.api()));
|
||||
const fileApi = new FileApi(appDir, new FileApiDriverOneDrive(this.api()));
|
||||
fileApi.setSyncTargetId(this.syncTargetId());
|
||||
fileApi.setLogger(this.logger());
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
public async initSynchronizer() {
|
||||
async initSynchronizer() {
|
||||
try {
|
||||
if (!(await this.isAuthenticated())) throw new Error('User is not authentified');
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
|
@@ -91,31 +91,31 @@ export default class Synchronizer {
|
||||
this.apiCall = this.apiCall.bind(this);
|
||||
}
|
||||
|
||||
public state() {
|
||||
state() {
|
||||
return this.state_;
|
||||
}
|
||||
|
||||
public db() {
|
||||
db() {
|
||||
return this.db_;
|
||||
}
|
||||
|
||||
public api() {
|
||||
api() {
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
public clientId() {
|
||||
clientId() {
|
||||
return this.clientId_;
|
||||
}
|
||||
|
||||
public setLogger(l: Logger) {
|
||||
setLogger(l: Logger) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
public logger() {
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
public lockHandler() {
|
||||
lockHandler() {
|
||||
if (this.lockHandler_) return this.lockHandler_;
|
||||
this.lockHandler_ = new LockHandler(this.api());
|
||||
return this.lockHandler_;
|
||||
@@ -127,13 +127,13 @@ export default class Synchronizer {
|
||||
return this.lockClientType_;
|
||||
}
|
||||
|
||||
public migrationHandler() {
|
||||
migrationHandler() {
|
||||
if (this.migrationHandler_) return this.migrationHandler_;
|
||||
this.migrationHandler_ = new MigrationHandler(this.api(), this.db(), this.lockHandler(), this.lockClientType(), this.clientId_);
|
||||
return this.migrationHandler_;
|
||||
}
|
||||
|
||||
public maxResourceSize() {
|
||||
maxResourceSize() {
|
||||
if (this.maxResourceSize_ !== null) return this.maxResourceSize_;
|
||||
return this.appType_ === AppType.Mobile ? 100 * 1000 * 1000 : Infinity;
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export default class Synchronizer {
|
||||
this.encryptionService_ = v;
|
||||
}
|
||||
|
||||
public encryptionService() {
|
||||
encryptionService() {
|
||||
return this.encryptionService_;
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export default class Synchronizer {
|
||||
return this.resourceService_;
|
||||
}
|
||||
|
||||
public async waitForSyncToFinish() {
|
||||
async waitForSyncToFinish() {
|
||||
if (this.state() === 'idle') return;
|
||||
|
||||
while (true) {
|
||||
@@ -177,7 +177,7 @@ export default class Synchronizer {
|
||||
return `${duration}ms`;
|
||||
}
|
||||
|
||||
public static reportToLines(report: any) {
|
||||
static reportToLines(report: any) {
|
||||
const lines = [];
|
||||
if (report.createLocal) lines.push(_('Created local items: %d.', report.createLocal));
|
||||
if (report.updateLocal) lines.push(_('Updated local items: %d.', report.updateLocal));
|
||||
@@ -193,7 +193,7 @@ export default class Synchronizer {
|
||||
return lines;
|
||||
}
|
||||
|
||||
public logSyncOperation(action: string, local: any = null, remote: RemoteItem = null, message: string = null, actionCount: number = 1) {
|
||||
logSyncOperation(action: string, local: any = null, remote: RemoteItem = null, message: string = null, actionCount: number = 1) {
|
||||
const line = ['Sync'];
|
||||
line.push(action);
|
||||
if (message) line.push(message);
|
||||
@@ -237,7 +237,7 @@ export default class Synchronizer {
|
||||
this.dispatch({ type: 'SYNC_REPORT_UPDATE', report: reportCopy });
|
||||
}
|
||||
|
||||
public async logSyncSummary(report: any) {
|
||||
async logSyncSummary(report: any) {
|
||||
logger.info('Operations completed: ');
|
||||
for (const n in report) {
|
||||
if (!report.hasOwnProperty(n)) continue;
|
||||
@@ -265,7 +265,7 @@ export default class Synchronizer {
|
||||
}
|
||||
}
|
||||
|
||||
public async cancel() {
|
||||
async cancel() {
|
||||
if (this.cancelling_ || this.state() === 'idle') return;
|
||||
|
||||
// Stop queue but don't set it to null as it may be used to
|
||||
@@ -285,11 +285,11 @@ export default class Synchronizer {
|
||||
});
|
||||
}
|
||||
|
||||
public cancelling() {
|
||||
cancelling() {
|
||||
return this.cancelling_;
|
||||
}
|
||||
|
||||
public logLastRequests() {
|
||||
logLastRequests() {
|
||||
const lastRequests = this.api().lastRequests();
|
||||
if (!lastRequests || !lastRequests.length) return;
|
||||
|
||||
@@ -300,17 +300,17 @@ export default class Synchronizer {
|
||||
}
|
||||
}
|
||||
|
||||
public static stateToLabel(state: string) {
|
||||
static stateToLabel(state: string) {
|
||||
if (state === 'idle') return _('Idle');
|
||||
if (state === 'in_progress') return _('In progress');
|
||||
return state;
|
||||
}
|
||||
|
||||
public isFullSync(steps: string[]) {
|
||||
isFullSync(steps: string[]) {
|
||||
return steps.includes('update_remote') && steps.includes('delete_remote') && steps.includes('delta');
|
||||
}
|
||||
|
||||
private async lockErrorStatus_() {
|
||||
async lockErrorStatus_() {
|
||||
const locks = await this.lockHandler().locks();
|
||||
const currentDate = await this.lockHandler().currentDate();
|
||||
|
||||
|
@@ -23,16 +23,16 @@ export default class TaskQueue {
|
||||
private name_: string;
|
||||
private logger_: Logger;
|
||||
|
||||
public constructor(name: string, logger: Logger = null) {
|
||||
constructor(name: string, logger: Logger = null) {
|
||||
this.name_ = name;
|
||||
this.logger_ = logger ? logger : new Logger();
|
||||
}
|
||||
|
||||
public concurrency() {
|
||||
concurrency() {
|
||||
return Setting.value('sync.maxConcurrentConnections');
|
||||
}
|
||||
|
||||
public push(id: string, callback: Function) {
|
||||
push(id: string, callback: Function) {
|
||||
if (this.stopping_) throw new Error('Cannot push task when queue is stopping');
|
||||
|
||||
this.waitingTasks_.push({
|
||||
@@ -42,7 +42,7 @@ export default class TaskQueue {
|
||||
this.processQueue_();
|
||||
}
|
||||
|
||||
private processQueue_() {
|
||||
processQueue_() {
|
||||
if (this.processingQueue_ || this.stopping_) return;
|
||||
|
||||
this.processingQueue_ = true;
|
||||
@@ -84,19 +84,19 @@ export default class TaskQueue {
|
||||
this.processingQueue_ = false;
|
||||
}
|
||||
|
||||
public isWaiting(taskId: string) {
|
||||
isWaiting(taskId: string) {
|
||||
return this.waitingTasks_.find(task => task.id === taskId);
|
||||
}
|
||||
|
||||
public isProcessing(taskId: string) {
|
||||
isProcessing(taskId: string) {
|
||||
return taskId in this.processingTasks_;
|
||||
}
|
||||
|
||||
public isDone(taskId: string) {
|
||||
isDone(taskId: string) {
|
||||
return taskId in this.results_;
|
||||
}
|
||||
|
||||
public async waitForAll() {
|
||||
async waitForAll() {
|
||||
return new Promise((resolve) => {
|
||||
const checkIID = setInterval(() => {
|
||||
if (this.waitingTasks_.length) return;
|
||||
@@ -107,16 +107,16 @@ export default class TaskQueue {
|
||||
});
|
||||
}
|
||||
|
||||
public taskExists(taskId: string) {
|
||||
taskExists(taskId: string) {
|
||||
return this.isWaiting(taskId) || this.isProcessing(taskId) || this.isDone(taskId);
|
||||
}
|
||||
|
||||
public taskResult(taskId: string) {
|
||||
taskResult(taskId: string) {
|
||||
if (!this.taskExists(taskId)) throw new Error(`No such task: ${taskId}`);
|
||||
return this.results_[taskId];
|
||||
}
|
||||
|
||||
public async waitForResult(taskId: string) {
|
||||
async waitForResult(taskId: string) {
|
||||
if (!this.taskExists(taskId)) throw new Error(`No such task: ${taskId}`);
|
||||
|
||||
while (true) {
|
||||
@@ -126,7 +126,7 @@ export default class TaskQueue {
|
||||
}
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
async stop() {
|
||||
this.stopping_ = true;
|
||||
|
||||
this.logger_.info(`TaskQueue.stop: ${this.name_}: waiting for tasks to complete: ${Object.keys(this.processingTasks_).length}`);
|
||||
@@ -146,7 +146,7 @@ export default class TaskQueue {
|
||||
this.logger_.info(`TaskQueue.stop: ${this.name_}: Done, waited for ${Date.now() - startTime}`);
|
||||
}
|
||||
|
||||
public isStopping() {
|
||||
isStopping() {
|
||||
return this.stopping_;
|
||||
}
|
||||
}
|
||||
|
@@ -46,7 +46,7 @@ export default class DatabaseDriverBetterSqlite {
|
||||
return this.db_.prepare(sql).run(params ? params : []);
|
||||
}
|
||||
|
||||
public lastInsertId() {
|
||||
lastInsertId() {
|
||||
throw new Error('NOT IMPLEMENTED');
|
||||
}
|
||||
}
|
||||
|
@@ -35,30 +35,30 @@ export default class Database {
|
||||
this.driver_ = driver;
|
||||
}
|
||||
|
||||
public setLogExcludedQueryTypes(v: string[]) {
|
||||
setLogExcludedQueryTypes(v: string[]) {
|
||||
this.logExcludedQueryTypes_ = v;
|
||||
}
|
||||
|
||||
// Converts the SQLite error to a regular JS error
|
||||
// so that it prints a stacktrace when passed to
|
||||
// console.error()
|
||||
public sqliteErrorToJsError(error: any, sql: string = null, params: SqlParams = null) {
|
||||
sqliteErrorToJsError(error: any, sql: string = null, params: SqlParams = null) {
|
||||
return this.driver().sqliteErrorToJsError(error, sql, params);
|
||||
}
|
||||
|
||||
public setLogger(l: Logger) {
|
||||
setLogger(l: Logger) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
public logger() {
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
public driver() {
|
||||
driver() {
|
||||
return this.driver_;
|
||||
}
|
||||
|
||||
public async open(options: any) {
|
||||
async open(options: any) {
|
||||
try {
|
||||
await this.driver().open(options);
|
||||
} catch (error) {
|
||||
@@ -97,7 +97,7 @@ export default class Database {
|
||||
return output.join(',');
|
||||
}
|
||||
|
||||
public async tryCall(callName: string, inputSql: StringOrSqlQuery, inputParams: SqlParams) {
|
||||
async tryCall(callName: string, inputSql: StringOrSqlQuery, inputParams: SqlParams) {
|
||||
let sql: string = null;
|
||||
let params: SqlParams = null;
|
||||
|
||||
@@ -157,11 +157,11 @@ export default class Database {
|
||||
}
|
||||
}
|
||||
|
||||
public async selectOne(sql: string, params: SqlParams = null): Promise<Row> {
|
||||
async selectOne(sql: string, params: SqlParams = null): Promise<Row> {
|
||||
return this.tryCall('selectOne', sql, params);
|
||||
}
|
||||
|
||||
public async loadExtension(/* path */) {
|
||||
async loadExtension(/* path */) {
|
||||
return; // Disabled for now as fuzzy search extension is not in use
|
||||
|
||||
// let result = null;
|
||||
@@ -173,11 +173,11 @@ export default class Database {
|
||||
// }
|
||||
}
|
||||
|
||||
public async selectAll(sql: string, params: SqlParams = null): Promise<Row[]> {
|
||||
async selectAll(sql: string, params: SqlParams = null): Promise<Row[]> {
|
||||
return this.tryCall('selectAll', sql, params);
|
||||
}
|
||||
|
||||
public async selectAllFields(sql: string, params: SqlParams, field: string): Promise<any[]> {
|
||||
async selectAllFields(sql: string, params: SqlParams, field: string): Promise<any[]> {
|
||||
const rows = await this.tryCall('selectAll', sql, params);
|
||||
const output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
@@ -188,11 +188,11 @@ export default class Database {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async exec(sql: StringOrSqlQuery, params: SqlParams = null) {
|
||||
async exec(sql: StringOrSqlQuery, params: SqlParams = null) {
|
||||
return this.tryCall('exec', sql, params);
|
||||
}
|
||||
|
||||
public async transactionExecBatch(queries: StringOrSqlQuery[]) {
|
||||
async transactionExecBatch(queries: StringOrSqlQuery[]) {
|
||||
if (queries.length <= 0) return;
|
||||
|
||||
if (queries.length === 1) {
|
||||
@@ -221,7 +221,7 @@ export default class Database {
|
||||
}
|
||||
}
|
||||
|
||||
public static enumId(type: string, s: string) {
|
||||
static enumId(type: string, s: string) {
|
||||
if (type === 'settings') {
|
||||
if (s === 'int') return 1;
|
||||
if (s === 'string') return 2;
|
||||
@@ -240,7 +240,7 @@ export default class Database {
|
||||
throw new Error(`Unknown enum type or value: ${type}, ${s}`);
|
||||
}
|
||||
|
||||
public static enumName(type: string, id: number) {
|
||||
static enumName(type: string, id: number) {
|
||||
if (type === 'fieldType') {
|
||||
if (id === Database.TYPE_UNKNOWN) return 'unknown';
|
||||
if (id === Database.TYPE_INT) return 'int';
|
||||
@@ -253,7 +253,7 @@ export default class Database {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public static formatValue(type: number, value: any) {
|
||||
static formatValue(type: number, value: any) {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (type === this.TYPE_INT) return Number(value);
|
||||
if (type === this.TYPE_TEXT) return value;
|
||||
@@ -261,7 +261,7 @@ export default class Database {
|
||||
throw new Error(`Unknown type: ${type}`);
|
||||
}
|
||||
|
||||
public sqlStringToLines(sql: string) {
|
||||
sqlStringToLines(sql: string) {
|
||||
const output = [];
|
||||
const lines = sql.split('\n');
|
||||
let statement = '';
|
||||
@@ -279,7 +279,7 @@ export default class Database {
|
||||
return output;
|
||||
}
|
||||
|
||||
public logQuery(sql: string, params: SqlParams = null) {
|
||||
logQuery(sql: string, params: SqlParams = null) {
|
||||
if (!this.sqlQueryLogEnabled_) return;
|
||||
|
||||
if (this.logExcludedQueryTypes_.length) {
|
||||
@@ -293,7 +293,7 @@ export default class Database {
|
||||
if (params !== null && params.length) this.logger().debug(JSON.stringify(params));
|
||||
}
|
||||
|
||||
public static insertQuery(tableName: string, data: Record<string, any>) {
|
||||
static insertQuery(tableName: string, data: Record<string, any>) {
|
||||
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
|
||||
|
||||
let keySql = '';
|
||||
@@ -314,7 +314,7 @@ export default class Database {
|
||||
};
|
||||
}
|
||||
|
||||
public static updateQuery(tableName: string, data: Record<string, any>, where: string | Record<string, any>) {
|
||||
static updateQuery(tableName: string, data: Record<string, any>, where: string | Record<string, any>) {
|
||||
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
|
||||
|
||||
let sql = '';
|
||||
@@ -343,7 +343,7 @@ export default class Database {
|
||||
};
|
||||
}
|
||||
|
||||
public alterColumnQueries(tableName: string, fields: Record<string, string>) {
|
||||
alterColumnQueries(tableName: string, fields: Record<string, string>) {
|
||||
const fieldsNoType = [];
|
||||
for (const n in fields) {
|
||||
if (!fields.hasOwnProperty(n)) continue;
|
||||
@@ -373,7 +373,7 @@ export default class Database {
|
||||
return sql.trim().split('\n');
|
||||
}
|
||||
|
||||
public wrapQueries(queries: any[]) {
|
||||
wrapQueries(queries: any[]) {
|
||||
const output = [];
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
output.push(this.wrapQuery(queries[i]));
|
||||
@@ -381,7 +381,7 @@ export default class Database {
|
||||
return output;
|
||||
}
|
||||
|
||||
public wrapQuery(sql: any, params: SqlParams = null): SqlQuery {
|
||||
wrapQuery(sql: any, params: SqlParams = null): SqlQuery {
|
||||
if (!sql) throw new Error(`Cannot wrap empty string: ${sql}`);
|
||||
|
||||
if (Array.isArray(sql)) {
|
||||
|
@@ -9,11 +9,11 @@ export class EventManager {
|
||||
private appStateWatchedProps_: string[];
|
||||
private appStateListeners_: any;
|
||||
|
||||
public constructor() {
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
reset() {
|
||||
this.emitter_ = new events.EventEmitter();
|
||||
|
||||
this.appStatePrevious_ = {};
|
||||
@@ -21,27 +21,27 @@ export class EventManager {
|
||||
this.appStateListeners_ = {};
|
||||
}
|
||||
|
||||
public on(eventName: string, callback: Function) {
|
||||
on(eventName: string, callback: Function) {
|
||||
return this.emitter_.on(eventName, callback);
|
||||
}
|
||||
|
||||
public emit(eventName: string, object: any = null) {
|
||||
emit(eventName: string, object: any = null) {
|
||||
return this.emitter_.emit(eventName, object);
|
||||
}
|
||||
|
||||
public removeListener(eventName: string, callback: Function) {
|
||||
removeListener(eventName: string, callback: Function) {
|
||||
return this.emitter_.removeListener(eventName, callback);
|
||||
}
|
||||
|
||||
public off(eventName: string, callback: Function) {
|
||||
off(eventName: string, callback: Function) {
|
||||
return this.removeListener(eventName, callback);
|
||||
}
|
||||
|
||||
public filterOn(filterName: string, callback: Function) {
|
||||
filterOn(filterName: string, callback: Function) {
|
||||
return this.emitter_.on(`filter:${filterName}`, callback);
|
||||
}
|
||||
|
||||
public filterOff(filterName: string, callback: Function) {
|
||||
filterOff(filterName: string, callback: Function) {
|
||||
return this.removeListener(`filter:${filterName}`, callback);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export class EventManager {
|
||||
return output;
|
||||
}
|
||||
|
||||
public appStateOn(propName: string, callback: Function) {
|
||||
appStateOn(propName: string, callback: Function) {
|
||||
if (!this.appStateListeners_[propName]) {
|
||||
this.appStateListeners_[propName] = [];
|
||||
this.appStateWatchedProps_.push(propName);
|
||||
@@ -76,7 +76,7 @@ export class EventManager {
|
||||
this.appStateListeners_[propName].push(callback);
|
||||
}
|
||||
|
||||
public appStateOff(propName: string, callback: Function) {
|
||||
appStateOff(propName: string, callback: Function) {
|
||||
if (!this.appStateListeners_[propName]) {
|
||||
throw new Error('EventManager: Trying to unregister a state prop watch for a non-watched prop (1)');
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export class EventManager {
|
||||
this.appStateListeners_[propName].splice(idx, 1);
|
||||
}
|
||||
|
||||
private stateValue_(state: any, propName: string) {
|
||||
stateValue_(state: any, propName: string) {
|
||||
const parts = propName.split('.');
|
||||
let s = state;
|
||||
for (const p of parts) {
|
||||
@@ -100,7 +100,7 @@ export class EventManager {
|
||||
// This function works by keeping a copy of the watched props and, whenever this function
|
||||
// is called, comparing the previous and new values and emitting events if they have changed.
|
||||
// The appStateEmit function should be called from a middleware.
|
||||
public appStateEmit(state: any) {
|
||||
appStateEmit(state: any) {
|
||||
if (!this.appStateWatchedProps_.length) return;
|
||||
|
||||
for (const propName of this.appStateWatchedProps_) {
|
||||
|
@@ -7,12 +7,12 @@ export default class FileApiDriverMemory {
|
||||
private items_: any[];
|
||||
private deletedItems_: any[];
|
||||
|
||||
public constructor() {
|
||||
constructor() {
|
||||
this.items_ = [];
|
||||
this.deletedItems_ = [];
|
||||
}
|
||||
|
||||
private encodeContent_(content: any) {
|
||||
encodeContent_(content: any) {
|
||||
if (content instanceof Buffer) {
|
||||
return content.toString('base64');
|
||||
} else {
|
||||
@@ -28,23 +28,23 @@ export default class FileApiDriverMemory {
|
||||
return true;
|
||||
}
|
||||
|
||||
private decodeContent_(content: any) {
|
||||
decodeContent_(content: any) {
|
||||
return Buffer.from(content, 'base64').toString('utf-8');
|
||||
}
|
||||
|
||||
public itemIndexByPath(path: string) {
|
||||
itemIndexByPath(path: string) {
|
||||
for (let i = 0; i < this.items_.length; i++) {
|
||||
if (this.items_[i].path === path) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public itemByPath(path: string) {
|
||||
itemByPath(path: string) {
|
||||
const index = this.itemIndexByPath(path);
|
||||
return index < 0 ? null : this.items_[index];
|
||||
}
|
||||
|
||||
public newItem(path: string, isDir = false) {
|
||||
newItem(path: string, isDir = false) {
|
||||
const now = time.unixMs();
|
||||
return {
|
||||
path: path,
|
||||
@@ -55,18 +55,18 @@ export default class FileApiDriverMemory {
|
||||
};
|
||||
}
|
||||
|
||||
public stat(path: string) {
|
||||
stat(path: string) {
|
||||
const item = this.itemByPath(path);
|
||||
return Promise.resolve(item ? Object.assign({}, item) : null);
|
||||
}
|
||||
|
||||
public async setTimestamp(path: string, timestampMs: number): Promise<any> {
|
||||
async setTimestamp(path: string, timestampMs: number): Promise<any> {
|
||||
const item = this.itemByPath(path);
|
||||
if (!item) return Promise.reject(new Error(`File not found: ${path}`));
|
||||
item.updated_time = timestampMs;
|
||||
}
|
||||
|
||||
public async list(path: string) {
|
||||
async list(path: string) {
|
||||
const output = [];
|
||||
|
||||
for (let i = 0; i < this.items_.length; i++) {
|
||||
@@ -89,7 +89,7 @@ export default class FileApiDriverMemory {
|
||||
});
|
||||
}
|
||||
|
||||
public async get(path: string, options: any) {
|
||||
async get(path: string, options: any) {
|
||||
const item = this.itemByPath(path);
|
||||
if (!item) return Promise.resolve(null);
|
||||
if (item.isDir) return Promise.reject(new Error(`${path} is a directory, not a file`));
|
||||
@@ -105,13 +105,13 @@ export default class FileApiDriverMemory {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async mkdir(path: string) {
|
||||
async mkdir(path: string) {
|
||||
const index = this.itemIndexByPath(path);
|
||||
if (index >= 0) return;
|
||||
this.items_.push(this.newItem(path, true));
|
||||
}
|
||||
|
||||
public async put(path: string, content: any, options: any = null) {
|
||||
async put(path: string, content: any, options: any = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.source === 'file') content = await fs.readFile(options.path);
|
||||
@@ -152,7 +152,7 @@ export default class FileApiDriverMemory {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async delete(path: string) {
|
||||
async delete(path: string) {
|
||||
const index = this.itemIndexByPath(path);
|
||||
if (index >= 0) {
|
||||
const item = Object.assign({}, this.items_[index]);
|
||||
@@ -163,18 +163,18 @@ export default class FileApiDriverMemory {
|
||||
}
|
||||
}
|
||||
|
||||
public async move(oldPath: string, newPath: string): Promise<any> {
|
||||
async move(oldPath: string, newPath: string): Promise<any> {
|
||||
const sourceItem = this.itemByPath(oldPath);
|
||||
if (!sourceItem) return Promise.reject(new Error(`Path not found: ${oldPath}`));
|
||||
await this.delete(newPath); // Overwrite if newPath already exists
|
||||
sourceItem.path = newPath;
|
||||
}
|
||||
|
||||
public async format() {
|
||||
async format() {
|
||||
this.items_ = [];
|
||||
}
|
||||
|
||||
public async delta(path: string, options: any = null) {
|
||||
async delta(path: string, options: any = null) {
|
||||
const getStatFn = async (path: string) => {
|
||||
const output = this.items_.slice();
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
@@ -189,7 +189,7 @@ export default class FileApiDriverMemory {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async clearRoot() {
|
||||
async clearRoot() {
|
||||
this.items_ = [];
|
||||
}
|
||||
}
|
||||
|
@@ -99,13 +99,13 @@ class FileApi {
|
||||
private remoteDateMutex_ = new Mutex();
|
||||
private initialized_ = false;
|
||||
|
||||
public constructor(baseDir: string | Function, driver: any) {
|
||||
constructor(baseDir: string | Function, driver: any) {
|
||||
this.baseDir_ = baseDir;
|
||||
this.driver_ = driver;
|
||||
this.driver_.fileApi_ = this;
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
async initialize() {
|
||||
if (this.initialized_) return;
|
||||
this.initialized_ = true;
|
||||
if (this.driver_.initialize) return this.driver_.initialize(this.fullPath(''));
|
||||
@@ -135,7 +135,7 @@ class FileApi {
|
||||
return !!this.driver().supportsLocks;
|
||||
}
|
||||
|
||||
private async fetchRemoteDateOffset_() {
|
||||
async fetchRemoteDateOffset_() {
|
||||
const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`;
|
||||
const startTime = Date.now();
|
||||
await this.put(tempFile, 'timeCheck');
|
||||
@@ -161,7 +161,7 @@ class FileApi {
|
||||
|
||||
// Approximates the current time on the sync target. It caches the time offset to
|
||||
// improve performance.
|
||||
public async remoteDate() {
|
||||
async remoteDate() {
|
||||
const shouldSyncTime = () => {
|
||||
return !this.remoteDateNextCheckTime_ || Date.now() > this.remoteDateNextCheckTime_;
|
||||
};
|
||||
@@ -193,60 +193,60 @@ class FileApi {
|
||||
// Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but
|
||||
// historically some drivers (eg. OneDrive) are already handling request repeating, so this is optional, per driver,
|
||||
// and it defaults to no repeating.
|
||||
public requestRepeatCount() {
|
||||
requestRepeatCount() {
|
||||
if (this.requestRepeatCount_ !== null) return this.requestRepeatCount_;
|
||||
if (this.driver_.requestRepeatCount) return this.driver_.requestRepeatCount();
|
||||
return 0;
|
||||
}
|
||||
|
||||
public lastRequests() {
|
||||
lastRequests() {
|
||||
return this.driver_.lastRequests ? this.driver_.lastRequests() : [];
|
||||
}
|
||||
|
||||
public clearLastRequests() {
|
||||
clearLastRequests() {
|
||||
if (this.driver_.clearLastRequests) this.driver_.clearLastRequests();
|
||||
}
|
||||
|
||||
public baseDir() {
|
||||
baseDir() {
|
||||
return typeof this.baseDir_ === 'function' ? this.baseDir_() : this.baseDir_;
|
||||
}
|
||||
|
||||
public tempDirName() {
|
||||
tempDirName() {
|
||||
if (this.tempDirName_ === null) throw Error('Temp dir not set!');
|
||||
return this.tempDirName_;
|
||||
}
|
||||
|
||||
public setTempDirName(v: string) {
|
||||
setTempDirName(v: string) {
|
||||
this.tempDirName_ = v;
|
||||
}
|
||||
|
||||
public fsDriver() {
|
||||
fsDriver() {
|
||||
return shim.fsDriver();
|
||||
}
|
||||
|
||||
public driver() {
|
||||
driver() {
|
||||
return this.driver_;
|
||||
}
|
||||
|
||||
public setSyncTargetId(v: number) {
|
||||
setSyncTargetId(v: number) {
|
||||
this.syncTargetId_ = v;
|
||||
}
|
||||
|
||||
public syncTargetId() {
|
||||
syncTargetId() {
|
||||
if (this.syncTargetId_ === null) throw new Error('syncTargetId has not been set!!');
|
||||
return this.syncTargetId_;
|
||||
}
|
||||
|
||||
public setLogger(l: Logger) {
|
||||
setLogger(l: Logger) {
|
||||
if (!l) l = new Logger();
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
public logger() {
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
public fullPath(path: string) {
|
||||
fullPath(path: string) {
|
||||
const output = [];
|
||||
if (this.baseDir()) output.push(this.baseDir());
|
||||
if (path) output.push(path);
|
||||
@@ -286,18 +286,18 @@ class FileApi {
|
||||
}
|
||||
|
||||
// Deprectated
|
||||
public setTimestamp(path: string, timestampMs: number) {
|
||||
setTimestamp(path: string, timestampMs: number) {
|
||||
logger.debug(`setTimestamp ${this.fullPath(path)}`);
|
||||
return tryAndRepeat(() => this.driver_.setTimestamp(this.fullPath(path), timestampMs), this.requestRepeatCount());
|
||||
// return this.driver_.setTimestamp(this.fullPath(path), timestampMs);
|
||||
}
|
||||
|
||||
public mkdir(path: string) {
|
||||
mkdir(path: string) {
|
||||
logger.debug(`mkdir ${this.fullPath(path)}`);
|
||||
return tryAndRepeat(() => this.driver_.mkdir(this.fullPath(path)), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
public async stat(path: string) {
|
||||
async stat(path: string) {
|
||||
logger.debug(`stat ${this.fullPath(path)}`);
|
||||
|
||||
const output = await tryAndRepeat(() => this.driver_.stat(this.fullPath(path)), this.requestRepeatCount());
|
||||
@@ -308,14 +308,14 @@ class FileApi {
|
||||
}
|
||||
|
||||
// Returns UTF-8 encoded string by default, or a Response if `options.target = 'file'`
|
||||
public get(path: string, options: any = null) {
|
||||
get(path: string, options: any = null) {
|
||||
if (!options) options = {};
|
||||
if (!options.encoding) options.encoding = 'utf8';
|
||||
logger.debug(`get ${this.fullPath(path)}`);
|
||||
return tryAndRepeat(() => this.driver_.get(this.fullPath(path), options), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
public async put(path: string, content: any, options: any = null) {
|
||||
async put(path: string, content: any, options: any = null) {
|
||||
logger.debug(`put ${this.fullPath(path)}`, options);
|
||||
|
||||
if (options && options.source === 'file') {
|
||||
@@ -330,27 +330,27 @@ class FileApi {
|
||||
return tryAndRepeat(() => this.driver_.multiPut(items, options), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
public delete(path: string) {
|
||||
delete(path: string) {
|
||||
logger.debug(`delete ${this.fullPath(path)}`);
|
||||
return tryAndRepeat(() => this.driver_.delete(this.fullPath(path)), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
// Deprectated
|
||||
public move(oldPath: string, newPath: string) {
|
||||
move(oldPath: string, newPath: string) {
|
||||
logger.debug(`move ${this.fullPath(oldPath)} => ${this.fullPath(newPath)}`);
|
||||
return tryAndRepeat(() => this.driver_.move(this.fullPath(oldPath), this.fullPath(newPath)), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
// Deprectated
|
||||
public format() {
|
||||
format() {
|
||||
return tryAndRepeat(() => this.driver_.format(), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
public clearRoot() {
|
||||
clearRoot() {
|
||||
return tryAndRepeat(() => this.driver_.clearRoot(this.baseDir()), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
public delta(path: string, options: any = null) {
|
||||
delta(path: string, options: any = null) {
|
||||
logger.debug(`delta ${this.fullPath(path)}`);
|
||||
return tryAndRepeat(() => this.driver_.delta(this.fullPath(path), options), this.requestRepeatCount());
|
||||
}
|
||||
|
@@ -58,7 +58,7 @@ const geoipServices: Record<string, GeoipService> = {
|
||||
};
|
||||
|
||||
export default class {
|
||||
public static async currentPosition(options: CurrentPositionOptions = null) {
|
||||
static async currentPosition(options: CurrentPositionOptions = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
for (const [serviceName, handler] of Object.entries(geoipServices)) {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
|
||||
const { setupDatabaseAndSynchronizer, switchClient, supportDir } = require('./testing/test-utils.js');
|
||||
const shim = require('./shim').default;
|
||||
const { enexXmlToHtml } = require('./import-enex-html-gen.js');
|
||||
@@ -49,7 +50,6 @@ const compareOutputToExpected = (options) => {
|
||||
const outputFile = fileWithPath(`${options.testName}.html`);
|
||||
const testTitle = `should convert from Enex to Html: ${options.testName}`;
|
||||
|
||||
// eslint-disable-next-line jest/require-top-level-describe
|
||||
it(testTitle, (async () => {
|
||||
const enexInput = await shim.fsDriver().readFile(inputFile);
|
||||
const expectedOutput = await shim.fsDriver().readFile(outputFile);
|
||||
|
@@ -10,23 +10,23 @@ export interface Notification {
|
||||
}
|
||||
|
||||
export default class Alarm extends BaseModel {
|
||||
public static tableName() {
|
||||
static tableName() {
|
||||
return 'alarms';
|
||||
}
|
||||
|
||||
public static modelType() {
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_ALARM;
|
||||
}
|
||||
|
||||
public static byNoteId(noteId: string) {
|
||||
static byNoteId(noteId: string) {
|
||||
return this.modelSelectOne('SELECT * FROM alarms WHERE note_id = ?', [noteId]);
|
||||
}
|
||||
|
||||
public static async deleteExpiredAlarms() {
|
||||
static async deleteExpiredAlarms() {
|
||||
return this.db().exec('DELETE FROM alarms WHERE trigger_time <= ?', [Date.now()]);
|
||||
}
|
||||
|
||||
public static async alarmIdsWithoutNotes() {
|
||||
static async alarmIdsWithoutNotes() {
|
||||
// https://stackoverflow.com/a/4967229/561309
|
||||
const alarms = await this.db().selectAll('SELECT alarms.id FROM alarms LEFT JOIN notes ON alarms.note_id = notes.id WHERE notes.id IS NULL');
|
||||
return alarms.map((a: any) => {
|
||||
@@ -34,7 +34,7 @@ export default class Alarm extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
public static async makeNotification(alarm: any, note: any = null): Promise<Notification> {
|
||||
static async makeNotification(alarm: any, note: any = null): Promise<Notification> {
|
||||
if (!note) {
|
||||
note = await Note.load(alarm.note_id);
|
||||
} else if (!note.todo_due) {
|
||||
@@ -55,7 +55,7 @@ export default class Alarm extends BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static async allDue() {
|
||||
static async allDue() {
|
||||
return this.modelSelectAll('SELECT * FROM alarms WHERE trigger_time >= ?', [Date.now()]);
|
||||
}
|
||||
}
|
||||
|
@@ -63,15 +63,15 @@ export default class BaseItem extends BaseModel {
|
||||
public static SYNC_ITEM_LOCATION_REMOTE = 2;
|
||||
|
||||
|
||||
public static useUuid() {
|
||||
static useUuid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static encryptionSupported() {
|
||||
static encryptionSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static loadClass(className: string, classRef: any) {
|
||||
static loadClass(className: string, classRef: any) {
|
||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
||||
if (BaseItem.syncItemDefinitions_[i].className === className) {
|
||||
BaseItem.syncItemDefinitions_[i].classRef = classRef;
|
||||
@@ -82,7 +82,7 @@ export default class BaseItem extends BaseModel {
|
||||
throw new Error(`Invalid class name: ${className}`);
|
||||
}
|
||||
|
||||
public static async findUniqueItemTitle(title: string, parentId: string = null) {
|
||||
static async findUniqueItemTitle(title: string, parentId: string = null) {
|
||||
let counter = 1;
|
||||
let titleToTry = title;
|
||||
while (true) {
|
||||
@@ -106,7 +106,7 @@ export default class BaseItem extends BaseModel {
|
||||
}
|
||||
|
||||
// Need to dynamically load the classes like this to avoid circular dependencies
|
||||
public static getClass(name: string) {
|
||||
static getClass(name: string) {
|
||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
||||
if (BaseItem.syncItemDefinitions_[i].className === name) {
|
||||
const classRef = BaseItem.syncItemDefinitions_[i].classRef;
|
||||
@@ -118,7 +118,7 @@ export default class BaseItem extends BaseModel {
|
||||
throw new Error(`Invalid class name: ${name}`);
|
||||
}
|
||||
|
||||
public static getClassByItemType(itemType: ModelType) {
|
||||
static getClassByItemType(itemType: ModelType) {
|
||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
||||
if (BaseItem.syncItemDefinitions_[i].type === itemType) {
|
||||
return BaseItem.syncItemDefinitions_[i].classRef;
|
||||
@@ -128,7 +128,7 @@ export default class BaseItem extends BaseModel {
|
||||
throw new Error(`Invalid item type: ${itemType}`);
|
||||
}
|
||||
|
||||
public static async syncedCount(syncTarget: number) {
|
||||
static async syncedCount(syncTarget: number) {
|
||||
const ItemClass = this.itemClass(this.modelType());
|
||||
const itemType = ItemClass.modelType();
|
||||
// The fact that we don't check if the item_id still exist in the corresponding item table, means
|
||||
@@ -145,7 +145,7 @@ export default class BaseItem extends BaseModel {
|
||||
else return `${itemOrId.id}.${extension}`;
|
||||
}
|
||||
|
||||
public static isSystemPath(path: string) {
|
||||
static isSystemPath(path: string) {
|
||||
// 1b175bb38bba47baac22b0b47f778113.md
|
||||
if (!path || !path.length) return false;
|
||||
let p: any = path.split('/');
|
||||
@@ -155,7 +155,7 @@ export default class BaseItem extends BaseModel {
|
||||
return p[0].length === 32 && p[1] === 'md';
|
||||
}
|
||||
|
||||
public static itemClass(item: any): any {
|
||||
static itemClass(item: any): any {
|
||||
if (!item) throw new Error('Item cannot be null');
|
||||
|
||||
if (typeof item === 'object') {
|
||||
@@ -171,7 +171,7 @@ export default class BaseItem extends BaseModel {
|
||||
}
|
||||
|
||||
// Returns the IDs of the items that have been synced at least once
|
||||
public static async syncedItemIds(syncTarget: number) {
|
||||
static async syncedItemIds(syncTarget: number) {
|
||||
if (!syncTarget) throw new Error('No syncTarget specified');
|
||||
const temp = await this.db().selectAll('SELECT item_id FROM sync_items WHERE sync_time > 0 AND sync_target = ?', [syncTarget]);
|
||||
const output = [];
|
||||
@@ -181,12 +181,12 @@ export default class BaseItem extends BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static async allSyncItems(syncTarget: number) {
|
||||
static async allSyncItems(syncTarget: number) {
|
||||
const output = await this.db().selectAll('SELECT * FROM sync_items WHERE sync_target = ?', [syncTarget]);
|
||||
return output;
|
||||
}
|
||||
|
||||
public static pathToId(path: string) {
|
||||
static pathToId(path: string) {
|
||||
const p = path.split('/');
|
||||
const s = p[p.length - 1].split('.');
|
||||
let name: any = s[0];
|
||||
@@ -195,11 +195,11 @@ export default class BaseItem extends BaseModel {
|
||||
return name[name.length - 1];
|
||||
}
|
||||
|
||||
public static loadItemByPath(path: string) {
|
||||
static loadItemByPath(path: string) {
|
||||
return this.loadItemById(this.pathToId(path));
|
||||
}
|
||||
|
||||
public static async loadItemById(id: string) {
|
||||
static async loadItemById(id: string) {
|
||||
const classes = this.syncItemClassNames();
|
||||
for (let i = 0; i < classes.length; i++) {
|
||||
const item = await this.getClass(classes[i]).load(id);
|
||||
@@ -208,7 +208,7 @@ export default class BaseItem extends BaseModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async loadItemsByIds(ids: string[]) {
|
||||
static async loadItemsByIds(ids: string[]) {
|
||||
if (!ids.length) return [];
|
||||
|
||||
const classes = this.syncItemClassNames();
|
||||
@@ -222,26 +222,26 @@ export default class BaseItem extends BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static loadItemByField(itemType: number, field: string, value: any) {
|
||||
static loadItemByField(itemType: number, field: string, value: any) {
|
||||
const ItemClass = this.itemClass(itemType);
|
||||
return ItemClass.loadByField(field, value);
|
||||
}
|
||||
|
||||
public static loadItem(itemType: ModelType, id: string) {
|
||||
static loadItem(itemType: ModelType, id: string) {
|
||||
const ItemClass = this.itemClass(itemType);
|
||||
return ItemClass.load(id);
|
||||
}
|
||||
|
||||
public static deleteItem(itemType: ModelType, id: string) {
|
||||
static deleteItem(itemType: ModelType, id: string) {
|
||||
const ItemClass = this.itemClass(itemType);
|
||||
return ItemClass.delete(id);
|
||||
}
|
||||
|
||||
public static async delete(id: string, options: DeleteOptions = null) {
|
||||
static async delete(id: string, options: DeleteOptions = null) {
|
||||
return this.batchDelete([id], options);
|
||||
}
|
||||
|
||||
public static async batchDelete(ids: string[], options: DeleteOptions = null) {
|
||||
static async batchDelete(ids: string[], options: DeleteOptions = null) {
|
||||
if (!options) options = {};
|
||||
let trackDeleted = true;
|
||||
if (options && options.trackDeleted !== null && options.trackDeleted !== undefined) trackDeleted = options.trackDeleted;
|
||||
@@ -287,20 +287,20 @@ export default class BaseItem extends BaseModel {
|
||||
// - Client 1 syncs with target 2 only => the note is *not* deleted from target 2 because no information
|
||||
// that it was previously deleted exist (deleted_items entry has been deleted).
|
||||
// The solution would be to permanently store the list of deleted items on each client.
|
||||
public static deletedItems(syncTarget: number) {
|
||||
static deletedItems(syncTarget: number) {
|
||||
return this.db().selectAll('SELECT * FROM deleted_items WHERE sync_target = ?', [syncTarget]);
|
||||
}
|
||||
|
||||
public static async deletedItemCount(syncTarget: number) {
|
||||
static async deletedItemCount(syncTarget: number) {
|
||||
const r = await this.db().selectOne('SELECT count(*) as total FROM deleted_items WHERE sync_target = ?', [syncTarget]);
|
||||
return r['total'];
|
||||
}
|
||||
|
||||
public static remoteDeletedItem(syncTarget: number, itemId: string) {
|
||||
static remoteDeletedItem(syncTarget: number, itemId: string) {
|
||||
return this.db().exec('DELETE FROM deleted_items WHERE item_id = ? AND sync_target = ?', [itemId, syncTarget]);
|
||||
}
|
||||
|
||||
public static serialize_format(propName: string, propValue: any) {
|
||||
static serialize_format(propName: string, propValue: any) {
|
||||
if (['created_time', 'updated_time', 'sync_time', 'user_updated_time', 'user_created_time'].indexOf(propName) >= 0) {
|
||||
if (!propValue) return '';
|
||||
propValue = `${moment.unix(propValue / 1000).utc().format('YYYY-MM-DDTHH:mm:ss.SSS')}Z`;
|
||||
@@ -322,7 +322,7 @@ export default class BaseItem extends BaseModel {
|
||||
.replace(/\r/g, '\\r');
|
||||
}
|
||||
|
||||
public static unserialize_format(type: ModelType, propName: string, propValue: any) {
|
||||
static unserialize_format(type: ModelType, propName: string, propValue: any) {
|
||||
if (propName[propName.length - 1] === '_') return propValue; // Private property
|
||||
|
||||
const ItemClass = this.itemClass(type);
|
||||
@@ -350,7 +350,7 @@ export default class BaseItem extends BaseModel {
|
||||
: propValue;
|
||||
}
|
||||
|
||||
public static async serialize(item: any, shownKeys: any[] = null) {
|
||||
static async serialize(item: any, shownKeys: any[] = null) {
|
||||
if (shownKeys === null) {
|
||||
shownKeys = this.itemClass(item).fieldNames();
|
||||
shownKeys.push('type_');
|
||||
@@ -395,12 +395,12 @@ export default class BaseItem extends BaseModel {
|
||||
return temp.join('\n\n');
|
||||
}
|
||||
|
||||
public static encryptionService() {
|
||||
static encryptionService() {
|
||||
if (!this.encryptionService_) throw new Error('BaseItem.encryptionService_ is not set!!');
|
||||
return this.encryptionService_;
|
||||
}
|
||||
|
||||
public static revisionService() {
|
||||
static revisionService() {
|
||||
if (!this.revisionService_) throw new Error('BaseItem.revisionService_ is not set!!');
|
||||
return this.revisionService_;
|
||||
}
|
||||
@@ -460,7 +460,7 @@ export default class BaseItem extends BaseModel {
|
||||
return ItemClass.serialize(reducedItem);
|
||||
}
|
||||
|
||||
public static async decrypt(item: any) {
|
||||
static async decrypt(item: any) {
|
||||
if (!item.encryption_cipher_text) throw new Error(`Item is not encrypted: ${item.id}`);
|
||||
|
||||
const ItemClass = this.itemClass(item);
|
||||
@@ -474,7 +474,7 @@ export default class BaseItem extends BaseModel {
|
||||
return ItemClass.save(plainItem, { autoTimestamp: false, changeSource: ItemChange.SOURCE_DECRYPTION });
|
||||
}
|
||||
|
||||
public static async unserialize(content: string) {
|
||||
static async unserialize(content: string) {
|
||||
const lines = content.split('\n');
|
||||
let output: any = {};
|
||||
let state = 'readingProps';
|
||||
@@ -539,7 +539,7 @@ export default class BaseItem extends BaseModel {
|
||||
};
|
||||
}
|
||||
|
||||
public static async encryptedItemsCount() {
|
||||
static async encryptedItemsCount() {
|
||||
const classNames = this.encryptableItemClassNames();
|
||||
let output = 0;
|
||||
|
||||
@@ -553,7 +553,7 @@ export default class BaseItem extends BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static async hasEncryptedItems() {
|
||||
static async hasEncryptedItems() {
|
||||
const classNames = this.encryptableItemClassNames();
|
||||
|
||||
for (let i = 0; i < classNames.length; i++) {
|
||||
@@ -567,7 +567,7 @@ export default class BaseItem extends BaseModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async itemsThatNeedDecryption(exclusions: string[] = [], limit = 100): Promise<ItemsThatNeedDecryptionResult> {
|
||||
static async itemsThatNeedDecryption(exclusions: string[] = [], limit = 100): Promise<ItemsThatNeedDecryptionResult> {
|
||||
const classNames = this.encryptableItemClassNames();
|
||||
|
||||
for (let i = 0; i < classNames.length; i++) {
|
||||
@@ -703,13 +703,13 @@ export default class BaseItem extends BaseModel {
|
||||
throw new Error('Unreachable');
|
||||
}
|
||||
|
||||
public static syncItemClassNames(): string[] {
|
||||
static syncItemClassNames(): string[] {
|
||||
return BaseItem.syncItemDefinitions_.map((def: any) => {
|
||||
return def.className;
|
||||
});
|
||||
}
|
||||
|
||||
public static encryptableItemClassNames() {
|
||||
static encryptableItemClassNames() {
|
||||
const temp = this.syncItemClassNames();
|
||||
const output = [];
|
||||
for (let i = 0; i < temp.length; i++) {
|
||||
@@ -725,14 +725,14 @@ export default class BaseItem extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
public static modelTypeToClassName(type: number) {
|
||||
static modelTypeToClassName(type: number) {
|
||||
for (let i = 0; i < BaseItem.syncItemDefinitions_.length; i++) {
|
||||
if (BaseItem.syncItemDefinitions_[i].type === type) return BaseItem.syncItemDefinitions_[i].className;
|
||||
}
|
||||
throw new Error(`Invalid type: ${type}`);
|
||||
}
|
||||
|
||||
public static async syncDisabledItems(syncTargetId: number) {
|
||||
static async syncDisabledItems(syncTargetId: number) {
|
||||
const rows = await this.db().selectAll('SELECT * FROM sync_items WHERE sync_disabled = 1 AND sync_target = ?', [syncTargetId]);
|
||||
const output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
@@ -749,7 +749,7 @@ export default class BaseItem extends BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static updateSyncTimeQueries(syncTarget: number, item: any, syncTime: number, syncDisabled = false, syncDisabledReason = '', itemLocation: number = null) {
|
||||
static updateSyncTimeQueries(syncTarget: number, item: any, syncTime: number, syncDisabled = false, syncDisabledReason = '', itemLocation: number = null) {
|
||||
const itemType = item.type_;
|
||||
const itemId = item.id;
|
||||
if (!itemType || !itemId || syncTime === undefined) throw new Error(sprintf('Invalid parameters in updateSyncTimeQueries(): %d, %s, %d', syncTarget, JSON.stringify(item), syncTime));
|
||||
@@ -768,12 +768,12 @@ export default class BaseItem extends BaseModel {
|
||||
];
|
||||
}
|
||||
|
||||
public static async saveSyncTime(syncTarget: number, item: any, syncTime: number) {
|
||||
static async saveSyncTime(syncTarget: number, item: any, syncTime: number) {
|
||||
const queries = this.updateSyncTimeQueries(syncTarget, item, syncTime);
|
||||
return this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
public static async saveSyncDisabled(syncTargetId: number, item: any, syncDisabledReason: string, itemLocation: number = null) {
|
||||
static async saveSyncDisabled(syncTargetId: number, item: any, syncDisabledReason: string, itemLocation: number = null) {
|
||||
const syncTime = 'sync_time' in item ? item.sync_time : 0;
|
||||
const queries = this.updateSyncTimeQueries(syncTargetId, item, syncTime, true, syncDisabledReason, itemLocation);
|
||||
return this.db().transactionExecBatch(queries);
|
||||
@@ -786,7 +786,7 @@ export default class BaseItem extends BaseModel {
|
||||
// When an item is deleted, its associated sync_items data is not immediately deleted for
|
||||
// performance reason. So this function is used to look for these remaining sync_items and
|
||||
// delete them.
|
||||
public static async deleteOrphanSyncItems() {
|
||||
static async deleteOrphanSyncItems() {
|
||||
const classNames = this.syncItemClassNames();
|
||||
|
||||
const queries = [];
|
||||
@@ -803,13 +803,13 @@ export default class BaseItem extends BaseModel {
|
||||
await this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
public static displayTitle(item: any) {
|
||||
static displayTitle(item: any) {
|
||||
if (!item) return '';
|
||||
if (item.encryption_applied) return `🔑 ${_('Encrypted')}`;
|
||||
return item.title ? item.title : _('Untitled');
|
||||
}
|
||||
|
||||
public static async markAllNonEncryptedForSync() {
|
||||
static async markAllNonEncryptedForSync() {
|
||||
const classNames = this.encryptableItemClassNames();
|
||||
|
||||
for (let i = 0; i < classNames.length; i++) {
|
||||
@@ -834,7 +834,7 @@ export default class BaseItem extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
public static async updateShareStatus(item: BaseItemEntity, isShared: boolean) {
|
||||
static async updateShareStatus(item: BaseItemEntity, isShared: boolean) {
|
||||
if (!item.id || !item.type_) throw new Error('Item must have an ID and a type');
|
||||
if (!!item.is_shared === !!isShared) return false;
|
||||
const ItemClass = this.getClassByItemType(item.type_);
|
||||
@@ -853,15 +853,15 @@ export default class BaseItem extends BaseModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async forceSync(itemId: string) {
|
||||
static async forceSync(itemId: string) {
|
||||
await this.db().exec('UPDATE sync_items SET force_sync = 1 WHERE item_id = ?', [itemId]);
|
||||
}
|
||||
|
||||
public static async forceSyncAll() {
|
||||
static async forceSyncAll() {
|
||||
await this.db().exec('UPDATE sync_items SET force_sync = 1');
|
||||
}
|
||||
|
||||
public static async save(o: any, options: any = null) {
|
||||
static async save(o: any, options: any = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.userSideValidation === true) {
|
||||
@@ -871,7 +871,7 @@ export default class BaseItem extends BaseModel {
|
||||
return super.save(o, options);
|
||||
}
|
||||
|
||||
public static markdownTag(itemOrId: any) {
|
||||
static markdownTag(itemOrId: any) {
|
||||
const item = typeof itemOrId === 'object' ? itemOrId : {
|
||||
id: itemOrId,
|
||||
title: '',
|
||||
@@ -885,7 +885,7 @@ export default class BaseItem extends BaseModel {
|
||||
return output.join('');
|
||||
}
|
||||
|
||||
public static isMarkdownTag(md: any) {
|
||||
static isMarkdownTag(md: any) {
|
||||
if (!md) return false;
|
||||
return !!md.match(/^\[.*?\]\(:\/[0-9a-zA-Z]{32}\)$/);
|
||||
}
|
||||
|
@@ -19,22 +19,22 @@ export interface FolderEntityWithChildren extends FolderEntity {
|
||||
}
|
||||
|
||||
export default class Folder extends BaseItem {
|
||||
public static tableName() {
|
||||
static tableName() {
|
||||
return 'folders';
|
||||
}
|
||||
|
||||
public static modelType() {
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_FOLDER;
|
||||
}
|
||||
|
||||
public static newFolder(): FolderEntity {
|
||||
static newFolder(): FolderEntity {
|
||||
return {
|
||||
id: null,
|
||||
title: '',
|
||||
};
|
||||
}
|
||||
|
||||
public static fieldToLabel(field: string) {
|
||||
static fieldToLabel(field: string) {
|
||||
const fieldsToLabels: any = {
|
||||
title: _('title'),
|
||||
last_note_user_updated_time: _('updated date'),
|
||||
@@ -43,7 +43,7 @@ export default class Folder extends BaseItem {
|
||||
return field in fieldsToLabels ? fieldsToLabels[field] : field;
|
||||
}
|
||||
|
||||
public static noteIds(parentId: string, options: any = null) {
|
||||
static noteIds(parentId: string, options: any = null) {
|
||||
options = Object.assign({}, {
|
||||
includeConflicts: false,
|
||||
}, options);
|
||||
@@ -66,17 +66,17 @@ export default class Folder extends BaseItem {
|
||||
});
|
||||
}
|
||||
|
||||
public static async subFolderIds(parentId: string) {
|
||||
static async subFolderIds(parentId: string) {
|
||||
const rows = await this.db().selectAll('SELECT id FROM folders WHERE parent_id = ?', [parentId]);
|
||||
return rows.map((r: FolderEntity) => r.id);
|
||||
}
|
||||
|
||||
public static async noteCount(parentId: string) {
|
||||
static async noteCount(parentId: string) {
|
||||
const r = await this.db().selectOne('SELECT count(*) as total FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]);
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
public static markNotesAsConflict(parentId: string) {
|
||||
static markNotesAsConflict(parentId: string) {
|
||||
const query = Database.updateQuery('notes', { is_conflict: 1 }, { parent_id: parentId });
|
||||
return this.db().exec(query);
|
||||
}
|
||||
@@ -108,15 +108,15 @@ export default class Folder extends BaseItem {
|
||||
});
|
||||
}
|
||||
|
||||
public static conflictFolderTitle() {
|
||||
static conflictFolderTitle() {
|
||||
return _('Conflicts');
|
||||
}
|
||||
|
||||
public static conflictFolderId() {
|
||||
static conflictFolderId() {
|
||||
return 'c04f1c7c04f1c7c04f1c7c04f1c7c04f';
|
||||
}
|
||||
|
||||
public static conflictFolder(): FolderEntity {
|
||||
static conflictFolder(): FolderEntity {
|
||||
return {
|
||||
type_: this.TYPE_FOLDER,
|
||||
id: this.conflictFolderId(),
|
||||
@@ -129,7 +129,7 @@ export default class Folder extends BaseItem {
|
||||
|
||||
// Calculates note counts for all folders and adds the note_count attribute to each folder
|
||||
// Note: this only calculates the overall number of nodes for this folder and all its descendants
|
||||
public static async addNoteCounts(folders: any[], includeCompletedTodos = true) {
|
||||
static async addNoteCounts(folders: any[], includeCompletedTodos = true) {
|
||||
const foldersById: any = {};
|
||||
for (const f of folders) {
|
||||
foldersById[f.id] = f;
|
||||
@@ -170,7 +170,7 @@ export default class Folder extends BaseItem {
|
||||
|
||||
// Folders that contain notes that have been modified recently go on top.
|
||||
// The remaining folders, that don't contain any notes are sorted by their own user_updated_time
|
||||
public static async orderByLastModified(folders: FolderEntity[], dir = 'DESC') {
|
||||
static async orderByLastModified(folders: FolderEntity[], dir = 'DESC') {
|
||||
dir = dir.toUpperCase();
|
||||
const sql = 'select parent_id, max(user_updated_time) content_updated_time from notes where parent_id != "" group by parent_id';
|
||||
const rows = await this.db().selectAll(sql);
|
||||
@@ -228,7 +228,7 @@ export default class Folder extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static async all(options: any = null) {
|
||||
static async all(options: any = null) {
|
||||
const output = await super.all(options);
|
||||
if (options && options.includeConflictFolder) {
|
||||
const conflictCount = await Note.conflictedCount();
|
||||
@@ -237,7 +237,7 @@ export default class Folder extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static async childrenIds(folderId: string) {
|
||||
static async childrenIds(folderId: string) {
|
||||
const folders = await this.db().selectAll('SELECT id FROM folders WHERE parent_id = ?', [folderId]);
|
||||
|
||||
let output: string[] = [];
|
||||
@@ -252,7 +252,7 @@ export default class Folder extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static async expandTree(folders: FolderEntity[], parentId: string) {
|
||||
static async expandTree(folders: FolderEntity[], parentId: string) {
|
||||
const folderPath = await this.folderPath(folders, parentId);
|
||||
folderPath.pop(); // We don't expand the leaft notebook
|
||||
|
||||
@@ -542,7 +542,7 @@ export default class Folder extends BaseItem {
|
||||
logger.debug('updateNoLongerSharedItems:', report);
|
||||
}
|
||||
|
||||
public static async allAsTree(folders: FolderEntity[] = null, options: any = null) {
|
||||
static async allAsTree(folders: FolderEntity[] = null, options: any = null) {
|
||||
const all = folders ? folders : await this.all(options);
|
||||
|
||||
if (options && options.includeNotes) {
|
||||
@@ -576,7 +576,7 @@ export default class Folder extends BaseItem {
|
||||
return getNestedChildren(all, '');
|
||||
}
|
||||
|
||||
public static folderPath(folders: FolderEntity[], folderId: string) {
|
||||
static folderPath(folders: FolderEntity[], folderId: string) {
|
||||
const idToFolders: Record<string, FolderEntity> = {};
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
idToFolders[folders[i].id] = folders[i];
|
||||
@@ -595,7 +595,7 @@ export default class Folder extends BaseItem {
|
||||
return path;
|
||||
}
|
||||
|
||||
public static folderPathString(folders: FolderEntity[], folderId: string, maxTotalLength = 80) {
|
||||
static folderPathString(folders: FolderEntity[], folderId: string, maxTotalLength = 80) {
|
||||
const path = this.folderPath(folders, folderId);
|
||||
|
||||
let currentTotalLength = 0;
|
||||
@@ -616,7 +616,7 @@ export default class Folder extends BaseItem {
|
||||
return output.join(' / ');
|
||||
}
|
||||
|
||||
public static buildTree(folders: FolderEntity[]): FolderEntityWithChildren[] {
|
||||
static buildTree(folders: FolderEntity[]): FolderEntityWithChildren[] {
|
||||
const idToFolders: Record<string, any> = {};
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
idToFolders[folders[i].id] = Object.assign({}, folders[i]);
|
||||
@@ -644,7 +644,7 @@ export default class Folder extends BaseItem {
|
||||
return rootFolders;
|
||||
}
|
||||
|
||||
public static async sortFolderTree(folders: FolderEntityWithChildren[] = null) {
|
||||
static async sortFolderTree(folders: FolderEntityWithChildren[] = null) {
|
||||
const output = folders ? folders : await this.allAsTree();
|
||||
|
||||
const sortFoldersAlphabetically = (folders: FolderEntityWithChildren[]) => {
|
||||
@@ -672,16 +672,16 @@ export default class Folder extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static load(id: string, _options: any = null): Promise<FolderEntity> {
|
||||
static load(id: string, _options: any = null): Promise<FolderEntity> {
|
||||
if (id === this.conflictFolderId()) return Promise.resolve(this.conflictFolder());
|
||||
return super.load(id);
|
||||
}
|
||||
|
||||
public static defaultFolder() {
|
||||
static defaultFolder() {
|
||||
return this.modelSelectOne('SELECT * FROM folders ORDER BY created_time DESC LIMIT 1');
|
||||
}
|
||||
|
||||
public static async canNestUnder(folderId: string, targetFolderId: string) {
|
||||
static async canNestUnder(folderId: string, targetFolderId: string) {
|
||||
if (folderId === targetFolderId) return false;
|
||||
|
||||
const folder = await Folder.load(folderId);
|
||||
@@ -702,7 +702,7 @@ export default class Folder extends BaseItem {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async moveToFolder(folderId: string, targetFolderId: string) {
|
||||
static async moveToFolder(folderId: string, targetFolderId: string) {
|
||||
if (!(await this.canNestUnder(folderId, targetFolderId))) throw new Error(_('Cannot move notebook to this location'));
|
||||
|
||||
// When moving a note to a different folder, the user timestamp is not updated.
|
||||
@@ -721,7 +721,7 @@ export default class Folder extends BaseItem {
|
||||
// manually creating a folder. They shouldn't be done for example when the folders
|
||||
// are being synced to avoid any strange side-effects. Technically it's possible to
|
||||
// have folders and notes with duplicate titles (or no title), or with reserved words.
|
||||
public static async save(o: FolderEntity, options: any = null) {
|
||||
static async save(o: FolderEntity, options: any = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.userSideValidation === true) {
|
||||
|
@@ -22,11 +22,11 @@ export default class ItemChange extends BaseModel {
|
||||
public static SOURCE_SYNC = 2;
|
||||
public static SOURCE_DECRYPTION = 2; // CAREFUL - SAME ID AS SOURCE_SYNC!
|
||||
|
||||
public static tableName() {
|
||||
static tableName() {
|
||||
return 'item_changes';
|
||||
}
|
||||
|
||||
public static modelType() {
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_ITEM_CHANGE;
|
||||
}
|
||||
|
||||
|
@@ -5,15 +5,15 @@ import BaseItem from './BaseItem';
|
||||
import uuid from '../uuid';
|
||||
|
||||
export default class MasterKey extends BaseItem {
|
||||
public static tableName() {
|
||||
static tableName() {
|
||||
return 'master_keys';
|
||||
}
|
||||
|
||||
public static modelType() {
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_MASTER_KEY;
|
||||
}
|
||||
|
||||
public static encryptionSupported() {
|
||||
static encryptionSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default class MasterKey extends BaseItem {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], methods: number[]) {
|
||||
static allWithoutEncryptionMethod(masterKeys: MasterKeyEntity[], methods: number[]) {
|
||||
return masterKeys.filter(m => !methods.includes(m.encryption_method));
|
||||
}
|
||||
|
||||
|
@@ -10,19 +10,19 @@ const migrationScripts: Record<number, any> = {
|
||||
};
|
||||
|
||||
export default class Migration extends BaseModel {
|
||||
public static tableName() {
|
||||
static tableName() {
|
||||
return 'migrations';
|
||||
}
|
||||
|
||||
public static modelType() {
|
||||
static modelType() {
|
||||
return BaseModel.TYPE_MIGRATION;
|
||||
}
|
||||
|
||||
public static migrationsToDo() {
|
||||
static migrationsToDo() {
|
||||
return this.modelSelectAll('SELECT * FROM migrations ORDER BY number ASC');
|
||||
}
|
||||
|
||||
public static script(number: number) {
|
||||
static script(number: number) {
|
||||
if (!migrationScripts[number]) throw new Error('Migration script has not been added to "migrationScripts" array');
|
||||
return migrationScripts[number];
|
||||
}
|
||||
|
@@ -409,36 +409,38 @@ describe('models/Note', () => {
|
||||
expect(movedNote.conflict_original_id).toBe('');
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('models/Note_replacePaths', () => {
|
||||
|
||||
function testResourceReplacment(body: string, pathsToTry: string[], expected: string) {
|
||||
expect(Note['replaceResourceExternalToInternalLinks_'](pathsToTry, body)).toBe(expected);
|
||||
}
|
||||
|
||||
test('basic replacement', () => {
|
||||
test('Basic replacement', () => {
|
||||
const body = '';
|
||||
const pathsToTry = ['file:///C:Users/Username/resources'];
|
||||
const expected = '';
|
||||
testResourceReplacment(body, pathsToTry, expected);
|
||||
});
|
||||
|
||||
test('replacement with spaces', () => {
|
||||
test('Replacement with spaces', () => {
|
||||
const body = '';
|
||||
const pathsToTry = ['file:///C:Users/Username with spaces/resources'];
|
||||
const expected = '';
|
||||
testResourceReplacment(body, pathsToTry, expected);
|
||||
});
|
||||
|
||||
test('replacement with Non-ASCII', () => {
|
||||
test('Replacement with Non-ASCII', () => {
|
||||
const body = '';
|
||||
const pathsToTry = ['file:///C:Users/UsernameWithéàö/resources'];
|
||||
const expected = '';
|
||||
testResourceReplacment(body, pathsToTry, expected);
|
||||
});
|
||||
|
||||
test('replacement with Non-ASCII and spaces', () => {
|
||||
test('Replacement with Non-ASCII and spaces', () => {
|
||||
const body = '';
|
||||
const pathsToTry = ['file:///C:Users/Username With éàö/resources'];
|
||||
const expected = '';
|
||||
testResourceReplacment(body, pathsToTry, expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user