You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-27 20:29:45 +02:00
Compare commits
51 Commits
publish_no
...
android-v2
Author | SHA1 | Date | |
---|---|---|---|
|
cea1aeac4b | ||
|
13ee1c89ea | ||
|
f01ec941b7 | ||
|
0853521bc9 | ||
|
e484671a08 | ||
|
50253d00e7 | ||
|
5364965a69 | ||
|
50baad3c04 | ||
|
cf219762c9 | ||
|
9e27b0881f | ||
|
44a96f347a | ||
|
cc6620a7e1 | ||
|
29f1abb666 | ||
|
9781a33419 | ||
|
0954794195 | ||
|
a996375b88 | ||
|
129ac1829d | ||
|
44e60bdda9 | ||
|
afc34b44c8 | ||
|
e08c74ae08 | ||
|
e5c669dc7a | ||
|
f4a7f5914e | ||
|
62eee4df56 | ||
|
c16445bc2f | ||
|
e05c5598a0 | ||
|
66c9ee0a1a | ||
|
d07788607c | ||
|
907dc7601b | ||
|
4b9adcde04 | ||
|
9f3a4e0d99 | ||
|
ea14488dc3 | ||
|
f59d29f1c5 | ||
|
0a9e919ac7 | ||
|
f11b6e8fa9 | ||
|
167560ff6f | ||
|
4b4e316bf0 | ||
|
7809228bd3 | ||
|
540fbbc22c | ||
|
2983d4f1a3 | ||
|
f6a8bf9ea2 | ||
|
e3ba02281b | ||
|
295b310079 | ||
|
62346575f8 | ||
|
0a590b7de9 | ||
|
dfd95f8385 | ||
|
6efe8c171a | ||
|
a7cdcaf25f | ||
|
6277958d6a | ||
|
24b4b879f2 | ||
|
86fbf82d36 | ||
|
96982849ce |
@@ -333,6 +333,9 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js.map
|
||||
@@ -597,6 +600,9 @@ packages/app-desktop/gui/OneDriveLoginScreen.js.map
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.d.ts
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js.map
|
||||
packages/app-desktop/gui/PdfViewer.d.ts
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PdfViewer.js.map
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.d.ts
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
|
||||
@@ -921,6 +927,9 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js.map
|
||||
@@ -2001,6 +2010,9 @@ packages/lib/uuid.js.map
|
||||
packages/lib/versionInfo.d.ts
|
||||
packages/lib/versionInfo.js
|
||||
packages/lib/versionInfo.js.map
|
||||
packages/pdf-viewer/FullViewer.d.ts
|
||||
packages/pdf-viewer/FullViewer.js
|
||||
packages/pdf-viewer/FullViewer.js.map
|
||||
packages/pdf-viewer/Page.d.ts
|
||||
packages/pdf-viewer/Page.js
|
||||
packages/pdf-viewer/Page.js.map
|
||||
@@ -2025,9 +2037,15 @@ packages/pdf-viewer/hooks/useScaledSize.js.map
|
||||
packages/pdf-viewer/hooks/useScrollSaver.d.ts
|
||||
packages/pdf-viewer/hooks/useScrollSaver.js
|
||||
packages/pdf-viewer/hooks/useScrollSaver.js.map
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.d.ts
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js.map
|
||||
packages/pdf-viewer/main.d.ts
|
||||
packages/pdf-viewer/main.js
|
||||
packages/pdf-viewer/main.js.map
|
||||
packages/pdf-viewer/messageService.d.ts
|
||||
packages/pdf-viewer/messageService.js
|
||||
packages/pdf-viewer/messageService.js.map
|
||||
packages/pdf-viewer/miniViewer.d.ts
|
||||
packages/pdf-viewer/miniViewer.js
|
||||
packages/pdf-viewer/miniViewer.js.map
|
||||
@@ -2037,6 +2055,9 @@ packages/pdf-viewer/pdfSource.test.js.map
|
||||
packages/pdf-viewer/types.d.ts
|
||||
packages/pdf-viewer/types.js
|
||||
packages/pdf-viewer/types.js.map
|
||||
packages/pdf-viewer/ui/GotoPage.d.ts
|
||||
packages/pdf-viewer/ui/GotoPage.js
|
||||
packages/pdf-viewer/ui/GotoPage.js.map
|
||||
packages/pdf-viewer/ui/IconButtons.d.ts
|
||||
packages/pdf-viewer/ui/IconButtons.js
|
||||
packages/pdf-viewer/ui/IconButtons.js.map
|
||||
|
@@ -90,6 +90,8 @@ module.exports = {
|
||||
// Disable because of this: https://github.com/facebook/react/issues/16265
|
||||
// "react-hooks/exhaustive-deps": "warn",
|
||||
|
||||
'promise/prefer-await-to-then': 'error',
|
||||
|
||||
// -------------------------------
|
||||
// Formatting
|
||||
// -------------------------------
|
||||
@@ -141,6 +143,7 @@ module.exports = {
|
||||
'@seiyab/eslint-plugin-react-hooks',
|
||||
// 'react-hooks',
|
||||
'import',
|
||||
'promise',
|
||||
],
|
||||
'overrides': [
|
||||
{
|
||||
|
4
.github/scripts/run_ci.sh
vendored
4
.github/scripts/run_ci.sh
vendored
@@ -175,8 +175,8 @@ cd "$ROOT_DIR/packages/app-desktop"
|
||||
|
||||
if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
echo "Step: Building and publishing desktop application..."
|
||||
cd "$ROOT_DIR/packages/tools"
|
||||
node bundleDefaultPlugins.js
|
||||
# cd "$ROOT_DIR/packages/tools"
|
||||
# node bundleDefaultPlugins.js
|
||||
cd "$ROOT_DIR/packages/app-desktop"
|
||||
USE_HARD_LINKS=false yarn run dist
|
||||
elif [[ $IS_LINUX = 1 ]] && [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then
|
||||
|
1
.github/workflows/github-actions-main.yml
vendored
1
.github/workflows/github-actions-main.yml
vendored
@@ -5,7 +5,6 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# Removed windows-2016 for now - discontinued by GitHub
|
||||
os: [macos-latest, ubuntu-latest, windows-2019]
|
||||
steps:
|
||||
|
||||
|
21
.gitignore
vendored
21
.gitignore
vendored
@@ -321,6 +321,9 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js.map
|
||||
@@ -585,6 +588,9 @@ packages/app-desktop/gui/OneDriveLoginScreen.js.map
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.d.ts
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js.map
|
||||
packages/app-desktop/gui/PdfViewer.d.ts
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PdfViewer.js.map
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.d.ts
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
|
||||
@@ -909,6 +915,9 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js.map
|
||||
@@ -1989,6 +1998,9 @@ packages/lib/uuid.js.map
|
||||
packages/lib/versionInfo.d.ts
|
||||
packages/lib/versionInfo.js
|
||||
packages/lib/versionInfo.js.map
|
||||
packages/pdf-viewer/FullViewer.d.ts
|
||||
packages/pdf-viewer/FullViewer.js
|
||||
packages/pdf-viewer/FullViewer.js.map
|
||||
packages/pdf-viewer/Page.d.ts
|
||||
packages/pdf-viewer/Page.js
|
||||
packages/pdf-viewer/Page.js.map
|
||||
@@ -2013,9 +2025,15 @@ packages/pdf-viewer/hooks/useScaledSize.js.map
|
||||
packages/pdf-viewer/hooks/useScrollSaver.d.ts
|
||||
packages/pdf-viewer/hooks/useScrollSaver.js
|
||||
packages/pdf-viewer/hooks/useScrollSaver.js.map
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.d.ts
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js.map
|
||||
packages/pdf-viewer/main.d.ts
|
||||
packages/pdf-viewer/main.js
|
||||
packages/pdf-viewer/main.js.map
|
||||
packages/pdf-viewer/messageService.d.ts
|
||||
packages/pdf-viewer/messageService.js
|
||||
packages/pdf-viewer/messageService.js.map
|
||||
packages/pdf-viewer/miniViewer.d.ts
|
||||
packages/pdf-viewer/miniViewer.js
|
||||
packages/pdf-viewer/miniViewer.js.map
|
||||
@@ -2025,6 +2043,9 @@ packages/pdf-viewer/pdfSource.test.js.map
|
||||
packages/pdf-viewer/types.d.ts
|
||||
packages/pdf-viewer/types.js
|
||||
packages/pdf-viewer/types.js.map
|
||||
packages/pdf-viewer/ui/GotoPage.d.ts
|
||||
packages/pdf-viewer/ui/GotoPage.js
|
||||
packages/pdf-viewer/ui/GotoPage.js.map
|
||||
packages/pdf-viewer/ui/IconButtons.d.ts
|
||||
packages/pdf-viewer/ui/IconButtons.js
|
||||
packages/pdf-viewer/ui/IconButtons.js.map
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 08 Aug 2022 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 08 Aug 2022 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin first meetup on 30 August!]]></title><description><![CDATA[<p>We are glad to announce <a href="https://www.meetup.com/joplin/events/287611873/">the first Joplin Meetup</a> that will take place on 30 August 2022 in London!</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>Tue, 06 Sep 2022 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Tue, 06 Sep 2022 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin interview on Website Planet]]></title><description><![CDATA[<p>Website Planet has recently conducted an interview about Joplin - it may give you some insight on the current status of the project, our priorities, and future plans! More on the article page - <a href="https://www.websiteplanet.com/blog/interview-joplin/">Organise Your Thoughts with Open Source Note-Taking App, Joplin</a></p>
|
||||
]]></description><link>https://joplinapp.org/news/20220906-interview-websiteplanet/</link><guid isPermaLink="false">20220906-interview-websiteplanet</guid><pubDate>Tue, 06 Sep 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin first meetup on 30 August!]]></title><description><![CDATA[<p>We are glad to announce <a href="https://www.meetup.com/joplin/events/287611873/">the first Joplin Meetup</a> that will take place on 30 August 2022 in London!</p>
|
||||
<p>This is an opportunity to meet other Joplin users as well as some of the main contributors, to discuss the apps, or to ask questions and exchange tips and tricks on how to use the app, develop plugins or contribute to the application. Everybody, technical or not, is welcome!</p>
|
||||
<p>We will meet at the Old Thameside Inn next to London Bridge. If the weather allows we will be on the terrace outside, if not inside.</p>
|
||||
<p>More information on the official Meetup page:</p>
|
||||
@@ -258,7 +259,4 @@
|
||||
<p>Also many thanks to everyone who voted and contributed to the tagline discussion! It helped narrow down what the tagline should be, along with the equally important description below. If you have any question or notice any issue with the website let me know!</p>
|
||||
]]></description><link>https://joplinapp.org/news/20210711-095626/</link><guid isPermaLink="false">20210711-095626</guid><pubDate>Sun, 11 Jul 2021 09:56:26 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Poll: What should Joplin tagline be?]]></title><description><![CDATA[<p>Thanks everyone for your tagline suggestions - there were lots of good ideas in there. I've compiled a few of them and create a poll in the forum, so please cast your vote! And if you have any other suggestions on what would make a good tagline, feel free to post over there or here.</p>
|
||||
<p><a href="https://discourse.joplinapp.org/t/poll-what-should-joplin-tagline-be/18487">https://discourse.joplinapp.org/t/poll-what-should-joplin-tagline-be/18487</a></p>
|
||||
]]></description><link>https://joplinapp.org/news/20210706-140228/</link><guid isPermaLink="false">20210706-140228</guid><pubDate>Tue, 06 Jul 2021 14:02:28 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Any ideas for a Joplin tagline?]]></title><description><![CDATA[<p>I'm going to update the website front page to better showcase the application. I have most of the sections right, but the part I'm still not sure about is the top tagline, so I'm wondering if anyone had any suggestion about it?</p>
|
||||
<p>From what I can see on Google Keep or Evernote for example it should be something like "Use our app to get X or Y benefit", it should be a sentence that directly speaks to the user essentially.</p>
|
||||
<p>So far I have "Your notes, anywhere you are" but I'm not certain that's particularly inspiring. Any other idea about what tagline could be used?</p>
|
||||
]]></description><link>https://joplinapp.org/news/20210705-094247/</link><guid isPermaLink="false">20210705-094247</guid><pubDate>Mon, 05 Jul 2021 09:42:47 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20210706-140228/</link><guid isPermaLink="false">20210706-140228</guid><pubDate>Tue, 06 Jul 2021 14:02:28 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
|
@@ -31,7 +31,7 @@ Joplin is available in multiple languages thanks to the help of its users. You c
|
||||
|
||||
If you want to start contributing to the project's code, please follow these guidelines before creating a pull request:
|
||||
|
||||
- Explain WHY you want to add this change. Explain it inside the pull request and you may link to an issue for additional information, but the PR should give a clear overview of why you want to add this.
|
||||
- The top post of the pull request should contain a full, self-contained explanation of the feature: what it does, how it does it, with examples of usage and screenshots. Also explain why you want to add this - what problem does it solve. Do not simply add a text `Implement feature #4345` or link to forum posts, because the information there will most likely be outdated or confusing (multiple discussions and opinions). The pull request needs to be self-contained.
|
||||
- Bug fixes are always welcome. Start by reviewing the [list of bugs](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
||||
- A good way to easily start contributing is to pick and work on a [good first issue](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). We try to make these issues as clear as possible and provide basic info on how the code should be changed, and if something is unclear feel free to ask for more information on the issue.
|
||||
- Before adding a new feature, ask about it in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue) or the [Joplin Forum](https://discourse.joplinapp.org/), or check if existing discussions exist to make sure the new functionality is desired.
|
||||
|
@@ -346,8 +346,8 @@ If you provide a configuration and you receive "success!" on the "check config"
|
||||
- Force Path Style: unchecked
|
||||
|
||||
### Linode
|
||||
- URL: https://<region>.linodeobjects.com
|
||||
- Region: empty
|
||||
- URL: https://regionName.linodeobjects.com (regionName is the region on the URL provided by Linode; this URL is also the same as the URL provided by Linode with the bucket name removed)
|
||||
- Region: Anything you want to type, can't be left empty
|
||||
- Force Path Style: unchecked
|
||||
|
||||
### UpCloud
|
||||
|
@@ -9,3 +9,7 @@ Only the latest version is supported with security updates.
|
||||
Please [contact support](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/AdresseSupport.png) **with a proof of concept** that shows the security vulnerability. Please do not contact us without this proof of concept, as we cannot fix anything without this.
|
||||
|
||||
For general opinions on what makes an app more or less secure, please use the forum.
|
||||
|
||||
## Bounty
|
||||
|
||||
We **do not** offer a bounty for discovering vulnerabilities, please do not ask. We can however credit you and link to your website in the changelog and release announcement.
|
||||
|
@@ -69,6 +69,7 @@
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-interactive": "^10.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.0.1",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"fs-extra": "^8.1.0",
|
||||
"glob": "^7.1.6",
|
||||
|
@@ -124,6 +124,7 @@ async function handleAutocompletionPromise(line) {
|
||||
return line;
|
||||
}
|
||||
function handleAutocompletion(str, callback) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
handleAutocompletionPromise(str).then(function(res) {
|
||||
callback(undefined, res);
|
||||
});
|
||||
|
@@ -36,6 +36,7 @@ async function createClients() {
|
||||
const client = createClient(clientId);
|
||||
promises.push(fs.remove(client.profileDir));
|
||||
promises.push(
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
execCommand(client, 'config sync.target 2').then(() => {
|
||||
return execCommand(client, `config sync.2.path ${syncDir}`);
|
||||
})
|
||||
@@ -2324,10 +2325,12 @@ async function main() {
|
||||
clients[clientId].activeCommandCount++;
|
||||
|
||||
execRandomCommand(clients[clientId])
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.catch(error => {
|
||||
logger.info(`Client ${clientId}:`);
|
||||
logger.error(error);
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then(r => {
|
||||
if (r) {
|
||||
logger.info(`Client ${clientId}:\n${r.trim()}`);
|
||||
|
@@ -50,6 +50,7 @@ export default class PluginRunner extends BasePluginRunner {
|
||||
const callId = `${pluginId}::${path}::${uuid.createNano()}`;
|
||||
this.activeSandboxCalls_[callId] = true;
|
||||
const promise = executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler);
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
promise.finally(() => {
|
||||
delete this.activeSandboxCalls_[callId];
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { installDefaultPlugins, getDefaultPluginsInstallState, setSettingsForDefaultPlugins, checkPreInstalledDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||
import PluginRunner from '../../../app/services/plugins/PluginRunner';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import { setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { checkThrow, setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import PluginService, { defaultPluginSetting, DefaultPluginsInfo, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
@@ -213,4 +213,57 @@ describe('defaultPluginsUtils', function() {
|
||||
await service.destroy();
|
||||
});
|
||||
|
||||
it('should not throw error on missing setting key', async () => {
|
||||
|
||||
const service = newPluginService();
|
||||
|
||||
const pluginScript = `
|
||||
/* joplin-manifest:
|
||||
{
|
||||
"id": "io.github.jackgruber.backup",
|
||||
"manifest_version": 1,
|
||||
"app_min_version": "1.4",
|
||||
"name": "JS Bundle test",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
*/
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.settings.registerSettings({
|
||||
path: {
|
||||
value: "initial-path",
|
||||
type: 2,
|
||||
section: "backupSection",
|
||||
public: true,
|
||||
label: "Backup path",
|
||||
},
|
||||
})
|
||||
},
|
||||
});`;
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
const defaultPluginsInfo: DefaultPluginsInfo = {
|
||||
'io.github.jackgruber.backup': {
|
||||
version: '1.0.2',
|
||||
settings: {
|
||||
'path': `${Setting.value('profileDir')}`,
|
||||
'missing-key1': 'someValue',
|
||||
},
|
||||
},
|
||||
'plugin.calebjohn.rich-markdown': {
|
||||
version: '0.8.3',
|
||||
settings: {
|
||||
'missing-key2': 'someValue',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Setting.setValue('installedDefaultPlugins', ['']);
|
||||
expect(checkThrow(() => setSettingsForDefaultPlugins(defaultPluginsInfo))).toBe(false);
|
||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`);
|
||||
await service.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -278,8 +278,8 @@ class Application extends BaseApplication {
|
||||
checkPreInstalledDefaultPlugins(defaultPluginsId, pluginSettings);
|
||||
|
||||
try {
|
||||
const pluginsDir = path.join(bridge().buildDir(), 'defaultPlugins');
|
||||
pluginSettings = await installDefaultPlugins(service, pluginsDir, defaultPluginsId, pluginSettings);
|
||||
const defaultPluginsDir = path.join(bridge().buildDir(), 'defaultPlugins');
|
||||
pluginSettings = await installDefaultPlugins(service, defaultPluginsDir, defaultPluginsId, pluginSettings);
|
||||
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
|
||||
await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
|
||||
}
|
||||
@@ -502,6 +502,7 @@ class Application extends BaseApplication {
|
||||
if (Setting.value('env') === 'dev') {
|
||||
void AlarmService.updateAllNotifications();
|
||||
} else {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
void reg.scheduleSync(1000).then(() => {
|
||||
// Wait for the first sync before updating the notifications, since synchronisation
|
||||
// might change the notifications.
|
||||
|
@@ -246,7 +246,7 @@ export class Bridge {
|
||||
}
|
||||
|
||||
async openItem(fullPath: string) {
|
||||
return require('electron').shell.openPath(fullPath);
|
||||
return require('electron').shell.openPath(toSystemSlashes(fullPath));
|
||||
}
|
||||
|
||||
screen() {
|
||||
|
@@ -45,6 +45,7 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
if (confirm(_('Are you sure you want to renew the authorisation token?'))) {
|
||||
void EncryptionService.instance()
|
||||
.generateApiToken()
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then((token) => {
|
||||
Setting.setValue('api.token', token);
|
||||
});
|
||||
|
@@ -15,6 +15,7 @@ import * as openFolder from './openFolder';
|
||||
import * as openFolderDialog from './openFolderDialog';
|
||||
import * as openItem from './openItem';
|
||||
import * as openNote from './openNote';
|
||||
import * as openPdfViewer from './openPdfViewer';
|
||||
import * as openTag from './openTag';
|
||||
import * as print from './print';
|
||||
import * as renameFolder from './renameFolder';
|
||||
@@ -55,6 +56,7 @@ const index:any[] = [
|
||||
openFolderDialog,
|
||||
openItem,
|
||||
openNote,
|
||||
openPdfViewer,
|
||||
openTag,
|
||||
print,
|
||||
renameFolder,
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'openPdfViewer',
|
||||
label: () => _('Open PDF viewer'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, resourceId: string, pageNo: number) => {
|
||||
|
||||
const resource = await Resource.load(resourceId);
|
||||
if (!resource) throw new Error(`No such resource: ${resourceId}`);
|
||||
if (resource.mime !== 'application/pdf') throw new Error(`Not a PDF: ${resource.mime}`);
|
||||
console.log('Opening PDF', resource);
|
||||
context.dispatch({
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'pdfViewer',
|
||||
props: {
|
||||
resource,
|
||||
pageNo: pageNo,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
@@ -848,6 +848,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
|
||||
function renderEditor() {
|
||||
|
||||
const matchBracesOptions = Setting.value('editor.autoMatchingBraces') ? { override: true, pairs: '<>()[]{}\'\'""‘’“”()《》「」『』【】〔〕〖〗〘〙〚〛' } : false;
|
||||
|
||||
return (
|
||||
<div style={cellEditorStyle}>
|
||||
<Editor
|
||||
@@ -858,7 +860,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
codeMirrorTheme={styles.editor.codeMirrorTheme}
|
||||
style={styles.editor}
|
||||
readOnly={props.visiblePanes.indexOf('editor') < 0}
|
||||
autoMatchBraces={Setting.value('editor.autoMatchingBraces')}
|
||||
autoMatchBraces={matchBracesOptions}
|
||||
keyMap={props.keyboardMode}
|
||||
plugins={props.plugins}
|
||||
onChange={codeMirror_change}
|
||||
|
@@ -86,7 +86,7 @@ export interface EditorProps {
|
||||
style: any;
|
||||
codeMirrorTheme: any;
|
||||
readOnly: boolean;
|
||||
autoMatchBraces: boolean;
|
||||
autoMatchBraces: boolean | object;
|
||||
keyMap: string;
|
||||
plugins: PluginStates;
|
||||
onChange: any;
|
||||
|
@@ -1008,7 +1008,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
} else { // Paste regular text
|
||||
const pastedHtml = event.clipboardData.getData('text/html');
|
||||
// event.clipboardData.getData('text/html') wraps the content with <html><body></body></html>,
|
||||
// which seems to be not supported in editor.insertContent().
|
||||
const pastedHtml = clipboard.readHTML();
|
||||
if (pastedHtml) { // Handles HTML
|
||||
const modifiedHtml = await processPastedHtml(pastedHtml);
|
||||
editor.insertContent(modifiedHtml);
|
||||
|
@@ -52,6 +52,8 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
|
||||
void CommandService.instance().execute(commandName, ...commandArgs);
|
||||
} else if (msg === 'postMessageService.message') {
|
||||
void PostMessageService.instance().postMessage(arg0);
|
||||
} else if (msg === 'openPdfViewer') {
|
||||
await CommandService.instance().execute('openPdfViewer', arg0.resourceId, arg0.pageNo);
|
||||
} else {
|
||||
await CommandService.instance().execute('openItem', msg);
|
||||
// bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
|
||||
|
101
packages/app-desktop/gui/PdfViewer.tsx
Normal file
101
packages/app-desktop/gui/PdfViewer.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import bridge from '../services/bridge';
|
||||
import contextMenu from './NoteEditor/utils/contextMenu';
|
||||
import { ContextMenuItemType, ContextMenuOptions } from './NoteEditor/utils/contextMenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import styled from 'styled-components';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
const Window = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
z-index: 999;
|
||||
background-color: ${(props: any) => props.theme.backgroundColor};
|
||||
color: ${(props: any) => props.theme.color};
|
||||
`;
|
||||
|
||||
const IFrame = styled.iframe`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Function;
|
||||
resource: any;
|
||||
pageNo: number;
|
||||
}
|
||||
|
||||
export default function PdfViewer(props: Props) {
|
||||
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
props.dispatch({
|
||||
type: 'DIALOG_CLOSE',
|
||||
name: 'pdfViewer',
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
const openExternalViewer = useCallback(async () => {
|
||||
await CommandService.instance().execute('openItem', `joplin://${props.resource.id}`);
|
||||
}, [props.resource.id]);
|
||||
|
||||
const textSelected = useCallback(async (text: string) => {
|
||||
if (!text) return;
|
||||
const itemType = ContextMenuItemType.Text;
|
||||
const menu = await contextMenu({
|
||||
itemType,
|
||||
resourceId: null,
|
||||
filename: null,
|
||||
mime: 'text/plain',
|
||||
textToCopy: text,
|
||||
linkToCopy: null,
|
||||
htmlToCopy: '',
|
||||
insertContent: () => { console.warn('insertContent() not implemented'); },
|
||||
} as ContextMenuOptions, props.dispatch);
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}, [props.dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage_ = async (event: any) =>{
|
||||
if (!event.data || !event.data.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.name === 'close') {
|
||||
onClose();
|
||||
} else if (event.data.name === 'externalViewer') {
|
||||
await openExternalViewer();
|
||||
} else if (event.data.name === 'textSelected') {
|
||||
await textSelected(event.data.text);
|
||||
} else {
|
||||
console.error('Unknown event received', event.data.name);
|
||||
}
|
||||
};
|
||||
const iframe = iframeRef.current;
|
||||
iframe.contentWindow.addEventListener('message', onMessage_);
|
||||
return () => {
|
||||
iframe.contentWindow.removeEventListener('message', onMessage_);
|
||||
};
|
||||
}, [onClose, openExternalViewer, textSelected]);
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
return (
|
||||
<Window theme={theme}>
|
||||
<IFrame src="./vendor/lib/@joplin/pdf-viewer/index.html" x-url={Resource.fullPath(props.resource)}
|
||||
x-appearance={theme.appearance} ref={iframeRef}
|
||||
x-title={props.resource.title}
|
||||
x-anchorpage={props.pageNo}
|
||||
x-type="full"></IFrame>
|
||||
</Window>
|
||||
);
|
||||
}
|
@@ -174,9 +174,11 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
return;
|
||||
}
|
||||
Resource.delete(resource.id)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.catch((error: Error) => {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.finally(() => {
|
||||
void this.reloadResources(this.state.sorting);
|
||||
});
|
||||
|
@@ -22,6 +22,7 @@ import Dialog from './Dialog';
|
||||
import SyncWizardDialog from './SyncWizard/Dialog';
|
||||
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
|
||||
import EditFolderDialog from './EditFolderDialog/Dialog';
|
||||
import PdfViewer from './PdfViewer';
|
||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
@@ -75,6 +76,11 @@ const registeredDialogs: Record<string, RegisteredDialog> = {
|
||||
return <EditFolderDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
||||
},
|
||||
},
|
||||
pdfViewer: {
|
||||
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||
return <PdfViewer key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
|
@@ -579,6 +579,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
ipc.textSelected = function(event) {
|
||||
ipcProxySendToHost('contextMenu', {
|
||||
type: 'text',
|
||||
textToCopy: event.text,
|
||||
});
|
||||
}
|
||||
|
||||
ipc.openPdfViewer = function(event) {
|
||||
ipcProxySendToHost('openPdfViewer', { resourceId: event.resourceId, mime: 'application/pdf', pageNo: event.pageNo || 1 });
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', webviewLib.logEnabledEventHandler(e => {
|
||||
if (!window.location.hash) return;
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.9.4",
|
||||
"version": "2.9.8",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
@@ -146,8 +146,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097670
|
||||
versionName "2.9.2"
|
||||
versionCode 2097671
|
||||
versionName "2.9.3"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -122,7 +122,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
};
|
||||
|
||||
const itemRenderer = (item: DropdownListItem) => {
|
||||
const key = item.value.toString();
|
||||
const key = item.value ? item.value.toString() : '__null'; // The top item ("Move item to notebook...") has a null value.
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={itemWrapperStyle}
|
||||
|
@@ -36,6 +36,10 @@ type OnFileUpdateCallback = (event: SourceFileUpdateEvent)=> void;
|
||||
interface Props {
|
||||
themeId: number;
|
||||
|
||||
// A name to be associated with the WebView (e.g. NoteEditor)
|
||||
// This name should be unique.
|
||||
webviewInstanceId: string;
|
||||
|
||||
// If HTML is still being loaded, [html] should be an empty string.
|
||||
html: string;
|
||||
|
||||
@@ -81,7 +85,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function createHtmlFile() {
|
||||
const tempFile = `${Setting.value('resourceDir')}/NoteEditor.html`;
|
||||
const tempFile = `${Setting.value('resourceDir')}/${props.webviewInstanceId}.html`;
|
||||
await shim.fsDriver().writeFile(tempFile, props.html, 'utf8');
|
||||
if (cancelled) return;
|
||||
|
||||
@@ -110,7 +114,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [props.html, props.onFileUpdate]);
|
||||
}, [props.html, props.webviewInstanceId, props.onFileUpdate]);
|
||||
|
||||
// - `setSupportMultipleWindows` must be `true` for security reasons:
|
||||
// https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0
|
||||
|
@@ -89,6 +89,7 @@ export default function NoteBodyViewer(props: Props) {
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<ExtendedWebView
|
||||
webviewInstanceId='NoteBodyViewer'
|
||||
themeId={props.themeId}
|
||||
style={webViewStyle}
|
||||
html={html}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
// A toolbar for the markdown editor.
|
||||
|
||||
const React = require('react');
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
import { useMemo, useState, useCallback } from 'react';
|
||||
|
||||
// See https://oblador.github.io/react-native-vector-icons/ for a list of
|
||||
@@ -20,6 +20,7 @@ import { ButtonSpec, StyleSheetData } from './types';
|
||||
import Toolbar from './Toolbar';
|
||||
import { buttonSize } from './ToolbarButton';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import ToggleSpaceButton from './ToggleSpaceButton';
|
||||
|
||||
type OnAttachCallback = ()=> void;
|
||||
|
||||
@@ -277,7 +278,11 @@ const MarkdownToolbar = (props: MarkdownToolbarProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToggleSpaceButton
|
||||
spaceApplicable={ Platform.OS === 'ios' && keyboardVisible }
|
||||
themeId={props.editorSettings.themeId}
|
||||
style={styles.container}
|
||||
>
|
||||
<Toolbar
|
||||
styleSheet={styleData}
|
||||
buttons={[
|
||||
@@ -299,21 +304,16 @@ const MarkdownToolbar = (props: MarkdownToolbarProps) => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={{
|
||||
// The keyboard on iOS can overlap the markdown toolbar.
|
||||
// Add additional padding to prevent this.
|
||||
height: (
|
||||
Platform.OS === 'ios' && keyboardVisible ? 16 : 0
|
||||
),
|
||||
}}/>
|
||||
</>
|
||||
</ToggleSpaceButton>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = (styleProps: any, theme: Theme) => {
|
||||
return useMemo(() => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
...styleProps,
|
||||
},
|
||||
button: {
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
@@ -348,10 +348,8 @@ const useStyles = (styleProps: any, theme: Theme) => {
|
||||
|
||||
// Add a small amount of additional padding for button borders
|
||||
height: buttonSize + 6,
|
||||
...styleProps,
|
||||
},
|
||||
toolbarContainer: {
|
||||
maxHeight: '65%',
|
||||
flexShrink: 1,
|
||||
},
|
||||
toolbarContent: {
|
||||
|
@@ -0,0 +1,96 @@
|
||||
|
||||
// On some devices, the SafeAreaView conflicts with the KeyboardAvoidingView, creating
|
||||
// additional (or a lack of additional) space at the bottom of the screen. Because this
|
||||
// is different on different devices, this button allows toggling additional space a the bottom
|
||||
// of the screen to compensate.
|
||||
|
||||
// Works around https://github.com/facebook/react-native/issues/13393 by adding additional
|
||||
// space below the given component when the keyboard is visible unless a button is pressed.
|
||||
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
|
||||
import * as React from 'react';
|
||||
import { ReactNode, useCallback, useState, useEffect } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import CustomButton from '../../CustomButton';
|
||||
|
||||
const AntIcon = require('react-native-vector-icons/AntDesign').default;
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
spaceApplicable: boolean;
|
||||
themeId: number;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
const ToggleSpaceButton = (props: Props) => {
|
||||
const [additionalSpace, setAdditionalSpace] = useState(0);
|
||||
const [decreaseSpaceBtnVisible, setDecreaseSpaceBtnVisible] = useState(true);
|
||||
|
||||
// Some devices need space added, others need space removed.
|
||||
const additionalPositiveSpace = 14;
|
||||
const additionalNegativeSpace = -14;
|
||||
|
||||
// Switch from adding +14px to -14px.
|
||||
const onDecreaseSpace = useCallback(() => {
|
||||
setAdditionalSpace(additionalNegativeSpace);
|
||||
setDecreaseSpaceBtnVisible(false);
|
||||
Setting.setValue('editor.mobile.removeSpaceBelowToolbar', true);
|
||||
}, [setAdditionalSpace, setDecreaseSpaceBtnVisible, additionalNegativeSpace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Setting.value('editor.mobile.removeSpaceBelowToolbar')) {
|
||||
onDecreaseSpace();
|
||||
}
|
||||
}, [onDecreaseSpace]);
|
||||
|
||||
const theme: Theme = themeStyle(props.themeId);
|
||||
|
||||
const decreaseSpaceButton = (
|
||||
<>
|
||||
<View style={{
|
||||
height: additionalPositiveSpace,
|
||||
zIndex: -2,
|
||||
}} />
|
||||
<CustomButton
|
||||
themeId={props.themeId}
|
||||
description={'Move toolbar to bottom of screen'}
|
||||
style={{
|
||||
height: additionalPositiveSpace,
|
||||
width: '100%',
|
||||
|
||||
// Ensure that the icon is near the bottom of the screen,
|
||||
// and thus invisible on devices where it isn't necessary.
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
|
||||
// Don't show the button on top of views with content.
|
||||
zIndex: -1,
|
||||
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onPress={onDecreaseSpace}
|
||||
>
|
||||
<AntIcon name='down' style={{
|
||||
color: theme.color,
|
||||
}}/>
|
||||
</CustomButton>
|
||||
</>
|
||||
);
|
||||
|
||||
const style: ViewStyle = {
|
||||
marginBottom: props.spaceApplicable ? additionalSpace : 0,
|
||||
...props.style,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
{props.children}
|
||||
{ decreaseSpaceBtnVisible && props.spaceApplicable ? decreaseSpaceButton : null }
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleSpaceButton;
|
@@ -2,7 +2,7 @@ const React = require('react');
|
||||
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ReactElement, useCallback, useState } from 'react';
|
||||
import { AccessibilityInfo, LayoutChangeEvent, ScrollView, View } from 'react-native';
|
||||
import { AccessibilityInfo, LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native';
|
||||
import ToggleOverflowButton from './ToggleOverflowButton';
|
||||
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
||||
import ToolbarOverflowRows from './ToolbarOverflowRows';
|
||||
@@ -11,6 +11,7 @@ import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
|
||||
interface ToolbarProps {
|
||||
buttons: ButtonGroup[];
|
||||
styleSheet: StyleSheetData;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
// Displays a list of buttons with an overflow menu.
|
||||
@@ -88,7 +89,7 @@ const Toolbar = (props: ToolbarProps) => {
|
||||
const styles = props.styleSheet.styles;
|
||||
const mainButtonRow = (
|
||||
<View style={styles.toolbarRow}>
|
||||
{!overflowButtonsVisible ? mainButtons : null }
|
||||
{ mainButtons }
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -101,6 +102,8 @@ const Toolbar = (props: ToolbarProps) => {
|
||||
// container. As such, we can't base the container's width on the
|
||||
// size of its content.
|
||||
width: '100%',
|
||||
|
||||
...props.style,
|
||||
}}
|
||||
onLayout={onContainerLayout}
|
||||
>
|
||||
@@ -111,8 +114,8 @@ const Toolbar = (props: ToolbarProps) => {
|
||||
visible={overflowButtonsVisible}
|
||||
onToggleOverflow={onToggleOverflowVisible}
|
||||
/>
|
||||
{ !overflowButtonsVisible ? mainButtonRow : null }
|
||||
</ScrollView>
|
||||
{ !overflowButtonsVisible ? mainButtonRow : null }
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
@@ -111,6 +111,7 @@ const ToolbarOverflowRows = (props: OverflowPopupProps) => {
|
||||
style={{
|
||||
height: props.buttonGroups.length * buttonSize,
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
onLayout={onContainerLayout}
|
||||
>
|
||||
|
@@ -374,6 +374,7 @@ function NoteEditor(props: Props, ref: any) {
|
||||
...props.contentStyle,
|
||||
}}>
|
||||
<ExtendedWebView
|
||||
webviewInstanceId='NoteEditor'
|
||||
themeId={props.themeId}
|
||||
ref={webviewRef}
|
||||
html={html}
|
||||
|
@@ -41,6 +41,7 @@ const DocumentPicker = require('react-native-document-picker').default;
|
||||
const ImageResizer = require('react-native-image-resizer').default;
|
||||
const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
|
||||
const ImagePicker = require('react-native-image-picker').default;
|
||||
import { ImagePickerResponse } from 'react-native-image-picker';
|
||||
import SelectDateTimeDialog from '../SelectDateTimeDialog';
|
||||
import ShareExtension from '../../utils/ShareExtension.js';
|
||||
import CameraView from '../CameraView';
|
||||
@@ -541,13 +542,14 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
});
|
||||
}
|
||||
|
||||
async pickDocument() {
|
||||
private async pickDocuments() {
|
||||
try {
|
||||
const result = await DocumentPicker.pick();
|
||||
// the result is an array
|
||||
const result = await DocumentPicker.pickMultiple();
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (DocumentPicker.isCancel(error)) {
|
||||
console.info('pickDocument: user has cancelled');
|
||||
console.info('pickDocuments: user has cancelled');
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
@@ -632,16 +634,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pickerResponse.error) {
|
||||
reg.logger().warn('Got error from picker', pickerResponse.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pickerResponse.didCancel) {
|
||||
reg.logger().info('User cancelled picker');
|
||||
return;
|
||||
}
|
||||
|
||||
const localFilePath = Platform.select({
|
||||
android: pickerResponse.uri,
|
||||
ios: decodeURI(pickerResponse.uri),
|
||||
@@ -688,9 +680,9 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
await shim.fsDriver().copy(localFilePath, targetPath);
|
||||
|
||||
const stat = await shim.fsDriver().stat(targetPath);
|
||||
if (stat.size >= 10000000) {
|
||||
if (stat.size >= 200 * 1024 * 1024) {
|
||||
await shim.fsDriver().remove(targetPath);
|
||||
throw new Error('Resources larger than 10 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.');
|
||||
throw new Error('Resources larger than 200 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -735,9 +727,23 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
async attachPhoto_onPress() {
|
||||
const response = await this.showImagePicker({ mediaType: 'photo', noData: true });
|
||||
await this.attachFile(response, 'image');
|
||||
private async attachPhoto_onPress() {
|
||||
// the selection Limit should be specfied. I think 200 is enough?
|
||||
const response: ImagePickerResponse = await this.showImagePicker({ mediaType: 'photo', includeBase64: false, selectionLimit: 200 });
|
||||
|
||||
if (response.errorCode) {
|
||||
reg.logger().warn('Got error from picker', response.errorCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.didCancel) {
|
||||
reg.logger().info('User cancelled picker');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const asset of response.assets) {
|
||||
await this.attachFile(asset, 'image');
|
||||
}
|
||||
}
|
||||
|
||||
takePhoto_onPress() {
|
||||
@@ -748,8 +754,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
void this.attachFile(
|
||||
{
|
||||
uri: data.uri,
|
||||
didCancel: false,
|
||||
error: null,
|
||||
type: 'image/jpg',
|
||||
},
|
||||
'image'
|
||||
@@ -762,9 +766,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ showCamera: false });
|
||||
}
|
||||
|
||||
async attachFile_onPress() {
|
||||
const response = await this.pickDocument();
|
||||
await this.attachFile(response, 'all');
|
||||
private async attachFile_onPress() {
|
||||
const response = await this.pickDocuments();
|
||||
for (const asset of response) {
|
||||
await this.attachFile(asset, 'all');
|
||||
}
|
||||
}
|
||||
|
||||
toggleIsTodo_onPress() {
|
||||
|
@@ -50,6 +50,7 @@ class FolderScreenComponent extends BaseScreenComponent {
|
||||
lastSavedFolder: Object.assign({}, folder),
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
Folder.load(this.props.folderId).then(folder => {
|
||||
this.setState({
|
||||
folder: folder,
|
||||
|
@@ -151,10 +151,12 @@ class NotesScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
deleteFolder_onPress(folderId) {
|
||||
// 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 => {
|
||||
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',
|
||||
@@ -162,6 +164,7 @@ class NotesScreenComponent extends BaseScreenComponent {
|
||||
smartFilterId: 'c3176726992c11e9ac940492261af972',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.catch(error => {
|
||||
alert(error.message);
|
||||
});
|
||||
|
@@ -229,7 +229,7 @@ PODS:
|
||||
- React
|
||||
- react-native-get-random-values (1.7.1):
|
||||
- React-Core
|
||||
- react-native-image-picker (2.3.4):
|
||||
- react-native-image-picker (4.10.0):
|
||||
- React-Core
|
||||
- react-native-image-resizer (1.4.5):
|
||||
- React-Core
|
||||
@@ -533,7 +533,7 @@ SPEC CHECKSUMS:
|
||||
react-native-document-picker: 20f652c2402d3ddc81f396d8167c3bd978add4a2
|
||||
react-native-geolocation: c956aeb136625c23e0dce0467664af2c437888c9
|
||||
react-native-get-random-values: 2c4ff6b44cb71291dabe9a8ae87d3877dcf387da
|
||||
react-native-image-picker: c6d75c4ab2cf46f9289f341242b219cb3c1180d3
|
||||
react-native-image-picker: 4bc9ed38c8be255b515d8c88babbaf74973f91a8
|
||||
react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f
|
||||
react-native-netinfo: 3d3769f0d65de15c83a9bf1346f8be71de5a24bf
|
||||
react-native-rsa-native: 1f6bba06dd02f0e652a66a384c75c270f7a0062f
|
||||
|
@@ -47,7 +47,7 @@
|
||||
"react-native-file-viewer": "^2.1.4",
|
||||
"react-native-fs": "^2.16.6",
|
||||
"react-native-get-random-values": "^1.7.0",
|
||||
"react-native-image-picker": "^2.3.4",
|
||||
"react-native-image-picker": "^4.10.0",
|
||||
"react-native-image-resizer": "^1.3.0",
|
||||
"react-native-modal-datetime-picker": "^9.0.0",
|
||||
"react-native-popup-menu": "^0.15.13",
|
||||
|
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
hash:"ea13a22d0df59339b671f6b5700e2914", files: {
|
||||
hash:"abde4aa0d823e75ca24be9dd0e8c971a", files: {
|
||||
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
|
||||
|
File diff suppressed because one or more lines are too long
@@ -489,6 +489,7 @@ async function initialize(dispatch: Function) {
|
||||
await migrateMasterPassword();
|
||||
|
||||
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
|
||||
reg.logger().info(`Client ID: ${Setting.value('clientId')}`);
|
||||
|
||||
if (Setting.value('firstStart')) {
|
||||
let locale = NativeModules.I18nManager.localeIdentifier;
|
||||
@@ -633,6 +634,7 @@ async function initialize(dispatch: Function) {
|
||||
// start almost immediately to get the latest data.
|
||||
// doWifiConnectionCheck set to true so initial sync
|
||||
// doesn't happen on mobile data
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
void reg.scheduleSync(1000, null, true).then(() => {
|
||||
// Wait for the first sync before updating the notifications, since synchronisation
|
||||
// might change the notifications.
|
||||
|
@@ -39,6 +39,7 @@ export default (dispatch: Function, folderId: string) => {
|
||||
void Note.save({
|
||||
parent_id: folderId,
|
||||
is_todo: isTodo,
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
}, { provisional: true }).then((newNote: any) => {
|
||||
dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -51,6 +52,7 @@ export default (dispatch: Function, folderId: string) => {
|
||||
|
||||
DeviceEventEmitter.addListener('quickActionShortcut', handleQuickAction);
|
||||
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
QuickActions.popInitialAction().then(handleQuickAction).catch((reason: any) => reg.logger().error(reason));
|
||||
};
|
||||
|
||||
|
@@ -41,6 +41,7 @@ class DatabaseDriverReactNative {
|
||||
}
|
||||
|
||||
selectAll(sql, params = null) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
return this.exec(sql, params).then(r => {
|
||||
const output = [];
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"name": "<%= packageName %>",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dist": "webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive",
|
||||
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
@@ -14,17 +14,16 @@
|
||||
"publish"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
"@types/node": "^18.7.13",
|
||||
"chalk": "^4.1.0",
|
||||
"copy-webpack-plugin": "^6.1.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"glob": "^7.1.6",
|
||||
"on-build-webpack": "^0.1.0",
|
||||
"tar": "^6.0.5",
|
||||
"ts-loader": "^7.0.5",
|
||||
"typescript": "^3.9.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"yargs": "^16.2.0"
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"glob": "^8.0.3",
|
||||
"tar": "^6.1.11",
|
||||
"ts-loader": "^9.3.1",
|
||||
"typescript": "^4.8.2",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"@joplin/lib": "~2.9"
|
||||
}
|
||||
}
|
||||
|
@@ -11,10 +11,10 @@ const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const WebpackOnBuildPlugin = require('on-build-webpack');
|
||||
const tar = require('tar');
|
||||
const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
const allPossibleCategories = require('@joplin/lib/pluginCategories.json');
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
@@ -29,12 +29,21 @@ const userConfig = Object.assign({}, {
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const allPossibleCategories = ['appearance', 'developer tools', 'productivity', 'themes', 'integrations', 'viewer', 'search', 'tags', 'editor', 'files', 'personal knowledge management'];
|
||||
const allPossibleScreenshotsType = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
const manifest = readManifest(manifestPath);
|
||||
const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
|
||||
const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`);
|
||||
|
||||
const { builtinModules } = require('node:module');
|
||||
|
||||
// Webpack5 doesn't polyfill by default and displays a warning when attempting to require() built-in
|
||||
// node modules. Set these to false to prevent Webpack from warning about not polyfilling these modules.
|
||||
// We don't need to polyfill because the plugins run in Electron's Node environment.
|
||||
const moduleFallback = {};
|
||||
for (const moduleName of builtinModules) {
|
||||
moduleFallback[moduleName] = false;
|
||||
}
|
||||
|
||||
function validatePackageJson() {
|
||||
const content = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) {
|
||||
@@ -73,7 +82,7 @@ function validateCategories(categories) {
|
||||
if (!categories) return null;
|
||||
if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed');
|
||||
categories.forEach(category => {
|
||||
if (!allPossibleCategories.includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid Categories are: \n${allPossibleCategories}\n`);
|
||||
if (!allPossibleCategories.map(category => { return category.name; }).includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid categories are: \n${allPossibleCategories.map(category => { return category.name; })}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -164,6 +173,7 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
},
|
||||
fallback: moduleFallback,
|
||||
// JSON files can also be required from scripts so we include this.
|
||||
// https://github.com/joplin/plugin-bibtex/pull/2
|
||||
extensions: ['.js', '.tsx', '.ts', '.json'],
|
||||
@@ -198,6 +208,7 @@ const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
},
|
||||
fallback: moduleFallback,
|
||||
extensions: ['.js', '.tsx', '.ts', '.json'],
|
||||
},
|
||||
});
|
||||
@@ -205,11 +216,18 @@ const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
const createArchiveConfig = {
|
||||
stats: 'errors-only',
|
||||
entry: './dist/index.js',
|
||||
resolve: {
|
||||
fallback: moduleFallback,
|
||||
},
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
path: publishDir,
|
||||
},
|
||||
plugins: [new WebpackOnBuildPlugin(onBuildCompleted)],
|
||||
plugins: [{
|
||||
apply(compiler) {
|
||||
compiler.hooks.done.tap('archiveOnBuildListener', onBuildCompleted);
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
function resolveExtraScriptPath(name) {
|
||||
@@ -250,11 +268,8 @@ function buildExtraScriptConfigs(userConfig) {
|
||||
return output;
|
||||
}
|
||||
|
||||
function main(processArgv) {
|
||||
const yargs = require('yargs/yargs');
|
||||
const argv = yargs(processArgv).argv;
|
||||
|
||||
const configName = argv['joplin-plugin-config'];
|
||||
function main(environ) {
|
||||
const configName = environ['joplin-plugin-config'];
|
||||
if (!configName) throw new Error('A config file must be specified via the --joplin-plugin-config flag');
|
||||
|
||||
// Webpack configurations run in parallel, while we need them to run in
|
||||
@@ -292,19 +307,22 @@ function main(processArgv) {
|
||||
return configs[configName];
|
||||
}
|
||||
|
||||
let exportedConfigs = [];
|
||||
|
||||
try {
|
||||
exportedConfigs = main(process.argv);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
module.exports = (env) => {
|
||||
let exportedConfigs = [];
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
try {
|
||||
exportedConfigs = main(env);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return exportedConfigs;
|
||||
};
|
||||
|
@@ -880,6 +880,7 @@ export default class BaseApplication {
|
||||
if (!Setting.value('api.token')) {
|
||||
void EncryptionService.instance()
|
||||
.generateApiToken()
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then((token: string) => {
|
||||
Setting.setValue('api.token', token);
|
||||
});
|
||||
|
@@ -248,6 +248,7 @@ class BaseModel {
|
||||
if (options.where) sql += ` WHERE ${options.where}`;
|
||||
return this.db()
|
||||
.selectOne(sql)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then((r: any) => {
|
||||
return r ? r['total'] : 0;
|
||||
});
|
||||
@@ -335,6 +336,7 @@ class BaseModel {
|
||||
if (params === null) params = [];
|
||||
return this.db()
|
||||
.selectOne(sql, params)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then((model: any) => {
|
||||
return this.filter(this.addModelMd(model));
|
||||
});
|
||||
@@ -344,6 +346,7 @@ class BaseModel {
|
||||
if (params === null) params = [];
|
||||
return this.db()
|
||||
.selectAll(sql, params)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then((models: any[]) => {
|
||||
return this.filterArray(this.addModelMd(models));
|
||||
});
|
||||
|
@@ -292,6 +292,7 @@ export default class JoplinDatabase extends Database {
|
||||
queries.push(this.wrapQuery('DELETE FROM table_fields'));
|
||||
|
||||
return this.selectAll('SELECT name FROM sqlite_master WHERE type="table"')
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then(tableRows => {
|
||||
const chain = [];
|
||||
for (let i = 0; i < tableRows.length; i++) {
|
||||
@@ -303,6 +304,7 @@ export default class JoplinDatabase extends Database {
|
||||
if (tableName === 'notes_spellfix') continue;
|
||||
if (tableName === 'search_aux') continue;
|
||||
chain.push(() => {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
return this.selectAll(`PRAGMA table_info("${tableName}")`).then(pragmas => {
|
||||
for (let i = 0; i < pragmas.length; i++) {
|
||||
const item = pragmas[i];
|
||||
@@ -325,6 +327,7 @@ export default class JoplinDatabase extends Database {
|
||||
|
||||
return promiseChain(chain);
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then(() => {
|
||||
queries.push({ sql: 'UPDATE version SET table_fields_version = ?', params: [newVersion] });
|
||||
return this.transactionExecBatch(queries);
|
||||
|
@@ -231,11 +231,14 @@ class Logger {
|
||||
// when many log operations are being done (eg. during sync in
|
||||
// dev mode).
|
||||
let release: Function = null;
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
writeToFileMutex_.acquire().then((r: Function) => {
|
||||
release = r;
|
||||
return Logger.fsDriver().appendFile(target.path, `${line.join(': ')}\n`, 'utf8');
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
}).catch((error: any) => {
|
||||
console.error('Cannot write to log file:', error);
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
}).finally(() => {
|
||||
if (release) release();
|
||||
});
|
||||
|
@@ -70,9 +70,11 @@ export default class TaskQueue {
|
||||
|
||||
task
|
||||
.callback()
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then((result: any) => {
|
||||
completeTask(task, result, null);
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.catch((error: Error) => {
|
||||
if (!error) error = new Error('Unknown error');
|
||||
completeTask(task, null, error);
|
||||
|
@@ -102,6 +102,7 @@ shared.saveNoteButton_press = async function(comp, folderId = null, options = nu
|
||||
comp.setState(newState);
|
||||
|
||||
if (isProvisionalNote) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
Note.updateGeolocation(note.id).then(geoNote => {
|
||||
const stateNote = comp.state.note;
|
||||
if (!stateNote || !geoNote) return;
|
||||
|
@@ -588,6 +588,7 @@ export default async function importEnex(parentFolderId: string, filePath: strin
|
||||
notes.push(note);
|
||||
|
||||
if (notes.length >= 10) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
processNotes().catch(error => {
|
||||
importOptions.onError(createErrorWithNoteTitle(this, error));
|
||||
});
|
||||
@@ -648,6 +649,7 @@ export default async function importEnex(parentFolderId: string, filePath: strin
|
||||
saxStream.on('end', handleSaxStreamEvent(function() {
|
||||
// Wait till there is no more notes to process.
|
||||
const iid = shim.setInterval(() => {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
void processNotes().then(allDone => {
|
||||
if (allDone) {
|
||||
shim.clearTimeout(iid);
|
||||
|
@@ -55,6 +55,7 @@ export default class Folder extends BaseItem {
|
||||
|
||||
return this.db()
|
||||
.selectAll(`SELECT id FROM notes WHERE ${where.join(' AND ')}`, [parentId])
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then((rows: any[]) => {
|
||||
const output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
@@ -759,6 +760,7 @@ export default class Folder extends BaseItem {
|
||||
|
||||
syncDebugLog.info('Folder Save:', o);
|
||||
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
return super.save(o, options).then((folder: FolderEntity) => {
|
||||
this.dispatch({
|
||||
type: 'FOLDER_UPDATE_ONE',
|
||||
|
@@ -1039,6 +1039,21 @@ class Setting extends BaseModel {
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
// Works around a bug in which additional space is visible beneath the toolbar on some devices.
|
||||
// See https://github.com/laurent22/joplin/pull/6823
|
||||
'editor.mobile.removeSpaceBelowToolbar': {
|
||||
value: false,
|
||||
type: SettingItemType.Bool,
|
||||
section: 'note',
|
||||
public: true,
|
||||
appTypes: [AppType.Mobile],
|
||||
show: (settings: any) => settings['editor.mobile.removeSpaceBelowToolbar'],
|
||||
label: () => 'Remove extra space below the markdown toolbar',
|
||||
description: () => 'Works around bug on some devices where the markdown toolbar does not touch the bottom of the screen.',
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
},
|
||||
|
||||
newTodoFocus: {
|
||||
value: 'title',
|
||||
type: SettingItemType.String,
|
||||
|
@@ -205,6 +205,7 @@ export default class Tag extends BaseItem {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
return super.save(o, options).then((tag: TagEntity) => {
|
||||
if (options.dispatchUpdateAction) {
|
||||
this.dispatch({
|
||||
|
@@ -82,12 +82,14 @@ class OneDriveApiNodeUtils {
|
||||
|
||||
this.api()
|
||||
.execTokenRequest(query.code, `http://localhost:${port.toString()}`)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then(() => {
|
||||
writeResponse(200, _('The application has been authorised - you may now close this browser tab.'));
|
||||
targetConsole.log('');
|
||||
targetConsole.log(_('The application has been successfully authorised.'));
|
||||
waitAndDestroy();
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.catch(error => {
|
||||
writeResponse(400, error.message);
|
||||
targetConsole.log('');
|
||||
|
35
packages/lib/pluginCategories.json
Normal file
35
packages/lib/pluginCategories.json
Normal file
@@ -0,0 +1,35 @@
|
||||
[
|
||||
{
|
||||
"name": "appearance"
|
||||
},
|
||||
{
|
||||
"name": "developer tools"
|
||||
},
|
||||
{
|
||||
"name": "productivity"
|
||||
},
|
||||
{
|
||||
"name": "themes"
|
||||
},
|
||||
{
|
||||
"name": "integrations"
|
||||
},
|
||||
{
|
||||
"name": "viewer"
|
||||
},
|
||||
{
|
||||
"name": "search"
|
||||
},
|
||||
{
|
||||
"name": "tags"
|
||||
},
|
||||
{
|
||||
"name": "editor"
|
||||
},
|
||||
{
|
||||
"name": "files"
|
||||
},
|
||||
{
|
||||
"name": "personal knowledge management"
|
||||
}
|
||||
]
|
@@ -4,6 +4,7 @@ function promiseChain(chain, defaultValue = null) {
|
||||
});
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
const f = chain[i];
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
output = output.then(f);
|
||||
}
|
||||
return output;
|
||||
|
@@ -49,6 +49,7 @@ describe('Registry', function() {
|
||||
|
||||
it('should sync if do wifi check is false', done => {
|
||||
void reg.scheduleSync(1, null, false)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then(() =>{
|
||||
expect(sync.start).toHaveBeenCalled();
|
||||
done();
|
||||
|
@@ -181,11 +181,13 @@ export default class ResourceFetcher extends BaseService {
|
||||
|
||||
fileApi
|
||||
.get(remoteResourceContentPath, { path: localResourceContentPath, target: 'file' })
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then(async () => {
|
||||
await Resource.setLocalState(resource, { fetch_status: Resource.FETCH_STATUS_DONE });
|
||||
this.logger().debug(`ResourceFetcher: Resource downloaded: ${resource.id}`);
|
||||
await completeDownload(true, localResourceContentPath);
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.catch(async (error: any) => {
|
||||
this.logger().error(`ResourceFetcher: Could not download resource: ${resource.id}`, error);
|
||||
await Resource.setLocalState(resource, { fetch_status: Resource.FETCH_STATUS_ERROR, fetch_error: error.message });
|
||||
|
@@ -77,6 +77,7 @@ export default async function populateDatabase(db: any, options: Options = null)
|
||||
let tagBatch = [];
|
||||
for (let i = 0; i < options.tagCount; i++) {
|
||||
const tagTitle = randomElement(wordList); // `tag${i}`
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
tagBatch.push(Tag.save({ title: tagTitle }, { dispatchUpdateAction: false }).then((savedTag: any) => {
|
||||
createdTagIds.push(savedTag.id);
|
||||
if (!options.silent) console.info(`Tags: ${i} / ${options.tagCount}`);
|
||||
@@ -99,6 +100,7 @@ export default async function populateDatabase(db: any, options: Options = null)
|
||||
const parentIndex = randomIndex(createdFolderIds);
|
||||
note.parent_id = createdFolderIds[parentIndex];
|
||||
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
noteBatch.push(Note.save(note, { dispatchUpdateAction: false }).then((savedNote: any) => {
|
||||
createdNoteIds.push(savedNote.id);
|
||||
console.info(`Notes: ${i} / ${options.noteCount}`);
|
||||
|
@@ -36,6 +36,7 @@ export default class JoplinPlugins {
|
||||
// We don't use `await` when calling onStart because the plugin might be awaiting
|
||||
// in that call too (for example, when opening a dialog on startup) so we don't
|
||||
// want to get stuck here.
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
void script.onStart({}).catch((error: any) => {
|
||||
// For some reason, error thrown from the executed script do not have the type "Error"
|
||||
// but are instead plain object. So recreate the Error object here so that it can
|
||||
@@ -43,6 +44,7 @@ export default class JoplinPlugins {
|
||||
const newError: Error = new Error(error.message);
|
||||
newError.stack = error.stack;
|
||||
logger.error(`Uncaught exception in plugin "${this.plugin.id}":`, newError);
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
}).then(() => {
|
||||
logger.info(`Finished running onStart handler: ${this.plugin.id} (Took ${Date.now() - startTime}ms)`);
|
||||
this.plugin.emit('started');
|
||||
|
@@ -3,9 +3,12 @@ import path = require('path');
|
||||
import Setting from '../../../models/Setting';
|
||||
import shim from '../../../shim';
|
||||
import PluginService, { defaultPluginSetting, DefaultPluginsInfo, PluginSettings } from '../PluginService';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import * as React from 'react';
|
||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||
|
||||
const logger = Logger.create('defaultPluginsUtils');
|
||||
|
||||
export function checkPreInstalledDefaultPlugins(defaultPluginsId: string[],pluginSettings: PluginSettings) {
|
||||
const installedDefaultPlugins: Array<string> = Setting.value('installedDefaultPlugins');
|
||||
for (const pluginId of defaultPluginsId) {
|
||||
@@ -15,8 +18,17 @@ export function checkPreInstalledDefaultPlugins(defaultPluginsId: string[],plugi
|
||||
}
|
||||
}
|
||||
|
||||
export async function installDefaultPlugins(service: PluginService, pluginsDir: string, defaultPluginsId: string[], pluginSettings: PluginSettings): Promise<PluginSettings> {
|
||||
const defaultPluginsPaths = await shim.fsDriver().readDirStats(pluginsDir);
|
||||
export async function installDefaultPlugins(service: PluginService, defaultPluginsDir: string, defaultPluginsId: string[], pluginSettings: PluginSettings): Promise<PluginSettings> {
|
||||
if (!await shim.fsDriver().exists(defaultPluginsDir)) {
|
||||
logger.info(`Could not find default plugins' directory: ${defaultPluginsDir} - skipping installation.`);
|
||||
return pluginSettings;
|
||||
}
|
||||
const defaultPluginsPaths = await shim.fsDriver().readDirStats(defaultPluginsDir);
|
||||
if (defaultPluginsPaths.length <= 0) {
|
||||
logger.info(`Default plugins' directory is empty: ${defaultPluginsDir} - skipping installation.`);
|
||||
return pluginSettings;
|
||||
}
|
||||
|
||||
const installedPlugins = Setting.value('installedDefaultPlugins');
|
||||
|
||||
for (let pluginId of defaultPluginsPaths) {
|
||||
@@ -24,7 +36,7 @@ export async function installDefaultPlugins(service: PluginService, pluginsDir:
|
||||
|
||||
// if pluginId is present in 'installedDefaultPlugins' array or it doesn't have default plugin ID, then we won't install it again as default plugin
|
||||
if (installedPlugins.includes(pluginId) || !defaultPluginsId.includes(pluginId)) continue;
|
||||
const defaultPluginPath: string = path.join(pluginsDir, pluginId, 'plugin.jpl');
|
||||
const defaultPluginPath: string = path.join(defaultPluginsDir, pluginId, 'plugin.jpl');
|
||||
await service.installPlugin(defaultPluginPath, false);
|
||||
|
||||
pluginSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||
@@ -41,7 +53,7 @@ export function setSettingsForDefaultPlugins(defaultPluginsInfo: DefaultPluginsI
|
||||
for (const pluginId of Object.keys(defaultPluginsInfo)) {
|
||||
if (!defaultPluginsInfo[pluginId].settings) continue;
|
||||
for (const settingName of Object.keys(defaultPluginsInfo[pluginId].settings)) {
|
||||
if (!installedDefaultPlugins.includes(pluginId)) {
|
||||
if (!installedDefaultPlugins.includes(pluginId) && Setting.keyExists(`plugin-${pluginId}.${settingName}`)) {
|
||||
Setting.setValue(`plugin-${pluginId}.${settingName}`, defaultPluginsInfo[pluginId].settings[settingName]);
|
||||
}
|
||||
}
|
||||
|
@@ -502,7 +502,9 @@ function shimInit(options = null) {
|
||||
const cleanUpOnError = error => {
|
||||
// We ignore any unlink error as we only want to report on the main error
|
||||
fs.unlink(filePath)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.catch(() => {})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then(() => {
|
||||
if (file) {
|
||||
file.close(() => {
|
||||
|
127
packages/pdf-viewer/FullViewer.tsx
Normal file
127
packages/pdf-viewer/FullViewer.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useRef, useState, useCallback } from 'react';
|
||||
import * as React from 'react';
|
||||
import usePdfDocument from './hooks/usePdfDocument';
|
||||
import VerticalPages from './VerticalPages';
|
||||
import MessageService from './messageService';
|
||||
import { DownloadButton, PrintButton, OpenLinkButton, CloseButton } from './ui/IconButtons';
|
||||
import ZoomControls from './ui/ZoomControls';
|
||||
import styled from 'styled-components';
|
||||
import GotoInput from './ui/GotoPage';
|
||||
|
||||
require('./fullScreen.css');
|
||||
|
||||
const TitleWrapper = styled.div`
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
min-width: 10rem;
|
||||
max-width: 18rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--secondary);
|
||||
padding: 0.2rem 0.6rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.2rem;
|
||||
color: var(--primary);
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
|
||||
export interface FullViewerProps {
|
||||
pdfPath: string;
|
||||
isDarkTheme: boolean;
|
||||
messageService: MessageService;
|
||||
startPage: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function FullViewer(props: FullViewerProps) {
|
||||
const pdfDocument = usePdfDocument(props.pdfPath);
|
||||
const [zoom, setZoom] = useState<number>(1);
|
||||
const [startPage, setStartPage] = useState<number>(props.startPage || 1);
|
||||
const [selectedPage, setSelectedPage] = useState<number>(startPage);
|
||||
const mainViewerRef = useRef<HTMLDivElement>(null);
|
||||
const thubmnailRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onActivePageChange = useCallback((pageNo: number) => {
|
||||
setSelectedPage(pageNo);
|
||||
}, []);
|
||||
|
||||
const goToPage = useCallback((pageNo: number) => {
|
||||
if (pageNo < 1 || pageNo > pdfDocument.pageCount || pageNo === selectedPage) return;
|
||||
setSelectedPage(pageNo);
|
||||
setStartPage(pageNo);
|
||||
}, [pdfDocument, selectedPage]);
|
||||
|
||||
if (!pdfDocument) {
|
||||
return (
|
||||
<div className="full-app loading">
|
||||
<div>Loading pdf..</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="full-app">
|
||||
<div className="top-bar">
|
||||
<div>
|
||||
<TitleWrapper>
|
||||
<Title title={props.title}>{props.title}</Title>
|
||||
<div>{selectedPage} of {pdfDocument.pageCount} pages</div>
|
||||
</TitleWrapper>
|
||||
</div>
|
||||
<div>
|
||||
<ZoomControls onChange={setZoom} zoom={zoom} size={1} />
|
||||
<OpenLinkButton onClick={props.messageService.openExternalViewer} size={1.3} />
|
||||
<PrintButton onClick={pdfDocument?.printPdf} size={1.3}/>
|
||||
<DownloadButton onClick={pdfDocument?.downloadPdf} size={1.3}/>
|
||||
<GotoInput onChange={goToPage} size={1.3} pageCount={pdfDocument.pageCount} currentPage={selectedPage} />
|
||||
</div>
|
||||
<div>
|
||||
<CloseButton onClick={props.messageService.close} size={1.3} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="viewers dark-bg">
|
||||
<div className="pane thumbnail-pane" ref={thubmnailRef}>
|
||||
<VerticalPages
|
||||
pdfDocument={pdfDocument}
|
||||
isDarkTheme={true}
|
||||
rememberScroll={false}
|
||||
container={thubmnailRef}
|
||||
pageGap={16}
|
||||
widthPercent={86}
|
||||
showPageNumbers={true}
|
||||
selectedPage={selectedPage}
|
||||
onPageClick={goToPage}
|
||||
textSelectable={false}
|
||||
zoom={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="pane main-pane" ref={mainViewerRef}>
|
||||
<VerticalPages
|
||||
pdfDocument={pdfDocument}
|
||||
isDarkTheme={true}
|
||||
rememberScroll={false}
|
||||
container={mainViewerRef}
|
||||
zoom={zoom}
|
||||
pageGap={5}
|
||||
anchorPage={startPage}
|
||||
onActivePageChange={onActivePageChange}
|
||||
textSelectable={true}
|
||||
onTextSelect={props.messageService.textSelected}
|
||||
showPageNumbers={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,32 +1,35 @@
|
||||
import { useEffect, useRef, useState, MutableRefObject } from 'react';
|
||||
import { useEffect, useRef, useState, MutableRefObject, useCallback } from 'react';
|
||||
import * as React from 'react';
|
||||
import useIsVisible from './hooks/useIsVisible';
|
||||
import useVisibleOnSelect, { VisibleOnSelect } from './hooks/useVisibleOnSelect';
|
||||
import PdfDocument from './PdfDocument';
|
||||
import { ScaledSize } from './types';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { ScaledSize, RenderRequest } from './types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const PageWrapper = styled.div`
|
||||
|
||||
require('./textLayer.css');
|
||||
|
||||
const PageWrapper = styled.div<{ isSelected?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border: solid thin rgba(120, 120, 120, 0.498);
|
||||
border: ${props => props.isSelected ? 'solid 5px #0079ff' : 'solid thin rgba(120, 120, 120, 0.498)'};
|
||||
background: rgb(233, 233, 233);
|
||||
position: relative;
|
||||
border-radius: 0px;
|
||||
border-radius: ${props => props.isSelected ? '0.3rem' : '0px'};
|
||||
`;
|
||||
|
||||
const PageInfo = styled.div`
|
||||
const PageInfo = styled.div<{ isSelected?: boolean }>`
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
padding: 0.3rem;
|
||||
background: rgba(203, 203, 203, 0.509);
|
||||
background: ${props => props.isSelected ? '#0079ff' : 'rgba(203, 203, 203, 0.509)'};
|
||||
border-radius: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
color: rgba(91, 91, 91, 0.829);
|
||||
color: ${props => props.isSelected ? 'white' : 'rgba(91, 91, 91, 0.829)'};
|
||||
backdrop-filter: blur(0.5rem);
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
@@ -43,61 +46,68 @@ export interface PageProps {
|
||||
scaledSize: ScaledSize;
|
||||
isDarkTheme: boolean;
|
||||
container: MutableRefObject<HTMLElement>;
|
||||
showPageNumbers?: boolean;
|
||||
showPageNumbers: boolean;
|
||||
isSelected: boolean;
|
||||
textSelectable: boolean;
|
||||
onTextSelect?: (text: string)=> void;
|
||||
onClick?: (page: number)=> void;
|
||||
onDoubleClick?: (page: number)=> void;
|
||||
}
|
||||
|
||||
|
||||
export default function Page(props: PageProps) {
|
||||
const [error, setError] = useState(null);
|
||||
const [page, setPage] = useState(null);
|
||||
const scaleRef = useRef<number>(null);
|
||||
const timestampRef = useRef<number>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const isVisible = useIsVisible(canvasRef, props.container);
|
||||
const isVisible = useIsVisible(wrapperRef, props.container);
|
||||
useVisibleOnSelect({
|
||||
isVisible,
|
||||
isSelected: props.isSelected,
|
||||
container: props.container,
|
||||
wrapperRef,
|
||||
} as VisibleOnSelect);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible || !page || !props.scaledSize || (scaleRef.current && props.scaledSize.scale === scaleRef.current)) return;
|
||||
try {
|
||||
const viewport = page.getViewport({ scale: props.scaledSize.scale || 1.0 });
|
||||
const canvas = canvasRef.current;
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const pageTimestamp = new Date().getTime();
|
||||
timestampRef.current = pageTimestamp;
|
||||
page.render({
|
||||
canvasContext: ctx,
|
||||
viewport,
|
||||
// Used so that the page rendering is throttled to some extent.
|
||||
// https://stackoverflow.com/questions/18069448/halting-pdf-js-page-rendering
|
||||
continueCallback: function(cont: any) {
|
||||
if (timestampRef.current !== pageTimestamp) {
|
||||
return;
|
||||
}
|
||||
cont();
|
||||
},
|
||||
});
|
||||
const isCancelled = () => props.scaledSize.scale !== scaleRef.current;
|
||||
|
||||
const renderPage = async () => {
|
||||
try {
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.style.height = '100%';
|
||||
canvasRef.current.style.width = '100%';
|
||||
}
|
||||
const renderRequest: RenderRequest = {
|
||||
pageNo: props.pageNo,
|
||||
scaledSize: props.scaledSize,
|
||||
getTextLayer: props.textSelectable,
|
||||
isCancelled,
|
||||
};
|
||||
const { canvas, textLayerDiv } = await props.pdfDocument.renderPage(renderRequest);
|
||||
|
||||
wrapperRef.current.appendChild(canvas);
|
||||
if (textLayerDiv) wrapperRef.current.appendChild(textLayerDiv);
|
||||
|
||||
if (canvasRef.current) canvasRef.current.remove();
|
||||
if (textRef.current) textRef.current.remove();
|
||||
|
||||
canvasRef.current = canvas;
|
||||
if (textLayerDiv) textRef.current = textLayerDiv;
|
||||
} catch (error) {
|
||||
if (isCancelled()) return;
|
||||
error.message = `Error rendering page no. ${props.pageNo}: ${error.message}`;
|
||||
setError(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
if (isVisible && props.scaledSize && (props.scaledSize.scale !== scaleRef.current)) {
|
||||
scaleRef.current = props.scaledSize.scale;
|
||||
|
||||
} catch (error) {
|
||||
error.message = `Error rendering page no. ${props.pageNo}: ${error.message}`;
|
||||
setError(error);
|
||||
throw error;
|
||||
void renderPage();
|
||||
}
|
||||
}, [page, props.scaledSize, isVisible, props.pageNo]);
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
if (page || !isVisible || !props.pdfDocument) return;
|
||||
try {
|
||||
const _page = await props.pdfDocument.getPage(props.pageNo);
|
||||
if (event.cancelled) return;
|
||||
setPage(_page);
|
||||
} catch (error) {
|
||||
console.error('Page load error', props.pageNo, error);
|
||||
setError(error);
|
||||
}
|
||||
}, [page, props.scaledSize, isVisible]);
|
||||
}, [props.scaledSize, isVisible, props.textSelectable, props.pageNo, props.pdfDocument]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.focusOnLoad) {
|
||||
@@ -106,6 +116,11 @@ export default function Page(props: PageProps) {
|
||||
}
|
||||
}, [props.container, props.focusOnLoad]);
|
||||
|
||||
|
||||
const onClick = useCallback(async (_e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (props.onClick) props.onClick(props.pageNo);
|
||||
}, [props.onClick, props.pageNo]);
|
||||
|
||||
let style: any = {};
|
||||
if (props.scaledSize) {
|
||||
style = {
|
||||
@@ -115,15 +130,23 @@ export default function Page(props: PageProps) {
|
||||
};
|
||||
}
|
||||
|
||||
const onContextMenu = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!props.textSelectable || !props.onTextSelect || !window.getSelection()) return;
|
||||
const text = window.getSelection().toString();
|
||||
if (!text) return;
|
||||
props.onTextSelect(text);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, [props.textSelectable, props.onTextSelect]);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
if (props.onDoubleClick) props.onDoubleClick(props.pageNo);
|
||||
}, [props.onDoubleClick, props.pageNo]);
|
||||
|
||||
return (
|
||||
<PageWrapper ref={wrapperRef} style={style}>
|
||||
<canvas ref={canvasRef} className="page-canvas" style={style}>
|
||||
<div>
|
||||
{error ? 'ERROR' : 'Loading..'}
|
||||
</div>
|
||||
Page {props.pageNo}
|
||||
</canvas>
|
||||
{props.showPageNumbers && <PageInfo>{props.isAnchored ? '📌' : ''} Page {props.pageNo}</PageInfo>}
|
||||
<PageWrapper onDoubleClick={onDoubleClick} isSelected={!!props.isSelected} onContextMenu={onContextMenu} onClick={onClick} ref={wrapperRef} style={style}>
|
||||
{ error && <div>Error: {error}</div> }
|
||||
{props.showPageNumbers && <PageInfo isSelected={!!props.isSelected}>{props.isAnchored ? '📌' : ''} Page {props.pageNo}</PageInfo>}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
@@ -1,11 +1,14 @@
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { ScaledSize } from './types';
|
||||
import { ScaledSize, RenderRequest, RenderResult } from './types';
|
||||
import { Mutex, MutexInterface, withTimeout } from 'async-mutex';
|
||||
|
||||
|
||||
export default class PdfDocument {
|
||||
public url: string | Uint8Array;
|
||||
private doc: any = null;
|
||||
public pageCount: number = null;
|
||||
private pages: any = {};
|
||||
private rendererMutex: MutexInterface = null;
|
||||
private pageSize: {
|
||||
height: number;
|
||||
width: number;
|
||||
@@ -14,6 +17,7 @@ export default class PdfDocument {
|
||||
|
||||
public constructor(document: HTMLDocument) {
|
||||
this.document = document;
|
||||
this.rendererMutex = withTimeout(new Mutex(), 40 * 1000);
|
||||
}
|
||||
|
||||
public loadDoc = async (url: string | Uint8Array) => {
|
||||
@@ -74,6 +78,64 @@ export default class PdfDocument {
|
||||
return Math.min(pageNo, this.pageCount);
|
||||
};
|
||||
|
||||
private renderPageImpl = async ({ pageNo, scaledSize, getTextLayer, isCancelled }: RenderRequest): Promise<RenderResult> => {
|
||||
|
||||
const checkCancelled = () => {
|
||||
if (isCancelled()) {
|
||||
throw new Error(`Render cancelled, page: ${pageNo}`);
|
||||
}
|
||||
};
|
||||
|
||||
const page = await this.getPage(pageNo);
|
||||
checkCancelled();
|
||||
|
||||
const canvas = this.document.createElement('canvas');
|
||||
canvas.classList.add('page-canvas');
|
||||
const viewport = page.getViewport({ scale: scaledSize.scale || 1.0 });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Could not get canvas context');
|
||||
}
|
||||
|
||||
await page.render({
|
||||
canvasContext: ctx,
|
||||
viewport,
|
||||
}).promise;
|
||||
checkCancelled();
|
||||
|
||||
let textLayerDiv = null;
|
||||
if (getTextLayer) {
|
||||
textLayerDiv = this.document.createElement('div');
|
||||
textLayerDiv.classList.add('textLayer');
|
||||
const textFragment = this.document.createDocumentFragment();
|
||||
const txtContext = await page.getTextContent();
|
||||
checkCancelled();
|
||||
// Pass the data to the method for rendering of text over the pdf canvas.
|
||||
textLayerDiv.style.height = `${viewport.height}px`;
|
||||
textLayerDiv.style.width = `${viewport.width}px`;
|
||||
await pdfjsLib.renderTextLayer({
|
||||
textContent: txtContext,
|
||||
enhanceTextSelection: true,
|
||||
container: textFragment,
|
||||
viewport: viewport,
|
||||
textDivs: [],
|
||||
}).promise;
|
||||
textLayerDiv.appendChild(textFragment);
|
||||
}
|
||||
|
||||
return { canvas, textLayerDiv };
|
||||
};
|
||||
|
||||
public async renderPage(task: RenderRequest): Promise<RenderResult> {
|
||||
// We're using a render mutex to avoid rendering too many pages at the same time
|
||||
// Which can cause the pdfjs library to abandon some of the in-progress rendering unexpectedly
|
||||
return await this.rendererMutex.runExclusive(async () => {
|
||||
return await this.renderPageImpl(task);
|
||||
});
|
||||
}
|
||||
|
||||
public printPdf = () => {
|
||||
const frame = this.document.createElement('iframe');
|
||||
frame.style.position = 'fixed';
|
||||
|
@@ -12,8 +12,8 @@ const PagesHolder = styled.div<{ pageGap: number }>`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-flow: column;
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
min-height: fit-content;
|
||||
row-gap: ${(props)=> props.pageGap || 2}px;
|
||||
`;
|
||||
@@ -22,13 +22,19 @@ export interface VerticalPagesProps {
|
||||
pdfDocument: PdfDocument;
|
||||
isDarkTheme: boolean;
|
||||
anchorPage?: number;
|
||||
rememberScroll?: boolean;
|
||||
rememberScroll: boolean;
|
||||
pdfId?: string;
|
||||
zoom?: number;
|
||||
zoom: number;
|
||||
container: MutableRefObject<HTMLElement>;
|
||||
pageGap: number;
|
||||
showPageNumbers?: boolean;
|
||||
onActivePageChange: (page: number)=> void;
|
||||
widthPercent?: number;
|
||||
showPageNumbers: boolean;
|
||||
selectedPage?: number;
|
||||
textSelectable: boolean;
|
||||
onTextSelect?: (text: string)=> void;
|
||||
onPageClick?: (page: number)=> void;
|
||||
onActivePageChange?: (page: number)=> void;
|
||||
onDoubleClick?: (page: number)=> void;
|
||||
}
|
||||
|
||||
export default function VerticalPages(props: VerticalPagesProps) {
|
||||
@@ -63,7 +69,8 @@ export default function VerticalPages(props: VerticalPagesProps) {
|
||||
|
||||
const updateWidth = () => {
|
||||
if (cancelled) return;
|
||||
setContainerWidth(props.container.current.clientWidth);
|
||||
const factor = (props.widthPercent || 100) / 100;
|
||||
setContainerWidth(props.container.current.clientWidth * factor);
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
@@ -85,7 +92,7 @@ export default function VerticalPages(props: VerticalPagesProps) {
|
||||
resizeTimer = null;
|
||||
}
|
||||
};
|
||||
}, [props.container, props.pdfDocument]);
|
||||
}, [props.container, props.pdfDocument, props.widthPercent]);
|
||||
|
||||
return (<PagesHolder pageGap={props.pageGap || 2} ref={innerContainerEl} >
|
||||
{scaledSize ?
|
||||
@@ -94,6 +101,11 @@ export default function VerticalPages(props: VerticalPagesProps) {
|
||||
return <Page pdfDocument={props.pdfDocument} pageNo={i + 1} focusOnLoad={scaledSize && props.anchorPage && props.anchorPage === i + 1}
|
||||
isAnchored={props.anchorPage && props.anchorPage === i + 1}
|
||||
showPageNumbers={props.showPageNumbers}
|
||||
isSelected={scaledSize && props.selectedPage && props.selectedPage === i + 1}
|
||||
onClick={props.onPageClick}
|
||||
textSelectable={props.textSelectable}
|
||||
onTextSelect={props.onTextSelect}
|
||||
onDoubleClick={props.onDoubleClick}
|
||||
isDarkTheme={props.isDarkTheme} scaledSize={scaledSize} container={props.container} key={i} />;
|
||||
}
|
||||
) : 'Calculating size...'
|
||||
|
@@ -84,4 +84,3 @@ hr {
|
||||
[data-theme="dark"] .dark-bg {
|
||||
background-color: rgb(54, 54, 54);
|
||||
}
|
||||
|
||||
|
64
packages/pdf-viewer/fullScreen.css
Normal file
64
packages/pdf-viewer/fullScreen.css
Normal file
@@ -0,0 +1,64 @@
|
||||
.full-app{
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: 2.8rem auto;
|
||||
}
|
||||
|
||||
.viewers{
|
||||
width: 100%;
|
||||
height: calc(100vh - 2.8rem);
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 11rem auto;
|
||||
padding-right: 0.4rem;
|
||||
}
|
||||
|
||||
.pane{
|
||||
display: block;
|
||||
margin: 0px auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0rem 0.2rem;
|
||||
position: relative;
|
||||
}
|
||||
.thumbnail-pane{
|
||||
background-color: var(--tertiary);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.top-bar{
|
||||
display: grid;
|
||||
grid-template-columns: 18rem auto 3rem;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
background-color: var(--tertiary);
|
||||
flex-direction: row;
|
||||
}
|
||||
.top-bar > div{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 0.2rem;
|
||||
flex-direction: row;
|
||||
column-gap: 0.6rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 30rem;
|
||||
margin: 0rem auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
.viewers{
|
||||
grid-template-columns: 9rem auto;
|
||||
}
|
||||
.top-bar{
|
||||
grid-template-columns: 10rem auto 3rem;
|
||||
}
|
||||
}
|
@@ -1,21 +1,37 @@
|
||||
import { useEffect, useState, MutableRefObject } from 'react';
|
||||
import { useEffect, useState, MutableRefObject, useRef } from 'react';
|
||||
|
||||
|
||||
const useIsVisible = (elementRef: MutableRefObject<HTMLElement>, rootRef: MutableRefObject<HTMLElement>) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const lastVisible = useRef(0);
|
||||
const invisibleOn = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
let observer: IntersectionObserver = null;
|
||||
let timeout: number = null;
|
||||
if (elementRef.current) {
|
||||
observer = new IntersectionObserver((entries, _observer) => {
|
||||
let visible = false;
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
visible = true;
|
||||
setIsVisible(true);
|
||||
lastVisible.current = Date.now();
|
||||
if ((invisibleOn.current - lastVisible.current) > 300) {
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
if (!timeout) {
|
||||
timeout = window.setTimeout(() => {
|
||||
if (invisibleOn.current < lastVisible.current) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
timeout = null;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!visible) {
|
||||
invisibleOn.current = Date.now();
|
||||
setIsVisible(false);
|
||||
}
|
||||
}, {
|
||||
@@ -29,6 +45,10 @@ const useIsVisible = (elementRef: MutableRefObject<HTMLElement>, rootRef: Mutabl
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
if (timeout) {
|
||||
window.clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
};
|
||||
}, [elementRef, rootRef]);
|
||||
|
||||
|
27
packages/pdf-viewer/hooks/useVisibleOnSelect.ts
Normal file
27
packages/pdf-viewer/hooks/useVisibleOnSelect.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useRef, useEffect, MutableRefObject } from 'react';
|
||||
|
||||
export interface VisibleOnSelect {
|
||||
container: MutableRefObject<HTMLElement>;
|
||||
wrapperRef: MutableRefObject<HTMLElement>;
|
||||
isVisible: boolean;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
// Used in thumbnail view, to scroll to the newly selected page.
|
||||
|
||||
const useVisibleOnSelect = ({ container, wrapperRef, isVisible, isSelected }: VisibleOnSelect) => {
|
||||
const isVisibleRef = useRef(isVisible);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && !isVisibleRef.current) {
|
||||
container.current.scrollTop = wrapperRef.current.offsetTop;
|
||||
}
|
||||
}, [isSelected, isVisibleRef, container, wrapperRef]);
|
||||
|
||||
useEffect(() => {
|
||||
isVisibleRef.current = isVisible;
|
||||
} , [isVisible]);
|
||||
|
||||
};
|
||||
|
||||
export default useVisibleOnSelect;
|
@@ -4,6 +4,8 @@ shim.setReact(React);
|
||||
import { render } from 'react-dom';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import MiniViewerApp from './miniViewer';
|
||||
import MessageService from './messageService';
|
||||
import FullViewer from './FullViewer';
|
||||
|
||||
require('./common.css');
|
||||
|
||||
@@ -15,15 +17,27 @@ const type = window.frameElement.getAttribute('x-type');
|
||||
const appearance = window.frameElement.getAttribute('x-appearance');
|
||||
const anchorPage = Number(window.frameElement.getAttribute('x-anchorPage')) || null;
|
||||
const pdfId = window.frameElement.getAttribute('id');
|
||||
const resourceId = window.frameElement.getAttribute('x-resourceid');
|
||||
const title = window.frameElement.getAttribute('x-title');
|
||||
|
||||
document.documentElement.setAttribute('data-theme', appearance);
|
||||
|
||||
const messageService = new MessageService(type);
|
||||
|
||||
function App() {
|
||||
if (type === 'mini') {
|
||||
return <MiniViewerApp pdfPath={url}
|
||||
isDarkTheme={appearance === 'dark'}
|
||||
anchorPage={anchorPage}
|
||||
pdfId={pdfId} />;
|
||||
pdfId={pdfId}
|
||||
resourceId={resourceId}
|
||||
messageService={messageService}/>;
|
||||
} else if (type === 'full') {
|
||||
return <FullViewer pdfPath={url}
|
||||
isDarkTheme={appearance === 'dark'}
|
||||
startPage={anchorPage || 1}
|
||||
title={title}
|
||||
messageService={messageService} />;
|
||||
}
|
||||
return <div>Error: Unknown app type "{type}"</div>;
|
||||
}
|
||||
|
35
packages/pdf-viewer/messageService.ts
Normal file
35
packages/pdf-viewer/messageService.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export default class MessageService {
|
||||
private viewerType: string;
|
||||
public constructor(type: string) {
|
||||
this.viewerType = type;
|
||||
}
|
||||
private sendMessage = (name: string, data?: any) => {
|
||||
if (this.viewerType === 'full') {
|
||||
const message = {
|
||||
name,
|
||||
...data,
|
||||
};
|
||||
window.postMessage(message, '*');
|
||||
} else if (this.viewerType === 'mini') {
|
||||
const message = {
|
||||
name,
|
||||
data,
|
||||
target: 'webview',
|
||||
};
|
||||
window.parent.postMessage(message, '*');
|
||||
}
|
||||
};
|
||||
public textSelected = (text: string) => {
|
||||
this.sendMessage('textSelected', { text });
|
||||
};
|
||||
public close = () => {
|
||||
this.sendMessage('close');
|
||||
};
|
||||
public openExternalViewer = () => {
|
||||
this.sendMessage('externalViewer');
|
||||
};
|
||||
|
||||
public openFullScreenViewer = (resourceId: string, pageNo: number) => {
|
||||
this.sendMessage('openPdfViewer', { resourceId, pageNo });
|
||||
};
|
||||
}
|
@@ -3,6 +3,7 @@ import useIsFocused from './hooks/useIsFocused';
|
||||
import usePdfDocument from './hooks/usePdfDocument';
|
||||
import VerticalPages from './VerticalPages';
|
||||
import ZoomControls from './ui/ZoomControls';
|
||||
import MessageService from './messageService';
|
||||
import { DownloadButton, PrintButton } from './ui/IconButtons';
|
||||
|
||||
require('./miniViewer.css');
|
||||
@@ -12,6 +13,8 @@ export interface MiniViewerAppProps {
|
||||
isDarkTheme: boolean;
|
||||
anchorPage: number;
|
||||
pdfId: string;
|
||||
resourceId?: string;
|
||||
messageService: MessageService;
|
||||
}
|
||||
|
||||
export default function MiniViewerApp(props: MiniViewerAppProps) {
|
||||
@@ -25,6 +28,10 @@ export default function MiniViewerApp(props: MiniViewerAppProps) {
|
||||
setActivePage(page);
|
||||
}, []);
|
||||
|
||||
const onDoubleClick = useCallback((pageNo: number) => {
|
||||
props.messageService.openFullScreenViewer(props.resourceId, pageNo);
|
||||
}, [props.messageService, props.resourceId]);
|
||||
|
||||
if (!pdfDocument) {
|
||||
return (
|
||||
<div className="mini-app loading">
|
||||
@@ -44,6 +51,9 @@ export default function MiniViewerApp(props: MiniViewerAppProps) {
|
||||
container={containerEl}
|
||||
showPageNumbers={true}
|
||||
zoom={zoom}
|
||||
textSelectable={true}
|
||||
onTextSelect={props.messageService.textSelected}
|
||||
onDoubleClick={onDoubleClick}
|
||||
pageGap={2}
|
||||
onActivePageChange={onActivePageChange} />
|
||||
</div>
|
||||
|
@@ -40,6 +40,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@joplin/lib": "workspace:^",
|
||||
"async-mutex": "^0.4.0",
|
||||
"pdfjs-dist": "^2.14.305",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.9.0",
|
||||
|
91
packages/pdf-viewer/textLayer.css
Normal file
91
packages/pdf-viewer/textLayer.css
Normal file
@@ -0,0 +1,91 @@
|
||||
/* Copyright 2014 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.textLayer {
|
||||
position: absolute;
|
||||
text-align: initial;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
.textLayer span,
|
||||
.textLayer br {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
cursor: text;
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
|
||||
/* Only necessary in Google Chrome, see issue 14205, and most unfortunately
|
||||
* the problem doesn't show up in "text" reference tests. */
|
||||
.textLayer span.markedContent {
|
||||
top: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.textLayer .highlight {
|
||||
margin: -1px;
|
||||
padding: 1px;
|
||||
background-color: rgba(180, 0, 170, 1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.textLayer .highlight.appended {
|
||||
position: initial;
|
||||
}
|
||||
|
||||
.textLayer .highlight.begin {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.textLayer .highlight.end {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.textLayer .highlight.middle {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.textLayer .highlight.selected {
|
||||
background-color: rgba(0, 100, 0, 1);
|
||||
}
|
||||
|
||||
/* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
|
||||
.textLayer br::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.textLayer .endOfContent {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.textLayer .endOfContent.active {
|
||||
top: 0;
|
||||
}
|
||||
|
@@ -9,3 +9,15 @@ export interface IconButtonProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface RenderRequest {
|
||||
pageNo: number;
|
||||
scaledSize: ScaledSize;
|
||||
getTextLayer: boolean;
|
||||
isCancelled: ()=> boolean;
|
||||
}
|
||||
|
||||
export interface RenderResult {
|
||||
canvas: HTMLCanvasElement;
|
||||
textLayerDiv: HTMLDivElement;
|
||||
}
|
||||
|
67
packages/pdf-viewer/ui/GotoPage.tsx
Normal file
67
packages/pdf-viewer/ui/GotoPage.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAngleRight, faAngleLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const Group = styled.div<{ size: number }>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-flow: row;
|
||||
color: var(--grey);
|
||||
cursor: initial;
|
||||
font-size: ${props => props.size}rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
svg:hover {
|
||||
color: var(--secondary);
|
||||
}
|
||||
`;
|
||||
|
||||
const GoToInput = styled.input`
|
||||
ont-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
max-width: 3rem;
|
||||
padding: 0.4rem 0.3rem;
|
||||
background: var(--bg);
|
||||
border: solid 2px transparent;
|
||||
border-radius: 3.5rem;
|
||||
color: var(--tertialry);
|
||||
text-align: center;
|
||||
margin: auto 0.6rem;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: solid 2px var(--blue);
|
||||
}
|
||||
`;
|
||||
|
||||
export interface GotoInputProps {
|
||||
onChange: (pageNo: number)=> void;
|
||||
size?: number;
|
||||
pageCount: number;
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
export default function GotoInput(props: GotoInputProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const inputFocus = useCallback(() => {
|
||||
inputRef.current?.select();
|
||||
} , []);
|
||||
|
||||
const onPageNoInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value.length <= 0) return;
|
||||
const pageNo = parseInt(e.target.value, 10);
|
||||
if (pageNo < 1 || pageNo > props.pageCount || pageNo === props.currentPage) return;
|
||||
props.onChange(pageNo);
|
||||
}, [props.onChange, props.pageCount, props.currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current.value = props.currentPage.toString();
|
||||
} , [props.currentPage]);
|
||||
|
||||
return (<Group size={props.size || 1}>
|
||||
<FontAwesomeIcon icon={faAngleLeft} title="Previous Page" style={{ cursor: 'pointer' }} onClick={() => props.onChange(props.currentPage - 1)} />
|
||||
<GoToInput onChange={onPageNoInput} placeholder="Page" ref={inputRef} onFocus={inputFocus} />
|
||||
<FontAwesomeIcon icon={faAngleRight} title="Next Page" style={{ cursor: 'pointer' }} onClick={() => props.onChange(props.currentPage + 1)} />
|
||||
</Group>);
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPrint, faDownload, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPrint, faDownload, faSquareArrowUpRight, faXmark, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||
import { IconButtonProps } from '../types';
|
||||
|
||||
|
||||
@@ -43,6 +43,18 @@ function BaseButton({ onClick, icon, name, size, color, hoverColor }: BaseButton
|
||||
);
|
||||
}
|
||||
|
||||
export function OpenLinkButton({ onClick, size, color }: IconButtonProps) {
|
||||
return (
|
||||
<BaseButton onClick={onClick} icon={faSquareArrowUpRight} name='Open in another app' size={size} color={color} />
|
||||
);
|
||||
}
|
||||
|
||||
export function CloseButton({ onClick, size, color }: IconButtonProps) {
|
||||
return (
|
||||
<BaseButton onClick={onClick} icon={faXmark} name='Close' size={size} color={color} hoverColor={'red'} />
|
||||
);
|
||||
}
|
||||
|
||||
export function DownloadButton({ onClick, size, color }: IconButtonProps) {
|
||||
return (
|
||||
<BaseButton onClick={onClick} icon={faDownload} name='Download' size={size} color={color} />
|
||||
|
@@ -69,7 +69,7 @@ export default function(link: Link, options: Options, linkIndexes: LinkIndexes)
|
||||
|
||||
return `<iframe src="${src}" x-url="${escapedResourcePath}"
|
||||
x-appearance="${options.theme.appearance}" ${anchorPageNo ? `x-anchorPage="${anchorPageNo}"` : ''} id="${id}"
|
||||
x-type="mini"
|
||||
x-type="mini" x-resourceid="${resourceId}"
|
||||
class="media-player media-pdf"></iframe>`;
|
||||
}
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@@ -46,7 +46,7 @@
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"markdown-it-toc-done-right": "^4.1.0",
|
||||
"md5": "^2.2.1",
|
||||
"mermaid": "^8.13.9"
|
||||
"mermaid": "^9.1.7"
|
||||
},
|
||||
"gitHead": "eb4b0e64eab40a51b0895d3a40a9d8c3cb7b1b14"
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.9.0",
|
||||
"version": "2.9.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "yarn run build && JOPLIN_IS_TESTING=1 nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
|
||||
|
@@ -1,19 +1,24 @@
|
||||
import markdownUtils from '@joplin/lib/markdownUtils';
|
||||
import config from '../../config';
|
||||
import { EmailSubjectBody } from '../../models/EmailModel';
|
||||
import { stripePortalUrl } from '../../utils/urlUtils';
|
||||
import { stripePortalUrl, helpUrl } from '../../utils/urlUtils';
|
||||
|
||||
export default (): EmailSubjectBody => {
|
||||
return {
|
||||
subject: `Your ${config().appName} payment could not be processed`,
|
||||
body: `
|
||||
|
||||
Your last ${config().appName} payment could not be processed. As a result your account has disabled.
|
||||
Your last ${config().appName} payment could not be processed. As a result your account has been disabled.
|
||||
|
||||
To re-activate your account, please update your payment details, or contact us for more details.
|
||||
To re-activate your account, please update your payment details.
|
||||
|
||||
[Manage your subscription](${markdownUtils.escapeLinkUrl(stripePortalUrl())})
|
||||
Following this link to [manage your subscription](${markdownUtils.escapeLinkUrl(stripePortalUrl())}).
|
||||
|
||||
For more information please see the [help page](${helpUrl()}).
|
||||
|
||||
Thank you,
|
||||
|
||||
Joplin Cloud Team
|
||||
`.trim(),
|
||||
};
|
||||
};
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import markdownUtils from '@joplin/lib/markdownUtils';
|
||||
import config from '../../config';
|
||||
import { EmailSubjectBody } from '../../models/EmailModel';
|
||||
import { stripePortalUrl } from '../../utils/urlUtils';
|
||||
import { stripePortalUrl, helpUrl } from '../../utils/urlUtils';
|
||||
|
||||
export default function(): EmailSubjectBody {
|
||||
return {
|
||||
@@ -13,7 +13,7 @@ We were not able to process your last payment. Please follow this URL to update
|
||||
|
||||
[Manage your subscription](${markdownUtils.escapeLinkUrl(stripePortalUrl())})
|
||||
|
||||
Please answer this email if you have any question.
|
||||
For more information please see the [help page](${helpUrl()}).
|
||||
|
||||
Thank you,
|
||||
|
||||
|
@@ -71,6 +71,7 @@ async function main() {
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error');
|
||||
console.error(error);
|
||||
|
@@ -87,6 +87,7 @@ async function start(): Promise<void> {
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
start().catch((error) => {
|
||||
console.error('Fatal error');
|
||||
console.error(error);
|
||||
|
@@ -48,6 +48,7 @@ const main = async () => {
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
|
@@ -7,6 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: 2022-10-06 19:20+0300\n"
|
||||
"Last-Translator: mrkaato0\n"
|
||||
"Language-Team: \n"
|
||||
"Language: fi_FI\n"
|
||||
@@ -14,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
"X-Generator: Poedit 3.1.1\n"
|
||||
|
||||
#: packages/app-mobile/components/screens/ConfigScreen.tsx:560
|
||||
msgid "- Camera: to allow taking a picture and attaching it to a note."
|
||||
@@ -29,7 +31,7 @@ msgid ""
|
||||
"- Storage: to allow attaching files to notes and to enable filesystem "
|
||||
"synchronisation."
|
||||
msgstr ""
|
||||
"- Tallennustila: sallia tiedostojen liittäminen muistiinpanoihin ja ottaa "
|
||||
"- Tallennustila: sallia tiedostojen liittämisen muistiinpanoihin ja ottaa "
|
||||
"tiedostojärjestelmän synkronointi käyttöön."
|
||||
|
||||
#: packages/lib/services/KeymapService.ts:314
|
||||
@@ -296,9 +298,8 @@ msgid "Also displays unset and hidden config variables."
|
||||
msgstr "Näyttää myös merkitsemättömät ja piilotetut konfigurointimuuttujat."
|
||||
|
||||
#: packages/app-desktop/gui/ShareNoteDialog.tsx:203
|
||||
#, fuzzy
|
||||
msgid "Also publish linked notes"
|
||||
msgstr "Peruuta muistiinpanon julkaisu"
|
||||
msgstr "Julkaise myös linkitetyt muistiinpanot"
|
||||
|
||||
#: packages/lib/models/Setting.ts:713
|
||||
msgid "Always"
|
||||
@@ -345,7 +346,7 @@ msgstr "Liitä valokuva"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note.tsx:892
|
||||
msgid "Attach..."
|
||||
msgstr "Liittää..."
|
||||
msgstr "Liitä..."
|
||||
|
||||
#: packages/app-cli/app/command-attach.js:13
|
||||
msgid "Attaches the given file to the note."
|
||||
@@ -866,18 +867,16 @@ msgstr ""
|
||||
"uudelleen, kun olet muodostanut yhteyden Internetiin."
|
||||
|
||||
#: packages/app-desktop/gui/PromptDialog.min.js:235
|
||||
#, fuzzy
|
||||
msgid "Create"
|
||||
msgstr "Luotu"
|
||||
msgstr "Luo"
|
||||
|
||||
#: packages/app-mobile/components/note-list.js:101
|
||||
msgid "Create a notebook"
|
||||
msgstr "Luo muistikirja"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/addProfile.ts:9
|
||||
#, fuzzy
|
||||
msgid "Create new profile..."
|
||||
msgstr "Luo uuden muistiinpanon."
|
||||
msgstr "Luo uusi profiili..."
|
||||
|
||||
#: packages/app-desktop/gui/EditFolderDialog/Dialog.tsx:161
|
||||
msgid "Create notebook"
|
||||
@@ -1325,7 +1324,7 @@ msgstr "Muokkaa muistikirjaa"
|
||||
|
||||
#: packages/app-desktop/commands/editProfileConfig.ts:9
|
||||
msgid "Edit profile configuration..."
|
||||
msgstr ""
|
||||
msgstr "Muokkaa profiilin asetuksia..."
|
||||
|
||||
#: packages/app-desktop/gui/NoteContentPropertiesDialog.tsx:138
|
||||
#: packages/lib/models/Setting.ts:876 packages/lib/models/Setting.ts:877
|
||||
@@ -1355,9 +1354,8 @@ msgstr "Editorin monospace fonttiperhe"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:100
|
||||
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:96
|
||||
#, fuzzy
|
||||
msgid "Editor: %s"
|
||||
msgstr "Editoija"
|
||||
msgstr "Editori: %s"
|
||||
|
||||
#: packages/app-cli/app/command-ls.js:31
|
||||
msgid "Either \"text\" or \"json\""
|
||||
@@ -1671,9 +1669,8 @@ msgid "File system"
|
||||
msgstr "Tiedostojärjestelmä"
|
||||
|
||||
#: packages/app-mobile/components/screens/NoteTagsDialog.js:190
|
||||
#, fuzzy
|
||||
msgid "Filter tags"
|
||||
msgstr "Uudet tunnisteet:"
|
||||
msgstr "Suodata tunnisteet"
|
||||
|
||||
#: packages/app-desktop/gui/ExtensionBadge.min.js:10
|
||||
msgid "Firefox Extension"
|
||||
@@ -2040,7 +2037,7 @@ msgstr "Virheellinen asetusarvo: \"%s\". Mahdolliset arvot ovat: %s."
|
||||
|
||||
#: packages/app-cli/app/command-e2ee.ts:46
|
||||
msgid "Invalid password"
|
||||
msgstr "Väärä salasana"
|
||||
msgstr "Virheellinen salasana"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:38
|
||||
msgid "Italic"
|
||||
@@ -2287,7 +2284,7 @@ msgstr "Pääsalasanan hallinta"
|
||||
|
||||
#: packages/lib/commands/openMasterPasswordDialog.ts:6
|
||||
msgid "Manage master password..."
|
||||
msgstr "Hallitse pääsalasanaa..."
|
||||
msgstr "Pääsalasanan hallinta..."
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx:314
|
||||
msgid "Manage your plugins"
|
||||
@@ -2563,7 +2560,7 @@ msgstr "Huomautus on tallennettu."
|
||||
#: packages/app-desktop/gui/NotePropertiesDialog.min.js:34
|
||||
#: packages/lib/models/Setting.ts:2303
|
||||
msgid "Note History"
|
||||
msgstr "Muistiinpano historia"
|
||||
msgstr "Muistiinpanohistoria"
|
||||
|
||||
#: packages/app-cli/app/command-done.js:21
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
@@ -2793,6 +2790,7 @@ msgid ""
|
||||
"Please click on \"%s\" to proceed, or set the passwords in the \"%s\" list "
|
||||
"below."
|
||||
msgstr ""
|
||||
"Jatka napsauttamalla \"%s\" tai aseta salasana oheiseen \"%s\" luetteloon."
|
||||
|
||||
#: packages/lib/components/EncryptionConfigScreen/utils.ts:65
|
||||
msgid ""
|
||||
@@ -2952,9 +2950,8 @@ msgid "Profile"
|
||||
msgstr "Profiili"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/addProfile.ts:17
|
||||
#, fuzzy
|
||||
msgid "Profile name:"
|
||||
msgstr "Profiili"
|
||||
msgstr "Profiilin nimi:"
|
||||
|
||||
#: packages/lib/versionInfo.ts:26
|
||||
msgid "Profile Version: %s"
|
||||
@@ -3181,9 +3178,8 @@ msgstr "Tallenna hälytys"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:100
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:93
|
||||
#, fuzzy
|
||||
msgid "Save as %s"
|
||||
msgstr "Tallenna nimellä..."
|
||||
msgstr "Tallenna nimellä %s"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:80
|
||||
msgid "Save as..."
|
||||
@@ -3553,9 +3549,8 @@ msgid "Switch between note and to-do type"
|
||||
msgstr "Vaihda muistiinpanon ja tehtävätyypin välillä"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.tsx:437
|
||||
#, fuzzy
|
||||
msgid "Switch profile"
|
||||
msgstr "Vie profiili"
|
||||
msgstr "Vaihda profiili"
|
||||
|
||||
#: packages/app-desktop/gui/utils/NoteListUtils.ts:105
|
||||
msgid "Switch to note type"
|
||||
@@ -3564,9 +3559,8 @@ msgstr "Vaihda muistiinpanotyyppiin"
|
||||
#: packages/app-desktop/commands/switchProfile1.ts:7
|
||||
#: packages/app-desktop/commands/switchProfile2.ts:7
|
||||
#: packages/app-desktop/commands/switchProfile3.ts:7
|
||||
#, fuzzy
|
||||
msgid "Switch to profile %d"
|
||||
msgstr "Vaihda muistiinpanotyyppiin"
|
||||
msgstr "Vaihda profiiliin %d"
|
||||
|
||||
#: packages/app-desktop/gui/utils/NoteListUtils.ts:114
|
||||
msgid "Switch to to-do type"
|
||||
@@ -3662,7 +3656,7 @@ msgstr "Tabloid"
|
||||
|
||||
#: packages/app-mobile/components/screens/NoteTagsDialog.js:179
|
||||
msgid "tag1, tag2, ..."
|
||||
msgstr ""
|
||||
msgstr "tunniste1, tunniste2, ..."
|
||||
|
||||
#: packages/app-cli/app/command-import.js:52
|
||||
#: packages/app-desktop/gui/ImportScreen.min.js:73
|
||||
@@ -3703,7 +3697,7 @@ msgstr ""
|
||||
#: packages/app-desktop/app.ts:332
|
||||
msgid ""
|
||||
"The application did not close properly. Would you like to start in safe mode?"
|
||||
msgstr ""
|
||||
msgstr "Sovellus ei sulkeutunut kunnolla. Haluatko aloittaa vikasietotilassa?"
|
||||
|
||||
#: packages/lib/onedrive-api-node-utils.js:86
|
||||
msgid ""
|
||||
@@ -4545,9 +4539,8 @@ msgid "Your data is going to be re-encrypted and synced again."
|
||||
msgstr "Tietosi salataan ja synkronoidaan uudelleen."
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:271
|
||||
#, fuzzy
|
||||
msgid "Your password is needed to decrypt some of your data."
|
||||
msgstr "Pääsalasanaa tarvitaan joidenkin tietojen salauksen purkamiseen."
|
||||
msgstr "Salasanaasi tarvitaan joidenkin tietojen salauksen purkamiseen."
|
||||
|
||||
#: packages/app-cli/app/command-sync.ts:242
|
||||
msgid ""
|
||||
@@ -4574,10 +4567,6 @@ msgstr "Lähennä"
|
||||
msgid "Zoom Out"
|
||||
msgstr "Loitonna"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Save as SVG"
|
||||
#~ msgstr "Tallenna nimellä..."
|
||||
|
||||
#~ msgid "Please click on \"%s\" to proceed"
|
||||
#~ msgstr "Napsauta \"%s\" jatkaaksesi"
|
||||
|
||||
|
@@ -12,6 +12,7 @@ async function main() {
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error');
|
||||
console.error(error);
|
||||
|
@@ -1,5 +1,17 @@
|
||||
# Joplin Android app changelog
|
||||
|
||||
## [android-v2.9.3](https://github.com/laurent22/joplin/releases/tag/android-v2.9.3) (Pre-release) - 2022-10-07T11:12:56Z
|
||||
|
||||
- Improved: Convert empty bolded regions to bold-italic regions in beta editor (#6807) (#6808 by Henry Heino)
|
||||
- Improved: Increase the attachment size limit to 200MB (#6848 by Self Not Found)
|
||||
- Improved: Show client ID in log (#6897 by Self Not Found)
|
||||
- Improved: Supports attaching multiple files to a note at once (#6831 by Self Not Found)
|
||||
- Improved: Update Mermaid 8.13.9 to 9.1.7 (#6849 by Helmut K. C. Tessarek)
|
||||
- Fixed: Double/triple-tap selection doesn't show context menu (#6803) (#6802 by Henry Heino)
|
||||
- Fixed: Fix multiple webview instances (#6841 by Henry Heino)
|
||||
- Fixed: Fix resources sync when proxy is set (#6817) (#6688 by Self Not Found)
|
||||
- Fixed: Fixed crash when trying to move note to notebook (#6898)
|
||||
|
||||
## [android-v2.9.2](https://github.com/laurent22/joplin/releases/tag/android-v2.9.2) (Pre-release) - 2022-09-01T11:14:58Z
|
||||
|
||||
- New: Add Markdown toolbar (#6753 by Henry Heino)
|
||||
|
@@ -1,5 +1,21 @@
|
||||
# Joplin Server Changelog
|
||||
|
||||
## [server-v2.9.1](https://github.com/laurent22/joplin/releases/tag/server-v2.9.1) - 2022-10-07T10:46:43Z
|
||||
|
||||
- New: Add support for sidebar in user pages (053dbab)
|
||||
- Improved: Automatically delete expired sessions (d5dfecc)
|
||||
- Improved: Cannot sort user deletions by email (8e18024)
|
||||
- Improved: Do not make checkboxes in published notes clickable (cb637e8)
|
||||
- Improved: Improve admin email UI (e3c9bcb)
|
||||
- Improved: Process user deletions once an hour (f99b8df)
|
||||
- Improved: Use apt to install tini to enable multi-platform support (#6097 by Erik Thomsen)
|
||||
- Fixed: Could not manually start task (#6491)
|
||||
- Fixed: Fixed Unsupported File Type error when sharing certain notes (#6531)
|
||||
- Fixed: Fixed removal of user deletion tasks (8f8cc12)
|
||||
- Fixed: Fixed sidebar menu selection (422a5bf)
|
||||
- Fixed: Fixed user deletion schedule (bfe5ee8)
|
||||
- Fixed: Published note must be scrollable when it contains a large table (#6370)
|
||||
|
||||
## [server-v2.7.4](https://github.com/laurent22/joplin/releases/tag/server-v2.7.4) - 2022-02-02T19:23:34Z
|
||||
|
||||
- New: Add task to automate deletion of disabled accounts (1afcb27)
|
||||
|
@@ -2,6 +2,16 @@
|
||||
|
||||
Coding style is mostly enforced by a pre-commit hook that runs `eslint`. This hook is installed whenever running `yarn install` on any of the application directory. If for some reason the pre-commit hook didn't get installed, you can manually install it by running `yarn install` at the root of the repository.
|
||||
|
||||
## Enforcing rules using eslint
|
||||
|
||||
Whenever possible, coding style should be enforced using an eslint rule. To do so, add the relevant rule or plugin to `eslintrc.js`. To manually run the linter, run `yarn run linter ./` from the root of the project.
|
||||
|
||||
When adding a rule, you will often find that many files will no longer pass the linter. In that case, you have two options:
|
||||
|
||||
- Fix the files one by one. If there aren't too many files, and the changes are simple (they are unlikely to introduce regressions), this is the preferred solution.
|
||||
|
||||
- Or use `yarn run linter-interactive ./` to disable existing errors. The interactive tool will process all the files and you can then choose to disable any existing error that it finds (by adding a `eslint-disable-next-line` comment above it). This allows keeping the existing, working codebase as it is, and enforcing that new code follows the rule. When using this method, add the comment "Old code before rule was applied" so that we can easily find back all the lines that have been automatically disabled.
|
||||
|
||||
## Use TypeScript for new files
|
||||
|
||||
### Creating a new `.ts` file
|
||||
|
@@ -107,7 +107,7 @@ Unfortunately it is not possible. Joplin synchronises with file systems using an
|
||||
|
||||
The end to end encryption that Joplin implements is to protect the data during transmission and on the cloud service so that only you can access it.
|
||||
|
||||
On the local device it is assumed that the data is safe due to the OS built-in security features. If additional security is needed it's always possible to put the notes on an encrypted Truecrypt drive for instance.
|
||||
On the local device it is assumed that the data is safe due to the OS built-in security features. If additional security is needed it's always possible to put the notes on an encrypted VeraCrypt drive for instance.
|
||||
|
||||
For these reasons, because the OS or yourself can easily protect the local data, no PIN or password is currently supported to access Joplin.
|
||||
|
||||
|
@@ -30,7 +30,7 @@ This is a quick summary of the Markdown syntax.
|
||||
|
||||
### Tables
|
||||
|
||||
Tables are created using pipes `|` and and hyphens `-`. This is a Markdown table:
|
||||
Tables are created using pipes `|` and hyphens `-`. This is a Markdown table:
|
||||
|
||||
| First Header | Second Header |
|
||||
| ------------- | ------------- |
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user