1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

...

92 Commits

Author SHA1 Message Date
Laurent Cozic
0c1f4031b4 Server v2.10.6 2023-02-06 19:01:57 +00:00
Laurent Cozic
9ed022458b Chore: Server: Clean up 2023-02-06 18:59:36 +00:00
Laurent Cozic
ba5f0bc6e3 Server: Fixed issue when an item is associated with a share that no longer exists 2023-02-06 18:59:36 +00:00
renovate[bot]
793e8f6c0f Update dependency jsdom to v21 (#7732) 2023-02-06 17:29:47 +00:00
renovate[bot]
6182ce521d Update dependency nodemailer to v6.9.1 (#7726) 2023-02-06 16:17:15 +00:00
Joplin Bot
544c50663a Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-02-06 10:17:57 +00:00
Laurent Cozic
53aa9e2b42 Chore: Restore stats 2023-02-06 10:07:25 +00:00
renovate[bot]
884260189c Update dependency punycode to v2.2.2 (#7724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-05 20:28:43 +00:00
Laurent Cozic
2f9464f21f Tools: Revert CI issue 2023-02-05 19:10:31 +00:00
github-actions[bot]
c6e993b04a @TaoK has signed the CLA from Pull Request #7729 2023-02-05 17:06:32 +00:00
Laurent Cozic
89eb012b25 Tools: Add repeat mechanism when electron-builder randomly fails to build 2023-02-05 16:51:47 +00:00
Laurent Cozic
1e2aa4e2b5 Tools: Fixed Renovate patterns 2023-02-05 12:27:09 +00:00
Laurent Cozic
0019bb8d6b Tools: Add eslint rule "@typescript-eslint/no-inferrable-types" 2023-02-05 12:27:09 +00:00
renovate[bot]
e629a4d325 Update dependency nodemailer to v6.9.0 (#7722) 2023-02-05 11:43:37 +00:00
renovate[bot]
049c769d37 Update dependency eslint-plugin-react to v7.32.0 (#7698) 2023-02-05 11:41:13 +00:00
renovate[bot]
47aed8742a Update dependency punycode to v2.2.0 (#7695) 2023-02-05 11:40:58 +00:00
Adarsh Singh
8aad67ccfe Desktop: Fixes #7521: Mermaid images are incorrectly sized when exported as PNG (#7546) 2023-02-05 11:39:26 +00:00
renovate[bot]
af7cbcbca7 Update dependency eslint-plugin-import to v2.27.4 (#7717) 2023-02-05 11:06:20 +00:00
Laurent Cozic
88a91314af Tools: Reduce noise with Renovate updates 2023-02-05 11:06:10 +00:00
renovate[bot]
9873c2d756 Update dependency glob to v8.1.0 (#7718) 2023-02-04 16:12:16 +00:00
renovate[bot]
46b68cf461 Update typescript-eslint monorepo to v5.48.2 (#7705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-04 07:58:54 +00:00
renovate[bot]
6b96b1f355 Update dependency react-native-paper to v5.1.4 (#7714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-04 05:37:03 +00:00
renovate[bot]
dc819700bb Update dependency knex to v2.4.2 (#7713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-04 01:11:49 +00:00
renovate[bot]
1d1d5fea06 Update dependency @react-native-community/datetimepicker to v6.7.3 (#7712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-03 21:02:58 +00:00
renovate[bot]
c3afc0ede7 Update dependency prettier to v2.8.3 (#7704)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-03 17:08:36 +00:00
github-actions[bot]
2092110a6e @julien-me has signed the CLA from Pull Request #7711 2023-02-03 12:51:14 +00:00
github-actions[bot]
fc940e9a7c @abhiippili has signed the CLA from Pull Request #7709 2023-02-01 17:12:16 +00:00
github-actions[bot]
8718310dd0 @deepampriyadarshi has signed the CLA from Pull Request #7708 2023-02-01 15:00:52 +00:00
renovate[bot]
1b527f2bbe Update dependency @react-native-community/slider to v4.4.1 (#7702)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-01 00:38:41 +00:00
Joplin Bot
4da217bc2f Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-01-31 00:43:35 +00:00
github-actions[bot]
d3abd4ebf2 @tessus has signed the CLA from Pull Request #7697 2023-01-31 00:13:17 +00:00
Mr-Kanister
38851edf86 All: Translation: Update de_DE.po (#7696) 2023-01-30 18:15:05 -05:00
Joplin Bot
05efb765d6 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-01-30 18:16:44 +00:00
majsterkovic
28dc4a6abd All: Translation: Update pl_PL.po (#7690) 2023-01-29 22:54:17 -05:00
renovate[bot]
2f7b56f96f Update dependency @react-native-community/datetimepicker to v6.7.2 (#7689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-30 02:26:11 +00:00
renovate[bot]
fdaa3735fb Update dependency @types/yargs to v17.0.20 (#7685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-29 17:03:51 +00:00
Laurent Cozic
7dfaea12f7 Chore: Fixed build following conversion from JSX to TSX 2023-01-29 13:11:53 +00:00
renovate[bot]
18199b27d9 Update dependency @types/react to v17.0.53 (#7684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-29 06:18:39 +00:00
renovate[bot]
a7c52082bb Update dependency @types/react to v16.14.35 (#7683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-29 02:18:29 +00:00
renovate[bot]
3b5357e0c1 Update dependency @types/jest to v29.2.6 (#7682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-28 22:41:15 +00:00
Self Not Found
10dd4e45ed Desktop: Fixes #7678: Fix open files with non-ASCII characters in path (#7679) 2023-01-28 12:28:01 +00:00
github-actions[bot]
1963835309 @carlosngo has signed the CLA from Pull Request #7681 2023-01-28 10:07:12 +00:00
renovate[bot]
46ec0c1381 Update dependency knex to v2.4.1 (#7676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-28 03:43:16 +00:00
Laurent Cozic
bb84ae4d68 Tools: Disable tests that randomly fail onn CI 2023-01-27 18:55:39 +00:00
Laurent Cozic
b3ff53c0da Tools: Added hack to try to fix issue with broken GitHub API 2023-01-27 17:16:23 +00:00
Self Not Found
acd7bfd9f5 Desktop: Remove auto-matching for greater than character (#7669) 2023-01-27 16:50:07 +00:00
renovate[bot]
1dff50d080 Update dependency knex to v2.4.0 (#7674) 2023-01-27 16:47:20 +00:00
Laurent Cozic
af40970d09 Ignore package 2023-01-27 16:46:37 +00:00
Joplin Bot
07535a494e Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-01-27 06:17:16 +00:00
Joplin Bot
907422cefa Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-01-27 00:45:49 +00:00
renovate[bot]
f643baea25 Update dependency tap to v16.3.4 (#7671)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 08:50:49 +00:00
github-actions[bot]
55a4f33982 @majsterkovic has signed the CLA from Pull Request #7668 2023-01-25 21:26:20 +00:00
Dmitriy Q
df6700959a All: Translation: Update ru_RU.po (#7665) 2023-01-25 16:10:50 -05:00
github-actions[bot]
fde8235f3e @krote5k has signed the CLA from Pull Request #7665 2023-01-25 12:59:46 +00:00
Laurent Cozic
f6ba56d966 Doc: Added GSoC idea 2023-01-24 15:56:22 +00:00
Laurent Cozic
4a5312823b Doc: Add info for GSoC 2023 2023-01-24 15:18:51 +00:00
Light
31a27b0e1c Desktop: Fixes #7565: Fix text editor text highlighting when used with special IME methods (#7630) 2023-01-24 14:46:40 +00:00
github-actions[bot]
984ad868e8 @trevor-james-nangosha has signed the CLA from Pull Request #7663 2023-01-24 12:56:39 +00:00
Betty Alagwu
9b657eeda2 Desktop: Resolves #7602: Fix copy text with no selection (#7641) 2023-01-23 18:50:24 +00:00
Laurent Cozic
6f3ad4b3b0 Doc: Allow setting period from parameter 2023-01-23 17:49:16 +00:00
renovate[bot]
56f06fae3c Update dependency ts-jest to v29.0.5 (#7659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-23 15:52:54 +00:00
ThetaDev
c22d884357 Doc: Update rscss link (#7653) 2023-01-23 10:11:34 +00:00
github-actions[bot]
35dc22197d @Theta-Dev has signed the CLA from Pull Request #7653 2023-01-22 22:43:08 +00:00
renovate[bot]
70d56ca0be Update dependency ts-jest to v29.0.4 (#7651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-22 02:05:55 +00:00
Laurent Cozic
bca09b9476 Android 2.10.5 2023-01-21 14:28:32 +00:00
javad mnjd
b82bf16505 Android: Resolves #6942: Improve filesystem sync performance (#7637) 2023-01-21 14:11:37 +00:00
Laurent Cozic
2f254d81cd Ignore some dependencies 2023-01-21 12:20:22 +00:00
Self Not Found
b14ce03e5b All: Translation: Update zh_CN.po (#7643) 2023-01-20 20:38:45 -05:00
Laurent Cozic
90b04cbd37 Doc: Fixes #7642: Fixed menu padding in narrower view 2023-01-20 18:07:50 +00:00
Laurent Cozic
5ae866ea85 iOS 12.10.2 2023-01-20 17:42:14 +00:00
Laurent Cozic
b450ab9f5a lock file 2023-01-20 17:41:03 +00:00
Laurent Cozic
138bc8144b Android: Fixes non-working alarms
Also imported react-native-alarm-notificatio into the project
2023-01-20 17:33:19 +00:00
Laurent Cozic
c9831833c4 Desktop: Fixes #7617: Note editor scrolls back to top when editing certain notes 2023-01-20 15:05:57 +00:00
Laurent Cozic
2813f93c18 Desktop: Fixes #7617: Note editor scrolls back to top when editing certain notes 2023-01-20 15:03:22 +00:00
Laurent Cozic
27bec674a0 Chore: Desktop: Convert last JSX files to TSX 2023-01-20 14:35:22 +00:00
renovate[bot]
ff79ca8781 Update dependency react-native-paper to v5.1.3 (#7638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-20 01:12:04 +00:00
github-actions[bot]
34a1342db6 @jd1378 has signed the CLA from Pull Request #7637 2023-01-19 20:42:18 +00:00
renovate[bot]
e252986b98 Update dependency tap to v16.3.3 (#7631)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-19 17:25:42 +00:00
Helmut K. C. Tessarek
3537c3e5f9 All: Translation: Update da_DK.po (thanks ERYpTION) 2023-01-19 08:19:19 -05:00
github-actions[bot]
fdc86f94c4 @LightAPIs has signed the CLA from Pull Request #7630 2023-01-18 17:05:59 +00:00
Laurent Cozic
dc5dc94ed5 Desktop: Fixes #7621: Certain plugins could create invalid settings, which could result in a crash 2023-01-17 15:34:04 +00:00
Laurent Cozic
f7682d3da3 Desktop: Resolves #7506: Disable custom PDF viewer by default 2023-01-17 13:35:08 +00:00
Joplin Bot
8e2975d23d Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-01-16 17:48:37 +00:00
Laurent Cozic
ce85489166 Docs: Added news about "GitHub Action Raw Log Viewer" 2023-01-16 17:28:30 +00:00
Mike Sheldon
13f5738090 All: Resolves #7627: Improve dialogue spacing in Fountain renderer (#7628) 2023-01-16 15:46:17 +00:00
github-actions[bot]
cce2ae7401 @Elleo has signed the CLA from Pull Request #7628 2023-01-16 14:00:37 +00:00
Laurent Cozic
c9b49a50c8 Desktop release v2.10.5 2023-01-16 13:41:31 +00:00
github-actions[bot]
c419c43622 @JackGruber has signed the CLA from Pull Request #7622 2023-01-15 15:44:53 +00:00
Joplin Bot
cc5ecfba2b Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-01-15 06:17:01 +00:00
Joplin Bot
a98d5feff6 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-01-15 00:44:15 +00:00
Laurent Cozic
7aa4feffd4 Update translations 2023-01-14 20:56:46 +00:00
Joplin Bot
0b46a744f1 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-01-14 18:17:51 +00:00
198 changed files with 23060 additions and 15526 deletions

View File

@@ -142,7 +142,12 @@ packages/app-desktop/gui/EditFolderDialog/IconSelector.js
packages/app-desktop/gui/EmojiBox.js
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ExtensionBadge.js
packages/app-desktop/gui/FolderIconBox.js
packages/app-desktop/gui/HelpButton.js
packages/app-desktop/gui/IconButton.js
packages/app-desktop/gui/ImportScreen.js
packages/app-desktop/gui/ItemList.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
packages/app-desktop/gui/KeymapConfig/styles/index.js
@@ -193,6 +198,7 @@ packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js
packages/app-desktop/gui/MenuBar.js
packages/app-desktop/gui/MultiNoteActions.js
packages/app-desktop/gui/Navigator.js
packages/app-desktop/gui/NoteContentPropertiesDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
@@ -252,6 +258,10 @@ packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
packages/app-desktop/gui/NoteListControls/commands/index.js
packages/app-desktop/gui/NoteListItem.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NotePropertiesDialog.js
packages/app-desktop/gui/NoteRevisionViewer.js
packages/app-desktop/gui/NoteSearchBar.js
packages/app-desktop/gui/NoteStatusBar.js
packages/app-desktop/gui/NoteTextViewer.js
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
packages/app-desktop/gui/OneDriveLoginScreen.js
@@ -290,12 +300,14 @@ packages/app-desktop/gui/Sidebar/styles/index.js
packages/app-desktop/gui/StatusScreen/StatusScreen.js
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js
packages/app-desktop/gui/SyncWizard/Dialog.js
packages/app-desktop/gui/TagItem.js
packages/app-desktop/gui/TagList.js
packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
packages/app-desktop/gui/ToolbarBase.js
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/dialogs.js
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js

View File

@@ -159,6 +159,7 @@ module.exports = {
// make everything public which is not great. New code however should specify member accessibility.
'@typescript-eslint/explicit-member-accessibility': ['warn'],
'@typescript-eslint/type-annotation-spacing': ['error', { 'before': false, 'after': true }],
'@typescript-eslint/no-inferrable-types': ['error', { 'ignoreParameters': true, 'ignoreProperties': true }],
'@typescript-eslint/comma-dangle': ['error', {
'arrays': 'always-multiline',
'objects': 'always-multiline',

12
.gitignore vendored
View File

@@ -130,7 +130,12 @@ packages/app-desktop/gui/EditFolderDialog/IconSelector.js
packages/app-desktop/gui/EmojiBox.js
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ExtensionBadge.js
packages/app-desktop/gui/FolderIconBox.js
packages/app-desktop/gui/HelpButton.js
packages/app-desktop/gui/IconButton.js
packages/app-desktop/gui/ImportScreen.js
packages/app-desktop/gui/ItemList.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
packages/app-desktop/gui/KeymapConfig/styles/index.js
@@ -181,6 +186,7 @@ packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js
packages/app-desktop/gui/MenuBar.js
packages/app-desktop/gui/MultiNoteActions.js
packages/app-desktop/gui/Navigator.js
packages/app-desktop/gui/NoteContentPropertiesDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
@@ -240,6 +246,10 @@ packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
packages/app-desktop/gui/NoteListControls/commands/index.js
packages/app-desktop/gui/NoteListItem.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NotePropertiesDialog.js
packages/app-desktop/gui/NoteRevisionViewer.js
packages/app-desktop/gui/NoteSearchBar.js
packages/app-desktop/gui/NoteStatusBar.js
packages/app-desktop/gui/NoteTextViewer.js
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
packages/app-desktop/gui/OneDriveLoginScreen.js
@@ -278,12 +288,14 @@ packages/app-desktop/gui/Sidebar/styles/index.js
packages/app-desktop/gui/StatusScreen/StatusScreen.js
packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js
packages/app-desktop/gui/SyncWizard/Dialog.js
packages/app-desktop/gui/TagItem.js
packages/app-desktop/gui/TagList.js
packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
packages/app-desktop/gui/ToolbarBase.js
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/dialogs.js
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js

View File

@@ -13,7 +13,8 @@
"@joplin/turndown",
"@joplin/turndown-plugin-gfm",
"@joplin/tools",
"@joplin/react-native-saf-x"
"@joplin/react-native-saf-x",
"@joplin/react-native-alarm-notification"
]
}
]

View File

@@ -728,6 +728,16 @@ footer .bottom-links-row p {
}
}
/*****************************************************************
LARGE VIEW
*****************************************************************/
@media (max-width: 1200px) {
#nav-section a {
margin-left: 10px;
}
}
/*****************************************************************
MEDIUM VIEW
- Make menu bar elements smaller and closer to each others

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

View File

@@ -1,4 +1,15 @@
<?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>Wed, 21 Dec 2022 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Wed, 21 Dec 2022 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)]]></title><description><![CDATA[<p>As was <a href="https://discourse.joplinapp.org/t/rfc-switch-to-agpl-license-for-joplin-server/16529">discussed last year</a>, Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0) for the desktop, mobile and CLI applications, as well as the web clipper.</p>
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 16 Jan 2023 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 16 Jan 2023 00:00:00 GMT</pubDate><item><title><![CDATA[Introducing the "GitHub Actions Raw Log Viewer" extension for Chrome]]></title><description><![CDATA[<p>If you've ever used GitHub Actions, you will find that they provide by default a nice coloured output for the log. It looks good and it's even interactive! (You can click to collapse/expand blocks of text) But unfortunately it doesn't scale to large workflows, like we have for Joplin - the log can freeze and it will take forever to search for something. Indeed searching is done in &quot;real time&quot;... which mostly means it will freeze for a minute or two for each letter you type in the search box. Not great.</p>
<p>Thankfully GitHub provides an alternative access: the raw logs. This is much better because they will open as plain text, without any styling or JS magic, which means you can use the browser native search and it will be fast.</p>
<p>But now the problem is that raw logs look like this:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-ga-raw-log.png" alt="Raw log without extension"></p>
<p>While it's not impossible to read, all colours that would display nicely in a terminal are gone and replaced by <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI codes</a>. You can find what you need in there but it's not particularly easy.</p>
<p>This is where the new <strong>GitHub Action Raw Log Viewer</strong> extension for Chrome can help. It will parse your raw log and convert the ANSI codes to proper colours. This results in a much more readable rendering:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-ga-raw-log-colored.png" alt="Raw log with extension"></p>
<p>The extension is fast even for very large logs and it's of course easy to search for text since it simply works with your browser built-in search.</p>
<p>The extension is open source, with the code available here: <a href="https://github.com/laurent22/github-actions-logs-extension">https://github.com/laurent22/github-actions-logs-extension</a></p>
<p>And to install it, follow this link:</p>
<p><a href="https://chrome.google.com/webstore/detail/github-action-raw-log-vie/lgejlnoopmcdglhfjblaeldbcfnmjddf"><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-extension-get-it-now.png" alt="Download GitHub Action Raw Log Viewer extension"></a></p>
]]></description><link>https://joplinapp.org/news/20230116-github-actions-log-viewer/</link><guid isPermaLink="false">20230116-github-actions-log-viewer</guid><pubDate>Mon, 16 Jan 2023 00:00:00 GMT</pubDate><twitter-text>Introducing the &quot;GitHub Action Raw Log Viewer&quot; extension for Chrome</twitter-text></item><item><title><![CDATA[Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)]]></title><description><![CDATA[<p>As was <a href="https://discourse.joplinapp.org/t/rfc-switch-to-agpl-license-for-joplin-server/16529">discussed last year</a>, Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0) for the desktop, mobile and CLI applications, as well as the web clipper.</p>
<p>Any open source or commercial fork of Joplin will have to license any changes they make under AGPL, and share these changes back with the community. This is the main reason we switch to this license. It allows us to continue releasing the project as open source while ensuring that those who benefit commercially (or not) from it share back their changes.</p>
<h2>What is the GPL license?<a name="what-is-the-gpl-license" href="#what-is-the-gpl-license" class="heading-anchor">🔗</a></h2>
<p>The AGPL license is based on the GPL license. This is what tldr;Legal has to say about the GPL license:</p>
@@ -288,9 +299,4 @@
<p>For now, since we don't have a review process, the recommended plugins are those developed by the Joplin team and frequent contributors, because we know those are safe to use.</p>
<p>Later we might have a review process and add more recommended plugins. That being said, in the meantime even if a plugin is not marked as recommended, there's a good chance it is still safe and have good performance too. Often you can search for it on the forum and if it's active with many users commenting, you're most likely good to go.</p>
<p>But if there's any doubt, the recommended tag is a good way to be sure.</p>
]]></description><link>https://joplinapp.org/news/20210901-113415/</link><guid isPermaLink="false">20210901-113415</guid><pubDate>Wed, 01 Sep 2021 11:34:15 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin Cloud is officially production ready!]]></title><description><![CDATA[<p><a href="https://joplinapp.org/plans/">Joplin Cloud</a> has been out of beta for a few weeks now and since then it has been quietly running without any troubles. There is no known bugs and the service is running smoothly so it's now safe to say that it is production ready!</p>
<p>As a reminder, Joplin Cloud is meant to provide a more seamless Joplin experience - if you want to quickly get started, it's as easy as downloading the app and getting a Joplin Cloud account. Besides improved sync performance, that will give you the ability to collaborate on notebooks with others, as well as publishing and sharing notes.</p>
<p>Of course Joplin still supports other sync options such as Nextcloud, Dropbox and OneDrive or AWS S3. You can also self host using Joplin Server. The advantage of Joplin Cloud being that you don't need to maintain a server yourself - for a small fee you'll get that taken care of.</p>
<p>Additionally, subscribing to Joplin Cloud is a great way to support the project as a whole, including the open source applications. Such support is needed in the long term to provide bug and security fixes, add new features, and provide support.</p>
<p>At some level it is also an experiment, to see if such a service is financially viable and can allow me to work full time on the project. This is certainly something I would like, and perhaps Joplin Cloud combined with your donations will allow that.</p>
]]></description><link>https://joplinapp.org/news/20210831-154354/</link><guid isPermaLink="false">20210831-154354</guid><pubDate>Tue, 31 Aug 2021 15:43:54 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
]]></description><link>https://joplinapp.org/news/20210901-113415/</link><guid isPermaLink="false">20210901-113415</guid><pubDate>Wed, 01 Sep 2021 11:34:15 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>

View File

@@ -130,7 +130,11 @@
});
setupBetaHandling(urlQuery);
applyPeriod('yearly');
if (urlQuery.get('period') === 'monthly') {
// Nothing - this is the default
} else {
applyPeriod('yearly');
}
});
</script>
</div>

View File

@@ -530,47 +530,47 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 82%
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 81%
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 23%
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 59%
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 46%
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 79%
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [MrKanister](mailto:pueblos_spatulas@aleeas.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 58%
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 45%
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 78%
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 97%
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [MrKanister](mailto:pueblos_spatulas@aleeas.com) | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 45%
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Mora](mailto:francisco.m.collao@gmail.com) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Mora](mailto:francisco.m.collao@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 26%
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 98%
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 98%
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 30%
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 80%
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 80%
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 81%
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 57%
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [X3NO](mailto:X3NO@disroot.org) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Renato Nunes Bastos](mailto:rnbastos@gmail.com) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 75%
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 52%
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 83%
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 38%
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 80%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 75%
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 83%
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 67%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [KaneGreen](mailto:737445366KG@Gmail.com) | 97%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 100%
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 29%
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 81%
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 78%
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 79%
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 56%
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [X3NO](mailto:X3NO@disroot.org) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Renato Nunes Bastos](mailto:rnbastos@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 73%
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 51%
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 81%
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 97%
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 37%
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 79%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 73%
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 81%
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 66%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [KaneGreen](mailto:737445366KG@Gmail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 90%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Contributors

View File

@@ -317,6 +317,9 @@
"packages/app-tools/github_oauth_token.txt": true,
"packages/generator-joplin/generators/app/templates/api/": true,
"packages/htmlpack/dist/": true,
"packages/react-native-alarm-notification/android/build": true,
"packages/react-native-saf-x/android/build": true,
"packages/react-native-saf-x/android/wrapper": true,
"packages/renderer/**/.vscode/": true,
"packages/renderer/**/copyLib.bat": true,
"packages/renderer/**/node_modules/": true,

View File

@@ -65,16 +65,16 @@
},
"devDependencies": {
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
"@typescript-eslint/eslint-plugin": "5.48.0",
"@typescript-eslint/parser": "5.48.0",
"@typescript-eslint/eslint-plugin": "5.48.2",
"@typescript-eslint/parser": "5.48.2",
"cspell": "5.21.2",
"eslint": "8.31.0",
"eslint-interactive": "10.3.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-import": "2.27.4",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react": "7.32.0",
"fs-extra": "11.1.0",
"glob": "8.0.3",
"glob": "8.1.0",
"gulp": "4.0.2",
"husky": "3.1.0",
"lerna": "3.22.1",

View File

@@ -70,7 +70,7 @@
"devDependencies": {
"@joplin/tools": "~2.10",
"@types/fs-extra": "9.0.13",
"@types/jest": "29.2.5",
"@types/jest": "29.2.6",
"@types/node": "18.11.18",
"gulp": "4.0.2",
"jest": "29.3.1",

View File

@@ -1,7 +1,7 @@
const React = require('react');
const { connect } = require('react-redux');
const { clipboard } = require('electron');
const ExtensionBadge = require('./ExtensionBadge.min');
import ExtensionBadge from './ExtensionBadge';
import bridge from '../services/bridge';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';

View File

@@ -453,6 +453,12 @@ class ConfigScreenComponent extends React.Component<any, any> {
inputStyle.marginBottom = subLabel.marginBottom;
const splitCmd = (cmdString: string) => {
// Normally not necessary but certain plugins found a way to
// set the set the value to "undefined", leading to a crash.
// This is now fixed at the model level but to be sure we
// check here too, to handle any already existing data.
// https://github.com/laurent22/joplin/issues/7621
if (!cmdString) cmdString = '';
const path = pathUtils.extractExecutablePath(cmdString);
const args = cmdString.substr(path.length + 1);
return [pathUtils.unquotePath(path), args];

View File

@@ -20,7 +20,7 @@ const { space } = require('styled-system');
const logger = Logger.create('PluginState');
const maxWidth: number = 320;
const maxWidth = 320;
const Root = styled.div`
display: flex;

View File

@@ -1,45 +0,0 @@
const React = require('react');
const bridge = require('@electron/remote').require('./bridge').default;
const styleSelector = require('./style/ExtensionBadge');
const { _ } = require('@joplin/lib/locale');
function platformAssets(type) {
if (type === 'firefox') {
return {
logoImage: `${bridge().buildDir()}/images/firefox-logo.svg`,
locationLabel: _('Firefox Extension'),
};
}
if (type === 'chrome') {
return {
logoImage: `${bridge().buildDir()}/images/chrome-logo.svg`,
locationLabel: _('Chrome Web Store'),
};
}
throw new Error(`Invalid type:${type}`);
}
function ExtensionBadge(props) {
const style = styleSelector(null, props);
const assets = platformAssets(props.type);
const onClick = () => {
bridge().openExternal(props.url);
};
const rootStyle = props.style ? Object.assign({}, style.root, props.style) : style.root;
return (
<a style={rootStyle} onClick={onClick} href="#">
<img style={style.logo} src={assets.logoImage}/>
<div style={style.labelGroup} >
<div>{_('Get it now:')}</div>
<div style={style.locationLabel}>{assets.locationLabel}</div>
</div>
</a>
);
}
module.exports = ExtensionBadge;

View File

@@ -0,0 +1,98 @@
import * as React from 'react';
import bridge from '../services/bridge';
import { _ } from '@joplin/lib/locale';
import { themeStyle } from '@joplin/lib/theme';
const { createSelector } = require('reselect');
interface Props {
themeId: number;
type: string;
url: string;
style?: any;
}
const themeSelector = (_state: any, props: any) => themeStyle(props.themeId);
const styleSelector = createSelector(
themeSelector,
(theme: any) => {
const output = {
root: {
width: 220,
height: 60,
borderRadius: 4,
border: '1px solid',
borderColor: theme.dividerColor,
backgroundColor: theme.backgroundColor,
paddingLeft: 14,
paddingRight: 14,
paddingTop: 8,
paddingBottom: 8,
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'row',
boxShadow: '0px 1px 1px rgba(0,0,0,0.3)',
},
logo: {
width: 42,
height: 42,
},
labelGroup: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
marginLeft: 14,
fontFamily: theme.fontFamily,
color: theme.color,
fontSize: theme.fontSize,
},
locationLabel: {
fontSize: theme.fontSize * 1.2,
fontWeight: 'bold',
},
};
return output;
}
);
function platformAssets(type: string) {
if (type === 'firefox') {
return {
logoImage: `${bridge().buildDir()}/images/firefox-logo.svg`,
locationLabel: _('Firefox Extension'),
};
}
if (type === 'chrome') {
return {
logoImage: `${bridge().buildDir()}/images/chrome-logo.svg`,
locationLabel: _('Chrome Web Store'),
};
}
throw new Error(`Invalid type:${type}`);
}
function ExtensionBadge(props: Props) {
const style = styleSelector(null, props);
const assets = platformAssets(props.type);
const onClick = () => {
void bridge().openExternal(props.url);
};
const rootStyle = props.style ? Object.assign({}, style.root, props.style) : style.root;
return (
<a style={rootStyle} onClick={onClick} href="#">
<img style={style.logo} src={assets.logoImage}/>
<div style={style.labelGroup} >
<div>{_('Get it now:')}</div>
<div style={style.locationLabel}>{assets.locationLabel}</div>
</div>
</a>
);
}
export default ExtensionBadge;

View File

@@ -1,10 +1,18 @@
const React = require('react');
import * as React from 'react';
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
import { themeStyle } from '@joplin/lib/theme';
import { AppState } from '../app.reducer';
class HelpButtonComponent extends React.Component {
constructor() {
super();
interface Props {
tip: string;
onClick: Function;
themeId: number;
style: any;
}
class HelpButtonComponent extends React.Component<Props> {
constructor(props: Props) {
super(props);
this.onClick = this.onClick.bind(this);
}
@@ -17,7 +25,7 @@ class HelpButtonComponent extends React.Component {
const theme = themeStyle(this.props.themeId);
const style = Object.assign({}, this.props.style, { color: theme.color, textDecoration: 'none' });
const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 };
const extraProps = {};
const extraProps: any = {};
if (this.props.tip) extraProps['data-tip'] = this.props.tip;
return (
<a href="#" style={style} onClick={this.onClick} {...extraProps}>
@@ -27,7 +35,7 @@ class HelpButtonComponent extends React.Component {
}
}
const mapStateToProps = state => {
const mapStateToProps = (state: AppState) => {
return {
themeId: state.settings.theme,
};
@@ -35,4 +43,4 @@ const mapStateToProps = state => {
const HelpButton = connect(mapStateToProps)(HelpButtonComponent);
module.exports = HelpButton;
export default HelpButton;

View File

@@ -1,7 +1,14 @@
const React = require('react');
const { themeStyle } = require('@joplin/lib/theme');
import * as React from 'react';
import { themeStyle } from '@joplin/lib/theme';
class IconButton extends React.Component {
interface Props {
themeId: number;
style: any;
iconName: string;
onClick: Function;
}
class IconButton extends React.Component<Props> {
render() {
const style = this.props.style;
const theme = themeStyle(this.props.themeId);
@@ -42,4 +49,4 @@ class IconButton extends React.Component {
}
}
module.exports = { IconButton };
export default IconButton;

View File

@@ -1,12 +1,29 @@
const React = require('react');
import * as React from 'react';
import Folder from '@joplin/lib/models/Folder';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import { filename, basename } from '@joplin/lib/path-utils';
import importEnex from '@joplin/lib/import-enex';
import { AppState } from '../app.reducer';
const { connect } = require('react-redux');
const Folder = require('@joplin/lib/models/Folder').default;
const { themeStyle } = require('@joplin/lib/theme');
const { _ } = require('@joplin/lib/locale');
const { filename, basename } = require('@joplin/lib/path-utils');
const importEnex = require('@joplin/lib/import-enex').default;
class ImportScreenComponent extends React.Component {
interface Props {
filePath: string;
themeId: number;
}
interface Message {
key: string;
text: string;
}
interface State {
filePath: string;
doImport: boolean;
messages: Message[];
}
class ImportScreenComponent extends React.Component<Props, State> {
UNSAFE_componentWillMount() {
this.setState({
doImport: true,
@@ -15,7 +32,7 @@ class ImportScreenComponent extends React.Component {
});
}
UNSAFE_componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps: Props) {
if (newProps.filePath) {
this.setState(
{
@@ -24,7 +41,7 @@ class ImportScreenComponent extends React.Component {
messages: [],
},
() => {
this.doImport();
void this.doImport();
}
);
}
@@ -32,11 +49,11 @@ class ImportScreenComponent extends React.Component {
componentDidMount() {
if (this.state.filePath && this.state.doImport) {
this.doImport();
void this.doImport();
}
}
addMessage(key, text) {
addMessage(key: string, text: string) {
const messages = this.state.messages.slice();
messages.push({ key: key, text: text });
@@ -66,7 +83,7 @@ class ImportScreenComponent extends React.Component {
let lastProgress = '';
const options = {
onProgress: progressState => {
onProgress: (progressState: any) => {
const line = [];
line.push(_('Found: %d.', progressState.loaded));
line.push(_('Created: %d.', progressState.created));
@@ -77,7 +94,7 @@ class ImportScreenComponent extends React.Component {
lastProgress = line.join(' ');
this.addMessage('progress', lastProgress);
},
onError: error => {
onError: (error: any) => {
// Don't display the error directly because most of the time it doesn't matter
// (eg. for weird broken HTML, but the note is still imported)
console.warn('When importing ENEX file', error);
@@ -116,7 +133,7 @@ class ImportScreenComponent extends React.Component {
}
}
const mapStateToProps = state => {
const mapStateToProps = (state: AppState) => {
return {
themeId: state.settings.theme,
};
@@ -124,4 +141,5 @@ const mapStateToProps = state => {
const ImportScreen = connect(mapStateToProps)(ImportScreenComponent);
module.exports = { ImportScreen };
export default ImportScreen;

View File

@@ -1,8 +1,27 @@
const React = require('react');
import * as React from 'react';
class ItemList extends React.Component {
constructor() {
super();
interface Props {
style: any;
itemHeight: number;
items: any[];
disabled?: boolean;
onKeyDown?: Function;
itemRenderer: Function;
className?: string;
}
interface State {
topItemIndex: number;
bottomItemIndex: number;
}
class ItemList extends React.Component<Props, State> {
private scrollTop_: number;
private listRef: any;
constructor(props: Props) {
super(props);
this.scrollTop_ = 0;
@@ -12,12 +31,12 @@ class ItemList extends React.Component {
this.onKeyDown = this.onKeyDown.bind(this);
}
visibleItemCount(props) {
visibleItemCount(props: Props = undefined) {
if (typeof props === 'undefined') props = this.props;
return Math.ceil(props.style.height / props.itemHeight);
}
updateStateItemIndexes(props) {
updateStateItemIndexes(props: Props = undefined) {
if (typeof props === 'undefined') props = this.props;
const topItemIndex = Math.floor(this.scrollTop_ / props.itemHeight);
@@ -44,20 +63,20 @@ class ItemList extends React.Component {
this.updateStateItemIndexes();
}
UNSAFE_componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps: Props) {
this.updateStateItemIndexes(newProps);
}
onScroll(event) {
onScroll(event: any) {
this.scrollTop_ = event.target.scrollTop;
this.updateStateItemIndexes();
}
onKeyDown(event) {
onKeyDown(event: any) {
if (this.props.onKeyDown) this.props.onKeyDown(event);
}
makeItemIndexVisible(itemIndex) {
makeItemIndexVisible(itemIndex: number) {
const top = Math.min(this.props.items.length - 1, this.state.topItemIndex);
const bottom = Math.max(0, this.state.bottomItemIndex);
@@ -105,7 +124,7 @@ class ItemList extends React.Component {
if (!this.props.itemHeight) throw new Error('itemHeight is required');
const blankItem = function(key, height) {
const blankItem = function(key: string, height: number) {
return <div key={key} style={{ height: height }}></div>;
};
@@ -129,4 +148,4 @@ class ItemList extends React.Component {
}
}
module.exports = { ItemList };
export default ItemList;

View File

@@ -43,7 +43,7 @@ import invitationRespond from '../../services/share/invitationRespond';
import restart from '../../services/restart';
const { connect } = require('react-redux');
import PromptDialog from '../PromptDialog';
const NotePropertiesDialog = require('../NotePropertiesDialog.min.js');
import NotePropertiesDialog from '../NotePropertiesDialog';
const PluginManager = require('@joplin/lib/services/PluginManager');
const ipcRenderer = require('electron').ipcRenderer;

View File

@@ -1,11 +1,15 @@
const React = require('react');
const Component = React.Component;
const Setting = require('@joplin/lib/models/Setting').default;
const { connect } = require('react-redux');
import Setting from '@joplin/lib/models/Setting';
import { AppState } from '../app.reducer';
const bridge = require('@electron/remote').require('./bridge').default;
class NavigatorComponent extends Component {
UNSAFE_componentWillReceiveProps(newProps) {
interface Props {
route: any;
}
class NavigatorComponent extends React.Component<Props> {
UNSAFE_componentWillReceiveProps(newProps: Props) {
if (newProps.route) {
const screenInfo = this.props.screens[newProps.route.routeName];
const devMarker = Setting.value('env') === 'dev' ? ` (DEV - ${Setting.value('profileDir')})` : '';
@@ -17,7 +21,7 @@ class NavigatorComponent extends Component {
}
}
updateWindowTitle(title) {
updateWindowTitle(title: string) {
try {
if (bridge().window()) bridge().window().setTitle(title);
} catch (error) {
@@ -46,10 +50,10 @@ class NavigatorComponent extends Component {
}
}
const Navigator = connect(state => {
const Navigator = connect((state: AppState) => {
return {
route: state.route,
};
})(NavigatorComponent);
module.exports = { Navigator };
export default Navigator;

View File

@@ -31,6 +31,7 @@ import dialogs from '../../../dialogs';
import convertToScreenCoordinates from '../../../utils/convertToScreenCoordinates';
import { MarkupToHtml } from '@joplin/renderer';
const { clipboard } = require('electron');
const debounce = require('debounce');
const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
@@ -287,8 +288,17 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const editorCopyText = useCallback(() => {
if (editorRef.current) {
const selections = editorRef.current.getSelections();
if (selections.length > 0) {
// Handle the case when there is a selection - copy the selection to the clipboard
// When there is no selection, the selection array contains an empty string.
if (selections.length > 0 && selections[0]) {
clipboard.writeText(selections[0]);
} else {
// This is the case when there is no selection - copy the current line to the clipboard
const cursor = editorRef.current.getCursor();
const line = editorRef.current.getLine(cursor.line);
clipboard.writeText(line);
}
}
}, []);
@@ -621,7 +631,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
contentMaxWidth: props.contentMaxWidth,
mapsToLine: true,
// Always using useCustomPdfViewer for now, we can add a new setting for it in future if we need to.
useCustomPdfViewer: true,
useCustomPdfViewer: props.useCustomPdfViewer,
noteId: props.noteId,
vendorDir: bridge().vendorDir(),
}));
@@ -673,7 +683,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
}, [renderedBody, webviewReady]);
useEffect(() => {
if (!props.searchMarkers) return;
if (!props.searchMarkers) return () => {};
// If there is a currently active search, it's important to re-search the text as the user
// types. However this is slow for performance so we ONLY want it to happen when there is
@@ -688,11 +698,19 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
webviewRef.current.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
if (editorRef.current) {
const matches = editorRef.current.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options);
// Fixes https://github.com/laurent22/joplin/issues/7565
const debouncedMarkers = debounce(() => {
const matches = editorRef.current.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options);
props.setLocalSearchResultCount(matches);
props.setLocalSearchResultCount(matches);
}, 50);
debouncedMarkers();
return () => {
debouncedMarkers.clear();
};
}
}
return () => {};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.searchMarkers, previousSearchMarkers, props.setLocalSearchResultCount, props.content, previousContent, renderedBody, previousRenderedBody, renderedBody]);
@@ -851,7 +869,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
function renderEditor() {
const matchBracesOptions = Setting.value('editor.autoMatchingBraces') ? { override: true, pairs: '<>()[]{}\'\'""‘’“”()《》「」『』【】〔〕〖〗〘〙〚〛' } : false;
const matchBracesOptions = Setting.value('editor.autoMatchingBraces') ? { override: true, pairs: '()[]{}\'\'""‘’“”()《》「」『』【】〔〕〖〗〘〙〚〛' } : false;
return (
<div style={cellEditorStyle}>
@@ -882,6 +900,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
<div style={cellViewerStyle}>
<NoteTextViewer
ref={webviewRef}
themeId={props.themeId}
viewerStyle={styles.viewer}
onIpcMessage={webview_ipcMessage}
onDomReady={webview_domReady}

View File

@@ -102,7 +102,7 @@ interface LastOnChangeEventInfo {
let loadedCssFiles_: string[] = [];
let loadedJsFiles_: string[] = [];
let dispatchDidUpdateIID_: any = null;
let changeId_: number = 1;
let changeId_ = 1;
const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const [editor, setEditor] = useState(null);

View File

@@ -34,12 +34,12 @@ import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
const { themeStyle } = require('@joplin/lib/theme');
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
const NoteSearchBar = require('../NoteSearchBar.min.js');
import NoteSearchBar from '../NoteSearchBar';
import { reg } from '@joplin/lib/registry';
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';
const bridge = require('@electron/remote').require('./bridge').default;
const NoteRevisionViewer = require('../NoteRevisionViewer.min');
import NoteRevisionViewer from '../NoteRevisionViewer';
const commands = [
require('./commands/showRevisions'),
@@ -424,6 +424,7 @@ function NoteEditor(props: NoteEditorProps) {
fontSize: Setting.value('style.editor.fontSize'),
contentMaxWidth: props.contentMaxWidth,
isSafeMode: props.isSafeMode,
useCustomPdfViewer: props.useCustomPdfViewer,
// We need it to identify the context for which media is rendered.
// It is currently used to remember pdf scroll position for each attacments of each note uniquely.
noteId: props.noteId,
@@ -630,6 +631,7 @@ const mapStateToProps = (state: AppState) => {
], whenClauseContext)[0],
contentMaxWidth: state.settings['style.editor.contentMaxWidth'],
isSafeMode: state.settings.isSafeMode,
useCustomPdfViewer: state.settings.useCustomPdfViewer,
};
};

View File

@@ -2,7 +2,7 @@ import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index'
import { _ } from '@joplin/lib/locale';
import { copyHtmlToClipboard } from './clipboardUtils';
import bridge from '../../../services/bridge';
import { ContextMenuItemType, ContextMenuOptions, ContextMenuItems, resourceInfo, textToDataUri, svgUriToPng } from './contextMenuUtils';
import { ContextMenuItemType, ContextMenuOptions, ContextMenuItems, resourceInfo, textToDataUri, svgUriToPng, svgDimensions } from './contextMenuUtils';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
import Resource from '@joplin/lib/models/Resource';
@@ -106,8 +106,10 @@ export function menuItems(dispatch: Function): ContextMenuItems {
if (!options.filename) {
throw new Error('Filename is needed to save as png');
}
// double dimensions to make sure it's always big enough even on hdpi screens
const [width, height] = svgDimensions(document, options.textToCopy).map((x: number) => x * 2 || undefined);
const dataUri = textToDataUri(options.textToCopy, options.mime);
const png = await svgUriToPng(document, dataUri);
const png = await svgUriToPng(document, dataUri, width, height);
const filename = options.filename.replace('.svg', '.png');
await saveFileData(png, filename);
},

View File

@@ -1,5 +1,6 @@
import Resource from '@joplin/lib/models/Resource';
import Logger from '@joplin/lib/Logger';
const logger = Logger.create('contextMenuUtils');
export enum ContextMenuItemType {
None = '',
Image = 'image',
@@ -40,8 +41,23 @@ export async function resourceInfo(options: ContextMenuOptions) {
export function textToDataUri(text: string, mime: string): string {
return `data:${mime};base64,${Buffer.from(text).toString('base64')}`;
}
export const svgUriToPng = (document: Document, svg: string) => {
export const svgDimensions = (document: Document, svg: string) => {
let width: number;
let height: number;
try {
const parser = new DOMParser();
const id = parser.parseFromString(svg, 'text/html').querySelector('svg').id;
({ width, height } = document.querySelector<HTMLIFrameElement>('.noteTextViewer').contentWindow.document.querySelector(`#${id}`).getBoundingClientRect());
} catch (error) {
logger.warn('Could not get SVG dimensions.');
logger.warn('Error was: ', error);
}
if (!width || !height) {
return [undefined, undefined];
}
return [width, height];
};
export const svgUriToPng = (document: Document, svg: string, width: number, height: number) => {
return new Promise<Uint8Array>((resolve, reject) => {
let canvas: HTMLCanvasElement;
let img: HTMLImageElement;
@@ -63,11 +79,21 @@ export const svgUriToPng = (document: Document, svg: string) => {
try {
canvas = document.createElement('canvas');
if (!canvas) throw new Error('Failed to create canvas element');
canvas.width = img.width;
canvas.height = img.height;
if (!width || !height) {
const maxDimension = 1024;
if (img.width > img.height) {
width = maxDimension;
height = width * (img.height / img.width);
} else {
height = maxDimension;
width = height * (img.width / img.height);
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Failed to get context');
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
const pngUri = canvas.toDataURL('image/png');
if (!pngUri) throw new Error('Failed to generate png uri');
const pngBase64 = pngUri.split(',')[1];

View File

@@ -43,6 +43,7 @@ export interface NoteEditorProps {
richTextBannerDismissed: boolean;
contentMaxWidth: number;
isSafeMode: boolean;
useCustomPdfViewer: boolean;
}
export interface NoteBodyEditorProps {
@@ -76,6 +77,7 @@ export interface NoteBodyEditorProps {
contentMaxWidth: number;
isSafeMode: boolean;
noteId: string;
useCustomPdfViewer: boolean;
}
export interface FormNote {

View File

@@ -13,7 +13,7 @@ import CommandService from '@joplin/lib/services/CommandService';
import shim from '@joplin/lib/shim';
import styled from 'styled-components';
import { themeStyle } from '@joplin/lib/theme';
const { ItemList } = require('../ItemList.min.js');
import ItemList from '../ItemList';
const { connect } = require('react-redux');
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';

View File

@@ -1,18 +1,38 @@
const React = require('react');
const { _ } = require('@joplin/lib/locale');
const { themeStyle } = require('@joplin/lib/theme');
const time = require('@joplin/lib/time').default;
const DialogButtonRow = require('./DialogButtonRow').default;
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import { themeStyle } from '@joplin/lib/theme';
import time from '@joplin/lib/time';
import DialogButtonRow from './DialogButtonRow';
import Note from '@joplin/lib/models/Note';
import bridge from '../services/bridge';
import shim from '@joplin/lib/shim';
import { NoteEntity } from '@joplin/lib/services/database/types';
const Datetime = require('react-datetime').default;
const Note = require('@joplin/lib/models/Note').default;
const formatcoords = require('formatcoords');
const bridge = require('@electron/remote').require('./bridge').default;
const shim = require('@joplin/lib/shim').default;
const { clipboard } = require('electron');
const formatcoords = require('formatcoords');
class NotePropertiesDialog extends React.Component {
constructor() {
super();
interface Props {
noteId: string;
onClose: Function;
onRevisionLinkClick: Function;
themeId: number;
}
interface State {
editedKey: string;
formNote: any;
editedValue: any;
}
class NotePropertiesDialog extends React.Component<Props, State> {
private okButton: any;
private keyToLabel_: Record<string, string>;
private styleKey_: number;
private styles_: any;
constructor(props: Props) {
super(props);
this.revisionsLink_click = this.revisionsLink_click.bind(this);
this.buttonRow_click = this.buttonRow_click.bind(this);
@@ -37,7 +57,7 @@ class NotePropertiesDialog extends React.Component {
}
componentDidMount() {
this.loadNote(this.props.noteId);
void this.loadNote(this.props.noteId);
}
componentDidUpdate() {
@@ -46,7 +66,7 @@ class NotePropertiesDialog extends React.Component {
}
}
async loadNote(noteId) {
async loadNote(noteId: string) {
if (!noteId) {
this.setState({ formNote: null });
} else {
@@ -56,8 +76,8 @@ class NotePropertiesDialog extends React.Component {
}
}
latLongFromLocation(location) {
const o = {};
latLongFromLocation(location: string) {
const o: any = {};
const l = location.split(',');
if (l.length === 2) {
o.latitude = l[0].trim();
@@ -69,8 +89,8 @@ class NotePropertiesDialog extends React.Component {
return o;
}
noteToFormNote(note) {
const formNote = {};
noteToFormNote(note: NoteEntity) {
const formNote: any = {};
formNote.user_updated_time = time.formatMsToLocal(note.user_updated_time);
formNote.user_created_time = time.formatMsToLocal(note.user_created_time);
@@ -93,7 +113,7 @@ class NotePropertiesDialog extends React.Component {
return formNote;
}
formNoteToNote(formNote) {
formNoteToNote(formNote: any) {
const note = Object.assign({ id: formNote.id }, this.latLongFromLocation(formNote.location));
note.user_created_time = time.formatLocalToMs(formNote.user_created_time);
note.user_updated_time = time.formatLocalToMs(formNote.user_updated_time);
@@ -107,7 +127,7 @@ class NotePropertiesDialog extends React.Component {
return note;
}
styles(themeId) {
styles(themeId: number) {
const styleKey = themeId;
if (styleKey === this.styleKey_) return this.styles_;
@@ -148,7 +168,7 @@ class NotePropertiesDialog extends React.Component {
return this.styles_;
}
async closeDialog(applyChanges) {
async closeDialog(applyChanges: boolean) {
if (applyChanges) {
await this.saveProperty();
const note = this.formNoteToNote(this.state.formNote);
@@ -163,26 +183,26 @@ class NotePropertiesDialog extends React.Component {
}
}
buttonRow_click(event) {
this.closeDialog(event.buttonName === 'ok');
buttonRow_click(event: any) {
void this.closeDialog(event.buttonName === 'ok');
}
revisionsLink_click() {
this.closeDialog(false);
void this.closeDialog(false);
if (this.props.onRevisionLinkClick) this.props.onRevisionLinkClick();
}
editPropertyButtonClick(key, initialValue) {
editPropertyButtonClick(key: string, initialValue: any) {
this.setState({
editedKey: key,
editedValue: initialValue,
});
shim.setTimeout(() => {
if (this.refs.editField.openCalendar) {
this.refs.editField.openCalendar();
if ((this.refs.editField as any).openCalendar) {
(this.refs.editField as any).openCalendar();
} else {
this.refs.editField.focus();
(this.refs.editField as any).focus();
}
}, 100);
}
@@ -190,7 +210,7 @@ class NotePropertiesDialog extends React.Component {
async saveProperty() {
if (!this.state.editedKey) return;
return new Promise((resolve) => {
return new Promise((resolve: Function) => {
const newFormNote = Object.assign({}, this.state.formNote);
if (this.state.editedKey.indexOf('_time') >= 0) {
@@ -214,7 +234,7 @@ class NotePropertiesDialog extends React.Component {
}
async cancelProperty() {
return new Promise((resolve) => {
return new Promise((resolve: Function) => {
this.okButton.current.focus();
this.setState({
editedKey: null,
@@ -225,7 +245,7 @@ class NotePropertiesDialog extends React.Component {
});
}
createNoteField(key, value) {
createNoteField(key: string, value: any) {
const styles = this.styles(this.props.themeId);
const theme = themeStyle(this.props.themeId);
const labelComp = <label style={Object.assign({}, theme.textStyle, theme.controlBoxLabel)}>{this.formatLabel(key)}</label>;
@@ -234,11 +254,11 @@ class NotePropertiesDialog extends React.Component {
let editCompHandler = null;
let editCompIcon = null;
const onKeyDown = event => {
const onKeyDown = (event: any) => {
if (event.keyCode === 13) {
this.saveProperty();
void this.saveProperty();
} else if (event.keyCode === 27) {
this.cancelProperty();
void this.cancelProperty();
}
};
@@ -251,17 +271,17 @@ class NotePropertiesDialog extends React.Component {
dateFormat={time.dateFormat()}
timeFormat={time.timeFormat()}
inputProps={{
onKeyDown: event => onKeyDown(event, key),
onKeyDown: (event: any) => onKeyDown(event),
style: styles.input,
}}
onChange={momentObject => {
onChange={(momentObject: any) => {
this.setState({ editedValue: momentObject });
}}
/>
);
editCompHandler = () => {
this.saveProperty();
void this.saveProperty();
};
editCompIcon = 'fa-save';
} else {
@@ -344,12 +364,12 @@ class NotePropertiesDialog extends React.Component {
);
}
formatLabel(key) {
formatLabel(key: string) {
if (this.keyToLabel_[key]) return this.keyToLabel_[key];
return key;
}
formatValue(key, note) {
formatValue(key: string, note: NoteEntity) {
if (key === 'location') {
if (!Number(note.latitude) && !Number(note.longitude)) return null;
const dms = formatcoords(Number(note.latitude), Number(note.longitude));
@@ -357,10 +377,10 @@ class NotePropertiesDialog extends React.Component {
}
if (['user_updated_time', 'user_created_time', 'todo_completed'].indexOf(key) >= 0) {
return time.formatMsToLocal(note[key]);
return time.formatMsToLocal((note as any)[key]);
}
return note[key];
return (note as any)[key];
}
render() {
@@ -389,4 +409,4 @@ class NotePropertiesDialog extends React.Component {
}
}
module.exports = NotePropertiesDialog;
export default NotePropertiesDialog;

View File

@@ -1,25 +1,45 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
const { _ } = require('@joplin/lib/locale');
const NoteTextViewer = require('./NoteTextViewer').default;
const HelpButton = require('./HelpButton.min');
const BaseModel = require('@joplin/lib/BaseModel').default;
const Revision = require('@joplin/lib/models/Revision').default;
import * as React from 'react';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import NoteTextViewer from './NoteTextViewer';
import HelpButton from './HelpButton';
import BaseModel from '@joplin/lib/BaseModel';
import Revision from '@joplin/lib/models/Revision';
import Setting from '@joplin/lib/models/Setting';
import RevisionService from '@joplin/lib/services/RevisionService';
import { MarkupToHtml } from '@joplin/renderer';
import time from '@joplin/lib/time';
import bridge from '../services/bridge';
import markupLanguageUtils from '../utils/markupLanguageUtils';
import { NoteEntity, RevisionEntity } from '@joplin/lib/services/database/types';
import { AppState } from '../app.reducer';
const urlUtils = require('@joplin/lib/urlUtils');
const Setting = require('@joplin/lib/models/Setting').default;
const RevisionService = require('@joplin/lib/services/RevisionService').default;
const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
const { MarkupToHtml } = require('@joplin/renderer');
const time = require('@joplin/lib/time').default;
const ReactTooltip = require('react-tooltip');
const { urlDecode } = require('@joplin/lib/string-utils');
const bridge = require('@electron/remote').require('./bridge').default;
const markupLanguageUtils = require('../utils/markupLanguageUtils').default;
const { connect } = require('react-redux');
const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
class NoteRevisionViewerComponent extends React.PureComponent {
constructor() {
super();
interface Props {
themeId: number;
noteId: string;
onBack: Function;
customCss: string;
}
interface State {
note: NoteEntity;
revisions: RevisionEntity[];
currentRevId: string;
restoring: boolean;
}
class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
private viewerRef_: any;
private helpButton_onClick: Function;
constructor(props: Props) {
super(props);
this.state = {
revisions: [],
@@ -65,7 +85,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
currentRevId: revisions.length ? revisions[revisions.length - 1].id : '',
},
() => {
this.reloadNote();
void this.reloadNote();
}
);
}
@@ -82,7 +102,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
if (this.props.onBack) this.props.onBack();
}
revisionList_onChange(event) {
revisionList_onChange(event: any) {
const value = event.target.value;
if (!value) {
@@ -93,7 +113,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
currentRevId: value,
},
() => {
this.reloadNote();
void this.reloadNote();
}
);
}
@@ -128,12 +148,12 @@ class NoteRevisionViewerComponent extends React.PureComponent {
});
this.viewerRef_.current.send('setHtml', result.html, {
cssFiles: result.cssFiles,
// cssFiles: result.cssFiles,
pluginAssets: result.pluginAssets,
});
}
async webview_ipcMessage(event) {
async webview_ipcMessage(event: any) {
// For the revision view, we only suppport a minimal subset of the IPC messages.
// For example, we don't need interactive checkboxes or sync between viewer and editor view.
// We try to get most links work though, except for internal (joplin://) links.
@@ -148,9 +168,9 @@ class NoteRevisionViewerComponent extends React.PureComponent {
throw new Error(_('Unsupported link or message: %s', msg));
} else if (urlUtils.urlProtocol(msg)) {
if (msg.indexOf('file://') === 0) {
require('electron').shell.openExternal(urlDecode(msg));
void require('electron').shell.openExternal(urlDecode(msg));
} else {
require('electron').shell.openExternal(msg);
void require('electron').shell.openExternal(msg);
}
} else if (msg.indexOf('#') === 0) {
// This is an internal anchor, which is handled by the WebView so skip this case
@@ -202,7 +222,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
const viewer = <NoteTextViewer themeId={this.props.themeId} viewerStyle={{ display: 'flex', flex: 1, borderLeft: 'none' }} ref={this.viewerRef_} onDomReady={this.viewer_domReady} onIpcMessage={this.webview_ipcMessage} />;
return (
<div style={style.root}>
<div style={style.root as any}>
{titleInput}
{viewer}
<ReactTooltip place="bottom" delayShow={300} className="help-tooltip" />
@@ -211,7 +231,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
}
}
const mapStateToProps = state => {
const mapStateToProps = (state: AppState) => {
return {
themeId: state.settings.theme,
};
@@ -219,4 +239,4 @@ const mapStateToProps = state => {
const NoteRevisionViewer = connect(mapStateToProps)(NoteRevisionViewerComponent);
module.exports = NoteRevisionViewer;
export default NoteRevisionViewer;

View File

@@ -1,10 +1,27 @@
const React = require('react');
const { themeStyle } = require('@joplin/lib/theme');
const { _ } = require('@joplin/lib/locale');
import * as React from 'react';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
class NoteSearchBar extends React.Component {
constructor() {
super();
interface Props {
themeId: number;
onNext: Function;
onPrevious: Function;
onClose: Function;
onChange: Function;
query: string;
searching: boolean;
resultCount: number;
selectedIndex: number;
visiblePanes: string[];
style: any;
}
class NoteSearchBar extends React.Component<Props> {
private backgroundColor: any;
constructor(props: Props) {
super(props);
this.searchInput_change = this.searchInput_change.bind(this);
this.searchInput_keyDown = this.searchInput_keyDown.bind(this);
@@ -29,7 +46,7 @@ class NoteSearchBar extends React.Component {
return style;
}
buttonIconComponent(iconName, clickHandler, isEnabled) {
buttonIconComponent(iconName: string, clickHandler: any, isEnabled: boolean) {
const theme = themeStyle(this.props.themeId);
const searchButton = {
@@ -57,12 +74,12 @@ class NoteSearchBar extends React.Component {
);
}
searchInput_change(event) {
searchInput_change(event: any) {
const query = event.currentTarget.value;
this.triggerOnChange(query);
}
searchInput_keyDown(event) {
searchInput_keyDown(event: any) {
if (event.keyCode === 13) {
// ENTER
event.preventDefault();
@@ -101,13 +118,13 @@ class NoteSearchBar extends React.Component {
if (this.props.onClose) this.props.onClose();
}
triggerOnChange(query) {
triggerOnChange(query: string) {
if (this.props.onChange) this.props.onChange(query);
}
focus() {
this.refs.searchInput.focus();
this.refs.searchInput.select();
(this.refs.searchInput as any).focus();
(this.refs.searchInput as any).select();
}
render() {
@@ -177,4 +194,4 @@ class NoteSearchBar extends React.Component {
}
}
module.exports = NoteSearchBar;
export default NoteSearchBar;

View File

@@ -1,9 +1,16 @@
const React = require('react');
import * as React from 'react';
import time from '@joplin/lib/time';
import { themeStyle } from '@joplin/lib/theme';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { AppState } from '../app.reducer';
const { connect } = require('react-redux');
const time = require('@joplin/lib/time').default;
const { themeStyle } = require('@joplin/lib/theme');
class NoteStatusBarComponent extends React.Component {
interface Props {
themeId: number;
note: NoteEntity;
}
class NoteStatusBarComponent extends React.Component<Props> {
style() {
const theme = themeStyle(this.props.themeId);
@@ -23,7 +30,7 @@ class NoteStatusBarComponent extends React.Component {
}
}
const mapStateToProps = state => {
const mapStateToProps = (state: AppState) => {
return {
// notes: state.notes,
// folders: state.folders,
@@ -34,4 +41,4 @@ const mapStateToProps = state => {
const NoteStatusBar = connect(mapStateToProps)(NoteStatusBarComponent);
module.exports = { NoteStatusBar };
export default NoteStatusBar;

View File

@@ -6,7 +6,8 @@ interface Props {
onDomReady: Function;
onIpcMessage: Function;
viewerStyle: any;
contentMaxWidth: number;
contentMaxWidth?: number;
themeId: number;
}
export default class NoteTextViewerComponent extends React.Component<Props, any> {

View File

@@ -24,9 +24,9 @@ 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');
import ImportScreen from './ImportScreen';
const { ResourceScreen } = require('./ResourceScreen.js');
const { Navigator } = require('./Navigator.min.js');
import Navigator from './Navigator';
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
const bridge = require('@electron/remote').require('./bridge').default;

View File

@@ -1,7 +1,8 @@
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
const CommandService = require('@joplin/lib/services/CommandService').default;
import { themeStyle } from '@joplin/lib/theme';
import CommandService from '@joplin/lib/services/CommandService';
import { AppState } from '../app.reducer';
class TagItemComponent extends React.Component {
render() {
@@ -13,10 +14,8 @@ class TagItemComponent extends React.Component {
}
}
const mapStateToProps = state => {
const mapStateToProps = (state: AppState) => {
return { themeId: state.settings.theme };
};
const TagItem = connect(mapStateToProps)(TagItemComponent);
module.exports = TagItem;
export default connect(mapStateToProps)(TagItemComponent);

View File

@@ -1,10 +1,10 @@
import * as React from 'react';
import { useMemo } from 'react';
import { AppState } from '../app.reducer';
import TagItem from './TagItem';
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
const TagItem = require('./TagItem.min.js');
interface Props {
themeId: number;

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import ToolbarButton from './ToolbarButton/ToolbarButton';
import ToggleEditorsButton, { Value } from './ToggleEditorsButton/ToggleEditorsButton';
const React = require('react');
import ToolbarSpace from './ToolbarSpace';
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
const ToolbarSpace = require('./ToolbarSpace.min.js');
interface Props {
themeId: number;

View File

@@ -1,14 +0,0 @@
const React = require('react');
const { themeStyle } = require('@joplin/lib/theme');
class ToolbarSpace extends React.Component {
render() {
const theme = themeStyle(this.props.themeId);
const style = Object.assign({}, theme.toolbarStyle);
style.minWidth = style.height / 2;
return <span style={style}></span>;
}
}
module.exports = ToolbarSpace;

View File

@@ -0,0 +1,18 @@
import * as React from 'react';
import { themeStyle } from '@joplin/lib/theme';
interface Props {
themeId: number;
}
class ToolbarSpace extends React.Component<Props> {
render() {
const theme = themeStyle(this.props.themeId);
const style = Object.assign({}, theme.toolbarStyle);
style.minWidth = style.height / 2;
return <span style={style}></span>;
}
}
export default ToolbarSpace;

View File

@@ -1,100 +0,0 @@
const React = require('react');
const bridge = require('@electron/remote').require('./bridge').default;
class VerticalResizer extends React.PureComponent {
constructor() {
super();
this.state = {
parentRight: 0,
parentHeight: 0,
parentWidth: 0,
drag: {
startX: 0,
lastX: 0,
},
};
this.onDragStart = this.onDragStart.bind(this);
this.onDrag = this.onDrag.bind(this);
this.onDragEnd = this.onDragEnd.bind(this);
this.document_onDragOver = this.document_onDragOver.bind(this);
}
document_onDragOver(event) {
// This is just to prevent the cursor from changing to a "+" as it's dragged
// over other elements. With this it stays a normal cursor.
event.dataTransfer.dropEffect = 'none';
}
onDragStart(event) {
document.addEventListener('dragover', this.document_onDragOver);
event.dataTransfer.dropEffect = 'none';
const cursor = bridge().screen().getCursorScreenPoint();
this.setState({
drag: {
startX: cursor.x,
lastX: cursor.x,
},
});
if (this.props.onDragStart) this.props.onDragStart({});
}
onDrag() {
// If we got a drag event with no buttons pressed, it's the last drag event
// that we should ignore, because it's sometimes use to put the dragged element
// back to its original position (if there was no valid drop target), which we don't want.
// Also if clientX, screenX, etc. are 0, it's also the last event and we want to ignore these buggy values.
// const e = event.nativeEvent;
// if (!e.buttons || (!e.clientX && !e.clientY && !e.screenX && !e.screenY)) return;
const cursor = bridge().screen().getCursorScreenPoint();
const newX = cursor.x;
const delta = newX - this.state.drag.lastX;
if (!delta) return;
this.setState(
{
drag: Object.assign({}, this.state.drag, { lastX: newX }),
},
() => {
this.props.onDrag({ deltaX: delta });
}
);
}
onDragEnd() {
document.removeEventListener('dragover', this.document_onDragOver);
}
componentWillUnmount() {
document.removeEventListener('dragover', this.document_onDragOver);
}
render() {
const debug = false;
const rootStyle = Object.assign(
{},
{
height: '100%',
width: 5,
borderColor: 'red',
borderWidth: debug ? 1 : 0,
borderStyle: 'solid',
cursor: 'col-resize',
boxSizing: 'border-box',
opacity: 0,
},
this.props.style
);
return <div style={rootStyle} draggable={true} onDragStart={this.onDragStart} onDrag={this.onDrag} onDragEnd={this.onDragEnd} />;
}
}
module.exports = VerticalResizer;

View File

@@ -137,8 +137,14 @@
// calculates viewer's GUI-dependent pixel-based raw percent
const viewerPercent = scrollmap.translateL2V(percent);
const newScrollTop = viewerPercent * maxScrollTop();
// Even if the scroll position hasn't changed (percent is the same),
// we still ignore the next scroll event, so that it doesn't create
// undesired side effects.
// https://github.com/laurent22/joplin/issues/7617
ignoreNextScrollEvent();
if (Math.floor(contentElement.scrollTop) !== Math.floor(newScrollTop)) {
ignoreNextScrollEvent();
percentScroll_ = percent;
contentElement.scrollTop = newScrollTop;
}

View File

@@ -1,49 +1,44 @@
'use strict';
const { createSelector } = require('reselect');
const { themeStyle } = require('@joplin/lib/theme');
const themeSelector = (state, props) => themeStyle(props.themeId);
const style = createSelector(
themeSelector,
(theme) => {
const output = {
root: {
width: 220,
height: 60,
borderRadius: 4,
border: '1px solid',
borderColor: theme.dividerColor,
backgroundColor: theme.backgroundColor,
paddingLeft: 14,
paddingRight: 14,
paddingTop: 8,
paddingBottom: 8,
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'row',
boxShadow: '0px 1px 1px rgba(0,0,0,0.3)',
},
logo: {
width: 42,
height: 42,
},
labelGroup: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
marginLeft: 14,
fontFamily: theme.fontFamily,
color: theme.color,
fontSize: theme.fontSize,
},
locationLabel: {
fontSize: theme.fontSize * 1.2,
fontWeight: 'bold',
},
};
return output;
}
);
const style = createSelector(themeSelector, (theme) => {
const output = {
root: {
width: 220,
height: 60,
borderRadius: 4,
border: '1px solid',
borderColor: theme.dividerColor,
backgroundColor: theme.backgroundColor,
paddingLeft: 14,
paddingRight: 14,
paddingTop: 8,
paddingBottom: 8,
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'row',
boxShadow: '0px 1px 1px rgba(0,0,0,0.3)',
},
logo: {
width: 42,
height: 42,
},
labelGroup: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
marginLeft: 14,
fontFamily: theme.fontFamily,
color: theme.color,
fontSize: theme.fontSize,
},
locationLabel: {
fontSize: theme.fontSize * 1.2,
fontWeight: 'bold',
},
};
return output;
});
module.exports = style;
// # sourceMappingURL=ExtensionBadge.js.map

View File

@@ -21,6 +21,9 @@ const tasks = {
electronRebuild: {
fn: require('./tools/electronRebuild.js'),
},
electronBuilder: {
fn: require('./tools/electronBuilder.js'),
},
tsc: require('@joplin/tools/gulp/tasks/tsc'),
updateIgnoredTypeScriptBuild: require('@joplin/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
buildCommandIndex: require('@joplin/tools/gulp/tasks/buildCommandIndex'),

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.10.4",
"version": "2.10.5",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -8,6 +8,7 @@
"dist": "yarn run electronRebuild && npx electron-builder",
"build": "gulp build",
"postinstall": "yarn run build",
"electronBuilder": "gulp electronBuilder",
"electronRebuild": "gulp electronRebuild",
"tsc": "tsc --project tsconfig.json",
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
@@ -108,18 +109,16 @@
"devDependencies": {
"@joplin/tools": "~2.10",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.2.5",
"@types/jest": "29.2.6",
"@types/node": "18.11.18",
"@types/react": "16.14.34",
"@types/react": "16.14.35",
"@types/react-redux": "7.1.25",
"@types/styled-components": "5.1.26",
"babel-cli": "6.26.0",
"babel-preset-react": "6.24.1",
"electron": "19.1.4",
"electron-builder": "23.6.0",
"electron-notarize": "1.2.2",
"electron-rebuild": "3.2.9",
"glob": "8.0.3",
"glob": "8.1.0",
"gulp": "4.0.2",
"jest": "29.3.1",
"jest-environment-jsdom": "29.3.1",

View File

@@ -3,18 +3,17 @@ import { AppState } from '../app.reducer';
import CommandService, { SearchResult as CommandSearchResult } from '@joplin/lib/services/CommandService';
import KeymapService from '@joplin/lib/services/KeymapService';
import shim from '@joplin/lib/shim';
const { connect } = require('react-redux');
const { _ } = require('@joplin/lib/locale');
const { themeStyle } = require('@joplin/lib/theme');
import { _ } from '@joplin/lib/locale';
import { themeStyle } from '@joplin/lib/theme';
import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
import gotoAnythingStyleQuery from '@joplin/lib/services/searchengine/gotoAnythingStyleQuery';
import BaseModel from '@joplin/lib/BaseModel';
import Tag from '@joplin/lib/models/Tag';
import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
const { ItemList } = require('../gui/ItemList.min');
const HelpButton = require('../gui/HelpButton.min');
import ItemList from '../gui/ItemList';
import HelpButton from '../gui/HelpButton';
const { surroundKeywords, nextWhitespaceIndex, removeDiacritics } = require('@joplin/lib/string-utils.js');
import { mergeOverlappingIntervals } from '@joplin/lib/ArrayUtils';
import markupLanguageUtils from '../utils/markupLanguageUtils';

View File

@@ -22,6 +22,8 @@
# ./runForTesting.sh 1 createUsers,createData,reset,sync && ./runForTesting.sh 2 reset,sync && ./runForTesting.sh 1
# ./runForTesting.sh 1 createUsers,createData,reset,sync && ./runForTesting.sh 2 reset,sync && ./runForTesting.sh 3 reset,sync && ./runForTesting.sh 1
# ----------------------------------------------------------------------------------
# To create two client profiles, in sync, both used by the same user:
# ----------------------------------------------------------------------------------

View File

@@ -1,61 +1,8 @@
const fs = require('fs-extra');
const spawnSync = require('child_process').spawnSync;
const { chdir } = require('process');
const basePath = `${__dirname}/../../..`;
function fileIsNewerThan(path1, path2) {
if (!fs.existsSync(path2)) return true;
const stat1 = fs.statSync(path1);
const stat2 = fs.statSync(path2);
return stat1.mtime > stat2.mtime;
}
function convertJsx(paths) {
chdir(`${__dirname}/..`);
paths.forEach(path => {
fs.readdirSync(path).forEach((filename) => {
const jsxPath = `${path}/${filename}`;
const p = jsxPath.split('.');
if (p.length <= 1) return;
const ext = p[p.length - 1];
if (ext !== 'jsx') return;
p.pop();
const basePath = p.join('.');
const jsPath = `${basePath}.min.js`;
if (fileIsNewerThan(jsxPath, jsPath)) {
console.info(`Compiling ${jsxPath}...`);
// { shell: true } is needed to get it working on Windows:
// https://discourse.joplinapp.org/t/attempting-to-build-on-windows/22559/12
const result = spawnSync('yarn', ['run', 'babel', '--presets', 'react', '--out-file', jsPath, jsxPath], { shell: true });
if (result.status !== 0) {
const msg = [];
if (result.stdout) msg.push(result.stdout.toString());
if (result.stderr) msg.push(result.stderr.toString());
console.error(msg.join('\n'));
if (result.error) console.error(result.error);
process.exit(result.status);
}
}
});
});
}
module.exports = function() {
convertJsx([
`${__dirname}/../gui`,
`${__dirname}/../gui/MainScreen`,
`${__dirname}/../gui/NoteList`,
`${__dirname}/../plugins`,
]);
const libContent = [
fs.readFileSync(`${basePath}/packages/lib/string-utils-common.js`, 'utf8'),
fs.readFileSync(`${basePath}/packages/lib/markJsUtils.js`, 'utf8'),

View File

@@ -0,0 +1,31 @@
// Note: this is not working because electron-builder needs access to env
// variables which are not defined in this context. To make it work, we'll need
// to somehow pass this to the execCommand call.
const execCommand = require('./execCommand');
async function main() {
process.chdir(`${__dirname}/..`);
const maxTries = 3;
for (let i = 0; i < maxTries; i++) {
try {
console.info(await execCommand(['yarn', 'run', 'electron-builder'].join(' ')));
console.info('electronBuilder: electron-builder completed successfully');
break;
} catch (error) {
console.info(error.stdout);
console.error(error);
if (error.stdout.includes('cannot resolve') && i !== maxTries - 1) {
console.info(`electronBuilder: electron-builder could not download an asset - trying again (${i + 1})`);
continue;
} else {
throw error;
}
}
}
}
module.exports = main;

View File

@@ -1,22 +1,4 @@
const execCommand = function(command) {
const exec = require('child_process').exec;
console.info(`Running: ${command}`);
return new Promise((resolve, reject) => {
exec(command, (error, stdout) => {
if (error) {
if (error.signal === 'SIGTERM') {
resolve('Process was killed');
} else {
reject(error);
}
} else {
resolve(stdout.trim());
}
});
});
};
const execCommand = require('./execCommand');
const isWindows = () => {
return process && process.platform === 'win32';

View File

@@ -0,0 +1,22 @@
const execCommand = (command) => {
const exec = require('child_process').exec;
console.info(`Running: ${command}`);
return new Promise((resolve, reject) => {
exec(command, (error, stdout) => {
if (error) {
if (error.signal === 'SIGTERM') {
resolve('Process was killed');
} else {
error.stdout = stdout;
reject(error);
}
} else {
resolve(stdout.trim());
}
});
});
};
module.exports = execCommand;

View File

@@ -1,24 +1,7 @@
const fs = require('fs');
const path = require('path');
const electron_notarize = require('electron-notarize');
function execCommand(command) {
const exec = require('child_process').exec;
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
if (error.signal === 'SIGTERM') {
resolve('Process was killed');
} else {
reject(new Error([stdout.trim(), stderr.trim()].join('\n')));
}
} else {
resolve([stdout.trim(), stderr.trim()].join('\n'));
}
});
});
}
const execCommand = require('./execCommand');
function isDesktopAppTag(tagName) {
if (!tagName) return false;

View File

@@ -150,8 +150,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097680
versionName "2.10.4"
versionCode 2097681
versionName "2.10.5"
// ndk {
// abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
// }

View File

@@ -65,41 +65,41 @@ describe('markdownCommands.toggleList', () => {
});
it('should correctly replace an unordered list with a checklist', async () => {
const editor = await createEditor(
unorderedListText,
EditorSelection.cursor(unorderedListText.length),
['BulletList']
);
// it('should correctly replace an unordered list with a checklist', async () => {
// const editor = await createEditor(
// unorderedListText,
// EditorSelection.cursor(unorderedListText.length),
// ['BulletList']
// );
toggleList(ListType.CheckList)(editor);
expect(editor.state.doc.toString()).toBe(
'- [ ] 1\n- [ ] 2\n- [ ] 3\n- [ ] 4\n- [ ] 5\n- [ ] 6\n- [ ] 7'
);
});
// toggleList(ListType.CheckList)(editor);
// expect(editor.state.doc.toString()).toBe(
// '- [ ] 1\n- [ ] 2\n- [ ] 3\n- [ ] 4\n- [ ] 5\n- [ ] 6\n- [ ] 7'
// );
// });
it('should properly toggle a sublist of a bulleted list', async () => {
const preSubListText = '# List test\n * This\n * is\n';
const initialDocText = `${preSubListText}\t* a\n\t* test\n * of list toggling`;
// it('should properly toggle a sublist of a bulleted list', async () => {
// const preSubListText = '# List test\n * This\n * is\n';
// const initialDocText = `${preSubListText}\t* a\n\t* test\n * of list toggling`;
const editor = await createEditor(
initialDocText,
EditorSelection.cursor(preSubListText.length + '\t* a'.length),
['BulletList', 'ATXHeading1']
);
// const editor = await createEditor(
// initialDocText,
// EditorSelection.cursor(preSubListText.length + '\t* a'.length),
// ['BulletList', 'ATXHeading1']
// );
// Indentation should be preserved when changing list types
toggleList(ListType.OrderedList)(editor);
expect(editor.state.doc.toString()).toBe(
'# List test\n * This\n * is\n\t1. a\n\t2. test\n * of list toggling'
);
// // Indentation should be preserved when changing list types
// toggleList(ListType.OrderedList)(editor);
// expect(editor.state.doc.toString()).toBe(
// '# List test\n * This\n * is\n\t1. a\n\t2. test\n * of list toggling'
// );
// The changed region should be selected
expect(editor.state.selection.main.from).toBe(preSubListText.length);
expect(editor.state.selection.main.to).toBe(
`${preSubListText}\t1. a\n\t2. test`.length
);
});
// // The changed region should be selected
// expect(editor.state.selection.main.from).toBe(preSubListText.length);
// expect(editor.state.selection.main.to).toBe(
// `${preSubListText}\t1. a\n\t2. test`.length
// );
// });
it('should not preserve indentation when removing sublists', async () => {
const preSubListText = '# List test\n * This\n * is\n';

View File

@@ -517,13 +517,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 85;
CURRENT_PROJECT_VERSION = 86;
DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.10.1;
MARKETING_VERSION = 12.10.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -546,12 +546,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 85;
CURRENT_PROJECT_VERSION = 86;
DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.10.1;
MARKETING_VERSION = 12.10.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -696,14 +696,14 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 85;
CURRENT_PROJECT_VERSION = 86;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.10.1;
MARKETING_VERSION = 12.10.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
@@ -727,14 +727,14 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 85;
CURRENT_PROJECT_VERSION = 86;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.10.1;
MARKETING_VERSION = 12.10.2;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -306,7 +306,7 @@ PODS:
- React-jsinspector (0.70.6)
- React-logger (0.70.6):
- glog
- react-native-alarm-notification (1.0.7):
- react-native-alarm-notification (2.10.0):
- React
- react-native-camera (4.2.1):
- React-Core
@@ -490,7 +490,7 @@ DEPENDENCIES:
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- react-native-alarm-notification (from `../node_modules/joplin-rn-alarm-notification`)
- "react-native-alarm-notification (from `../node_modules/@joplin/react-native-alarm-notification`)"
- react-native-camera (from `../node_modules/react-native-camera`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
@@ -597,7 +597,7 @@ EXTERNAL SOURCES:
React-logger:
:path: "../node_modules/react-native/ReactCommon/logger"
react-native-alarm-notification:
:path: "../node_modules/joplin-rn-alarm-notification"
:path: "../node_modules/@joplin/react-native-alarm-notification"
react-native-camera:
:path: "../node_modules/react-native-camera"
react-native-document-picker:
@@ -714,7 +714,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: b4a65947391c658450151275aa406f2b8263178f
React-jsinspector: 60769e5a0a6d4b32294a2456077f59d0266f9a8b
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
react-native-alarm-notification: 4e150e89c1707e057bc5e8c87ab005f1ea4b8d52
react-native-alarm-notification: 0f58eaa37a4188480536fd7ab62db9b1dfba392f
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe

View File

@@ -16,6 +16,21 @@
const path = require('path');
const localPackages = {
'@joplin/lib': path.resolve(__dirname, '../lib/'),
'@joplin/renderer': path.resolve(__dirname, '../renderer/'),
'@joplin/tools': path.resolve(__dirname, '../tools/'),
'@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
'@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
'@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
'@joplin/react-native-alarm-notification': path.resolve(__dirname, '../react-native-alarm-notification/'),
};
const watchedFolders = [];
for (const [, v] of Object.entries(localPackages)) {
watchedFolders.push(v);
}
module.exports = {
transformer: {
getTransformOptions: async () => ({
@@ -26,26 +41,19 @@ module.exports = {
}),
},
resolver: {
// This configuration allows you to build React-Native modules and
// * test them without having to publish the module. Any exports provided
// * by your source should be added to the "target" parameter. Any import
// * not matched by a key in target will have to be located in the embedded
// * app's node_modules directory.
// This configuration allows you to build React-Native modules and test
// them without having to publish the module. Any exports provided by
// your source should be added to the "target" parameter. Any import not
// matched by a key in target will have to be located in the embedded
// app's node_modules directory.
//
extraNodeModules: new Proxy(
// The first argument to the Proxy constructor is passed as
// * "target" to the "get" method below.
// * Put the names of the libraries included in your reusable
// * module as they would be imported when the module is actually used.
// The first argument to the Proxy constructor is passed as "target"
// to the "get" method below. Put the names of the libraries
// included in your reusable module as they would be imported when
// the module is actually used.
//
{
'@joplin/lib': path.resolve(__dirname, '../lib/'),
'@joplin/renderer': path.resolve(__dirname, '../renderer/'),
'@joplin/tools': path.resolve(__dirname, '../tools/'),
'@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
'@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
'@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
},
localPackages,
{
get: (target, name) => {
if (target.hasOwnProperty(name)) {
@@ -57,12 +65,5 @@ module.exports = {
),
},
projectRoot: path.resolve(__dirname),
watchFolders: [
path.resolve(__dirname, '../lib'),
path.resolve(__dirname, '../renderer'),
path.resolve(__dirname, '../tools'),
path.resolve(__dirname, '../fork-htmlparser2'),
path.resolve(__dirname, '../fork-uslug'),
path.resolve(__dirname, '../react-native-saf-x'),
],
watchFolders: watchedFolders,
};

View File

@@ -19,25 +19,25 @@
},
"dependencies": {
"@joplin/lib": "~2.10",
"@joplin/react-native-alarm-notification": "~2.10",
"@joplin/react-native-saf-x": "~2.10",
"@joplin/renderer": "~2.10",
"@react-native-community/clipboard": "1.5.1",
"@react-native-community/datetimepicker": "6.7.1",
"@react-native-community/datetimepicker": "6.7.3",
"@react-native-community/geolocation": "2.1.0",
"@react-native-community/netinfo": "9.3.7",
"@react-native-community/push-notification-ios": "1.10.1",
"@react-native-community/slider": "4.4.0",
"@react-native-community/slider": "4.4.1",
"assert-browserify": "2.0.0",
"buffer": "6.0.3",
"constants-browserify": "1.0.0",
"crypto-browserify": "3.12.0",
"events": "3.3.0",
"joplin-rn-alarm-notification": "1.0.7",
"jsc-android": "241213.1.0",
"lodash": "4.17.21",
"md5": "2.3.0",
"prop-types": "15.8.1",
"punycode": "2.1.1",
"punycode": "2.2.2",
"react": "18.2.0",
"react-native": "0.70.6",
"react-native-action-button": "2.8.5",
@@ -53,7 +53,7 @@
"react-native-image-picker": "4.10.3",
"react-native-image-resizer": "1.4.5",
"react-native-modal-datetime-picker": "14.0.1",
"react-native-paper": "5.1.2",
"react-native-paper": "5.1.4",
"react-native-popup-menu": "0.16.1",
"react-native-quick-actions": "0.3.13",
"react-native-rsa-native": "2.0.5",
@@ -94,7 +94,7 @@
"@joplin/tools": "~2.10",
"@lezer/highlight": "1.1.3",
"@types/fs-extra": "9.0.13",
"@types/jest": "29.2.5",
"@types/jest": "29.2.6",
"@types/react-native": "0.64.19",
"@types/react-redux": "7.1.25",
"babel-plugin-module-resolver": "4.1.0",
@@ -104,11 +104,11 @@
"jest": "29.3.1",
"jest-environment-jsdom": "29.3.1",
"jetifier": "2.0.0",
"jsdom": "20.0.0",
"jsdom": "21.0.0",
"md5-file": "5.0.0",
"metro-react-native-babel-preset": "0.72.3",
"nodemon": "2.0.20",
"ts-jest": "29.0.3",
"ts-jest": "29.0.5",
"ts-loader": "9.4.2",
"ts-node": "10.9.1",
"typescript": "4.9.4",

View File

@@ -1,13 +1,13 @@
import Logger from '@joplin/lib/Logger';
import { Notification } from '@joplin/lib/models/Alarm';
const ReactNativeAN = require('joplin-rn-alarm-notification').default;
const ReactNativeAN = require('@joplin/react-native-alarm-notification').default;
export default class AlarmServiceDriver {
private logger_: Logger;
constructor(logger: Logger) {
public constructor(logger: Logger) {
this.logger_ = logger;
}

View File

@@ -45,16 +45,16 @@
"entities": "2.2.0"
},
"devDependencies": {
"@types/jest": "29.2.5",
"@types/jest": "29.2.6",
"@types/node": "18.11.18",
"@typescript-eslint/eslint-plugin": "5.48.0",
"@typescript-eslint/parser": "5.48.0",
"@typescript-eslint/eslint-plugin": "5.48.2",
"@typescript-eslint/parser": "5.48.2",
"coveralls": "3.1.1",
"eslint": "8.31.0",
"eslint-config-prettier": "8.6.0",
"jest": "29.3.1",
"prettier": "2.8.1",
"ts-jest": "29.0.3",
"prettier": "2.8.3",
"ts-jest": "29.0.5",
"typescript": "4.9.4"
},
"jest": {

View File

@@ -16,7 +16,7 @@
],
"devDependencies": {
"standard": "17.0.0",
"tap": "16.3.2"
"tap": "16.3.4"
},
"gitHead": "eb4b0e64eab40a51b0895d3a40a9d8c3cb7b1b14"
}

View File

@@ -3,7 +3,6 @@ import { _ } from '../../locale';
import BaseItem, { EncryptedItemsStats } from '../../models/BaseItem';
import useAsyncEffect, { AsyncEffectEvent } from '../../hooks/useAsyncEffect';
import { MasterKeyEntity } from '../../services/e2ee/types';
// import time from '../../time';
import { findMasterKeyPassword, getMasterPasswordStatus, masterPasswordIsValid, MasterPasswordStatus } from '../../services/e2ee/utils';
import EncryptionService from '../../services/e2ee/EncryptionService';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';

View File

@@ -792,6 +792,7 @@
"Step 2: Install the extension": "Schritt 2: Erweiterung installieren",
"Stop": "Stopp",
"Stop external editing": "Externe Bearbeitung stoppen",
"Storage space": "Speicherplatz",
"Strikethrough": "Durchstreichen",
"strong text": "fetter Text",
"Submit": "Absenden",

View File

@@ -1,8 +1,12 @@
{
"\"%s\" is missing the required \"%s\" property.": "\"%s\" requiert la propriété \"%s\".",
"%d days": "%d jours",
"%d GB": "%d Go",
"%d GB storage space": "%d Go en espace de stockage",
"%d hour": "%d heure",
"%d hours": "%d heures",
"%d MB": "%d Mo",
"%d MB per note or attachment": "%d Mo pour les notes et pièces jointes",
"%d minutes": "%d minutes",
"%d notes match this pattern. Delete them?": "%d notes correspondent à ce motif. Les supprimer ?",
"%s %s (%s, %s)": "%s %s (%s, %s)",
@@ -58,6 +62,7 @@
"Admin dashboard": "Tableau de bord",
"Advanced options": "Options avancées",
"Advanced tools": "Outils avancés",
"All data, including notes, notebooks and tags will be permanently deleted.": "Toutes vos données, y compris les notes, carnets et étiquettes seront effacés de façon permanente.",
"All notes": "Toutes les notes",
"All potential ports are in use - please report the issue at %s": "Tous les ports sont en cours d'utilisation. Veuillez signaler ce problème sur %s",
"Also displays unset and hidden config variables.": "Afficher également les variables cachées.",
@@ -91,6 +96,7 @@
"Automatically check for updates": "Vérifier automatiquement les mises à jour",
"Automatically switch theme to match system theme": "Changer le thème automatiquement pour correspondre au thème système",
"Back": "Retour",
"Basic": "Basique",
"Bold": "Gras",
"Browse all plugins": "Parcourir les plugins",
"Browse...": "Parcourir…",
@@ -131,6 +137,7 @@
"Click to add tags...": "Cliquer pour ajouter des étiquettes…",
"Client ID: %s": "ID client : %s",
"Close": "Fermer",
"Close dropdown": "Fermer la liste déroulante",
"Close Window": "Fermer la fenêtre",
"Code": "Code",
"Code Block": "Bloc de code",
@@ -154,7 +161,9 @@
"Conflicted: %d": "Conflits : %d",
"Conflicts": "Conflits",
"Conflicts (attachments)": "Conflits (fichiers joints)",
"Consolidated billing": "Facturation consolidée",
"Content provided by %s": "Contenu fourni par %s",
"Continue": "Continuer",
"Convert to note": "Convertir en note",
"Convert to todo": "Convertir en tâche",
"Copy": "Copier",
@@ -172,8 +181,10 @@
"Could not export notes: %s": "Impossible d'exporter les notes : %s",
"Could not install plugin: %s": "Impossible d'installer le module : %s",
"Could not respond to the invitation. Please try again, or check with the notebook owner if they are still sharing it.\n\nThe error was: \"%s\"": "Impossible de répondre à l'invitation. Veuillez réessayer, ou demandez au propriétaire du carnet s'il le partage toujours.\n\nL'erreur était : \"%s\"",
"Could not switch profile: %s": "Impossible de changer de profil : %s",
"Could not upgrade master key: %s": "Impossible de mettre la clef à niveau : %s",
"Could not verify the share status of this notebook - aborting. Please try again when you are connected to the internet.": "Impossible de vérifier l'état du partage du carnet - annulation. Veuillez réessayer lorsque vous serez connecté à internet.",
"Could not verify your identify": "Impossible de vérifier votre identité",
"Create": "Créer",
"Create a notebook": "Créer un carnet",
"Create new profile...": "Créer un nouveau profil...",
@@ -222,9 +233,11 @@
"Delete notebook \"%s\"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.": "Effacer le carnet \"%s\" ?\n\nToutes les notes et sous‑carnets dans ce carnet seront également effacés.",
"Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.": "Effacer le carnet ? Toutes les notes et sous‑carnets dans ce carnet seront également effacés.",
"Delete plugin \"%s\"?": "Supprimer plugin \"%s\" ?",
"Delete profile \"%s\"": "Supprimer profil \"%s\" ?",
"Delete selected notes": "Supprimer les notes sélectionnées",
"Delete these %d notes?": "Supprimer ces %d notes ?",
"Delete this invitation? The recipient will no longer have access to this shared notebook.": "Supprimer cette invitation ? Le destinataire n'aura plus accès au carnet partagé.",
"Delete this profile?": "Supprimer ce profil ?",
"Deleted local items: %d.": "Objets suppr. localement : %d.",
"Deleted remote items: %d.": "Objets distants suppr. : %d.",
"Deletes the given notebook.": "Supprimer le carnet.",
@@ -275,6 +288,7 @@
"Edit link": "Éditer lien",
"Edit note.": "Éditer la note.",
"Edit notebook": "Éditer le carnet",
"Edit profile": "Editer le profil",
"Edit profile configuration...": "Editer la configuration des profils...",
"Editor": "Éditeur",
"Editor font": "Police de l'éditeur",
@@ -294,6 +308,7 @@
"Enable ^sup^ syntax": "Activer la syntaxe ^exposant^",
"Enable abbreviation syntax": "Activer la syntaxe pour abréviations",
"Enable audio player": "Activer lecteur audio",
"Enable biometrics authentication?": "Activer l'authentification biométrique ?",
"Enable deflist syntax": "Activer les listes de définitions",
"Enable encryption": "Activer le chiffrement",
"Enable footnotes": "Activer les notes de bas de page",
@@ -363,7 +378,7 @@
"Focus body": "Curseur sur corps du message",
"Focus title": "Curseur sur le titre",
"Folders": "Carnets",
"For debugging purpose only: export your profile to an external SD card.": "Seulement pour du débogage : exporter votre profile vers une carte SD externe.",
"For debugging purpose only: export your profile to an external SD card.": "Seulement pour du débogage : exporter votre profil vers une carte SD externe.",
"For example \"%s\"": "Par exemple \"%s\"",
"For information on how to customise the shortcuts please visit %s": "Pour personnaliser les raccourcis veuillez consulter la documentation à %s",
"For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:": "Pour plus d'informations sur le chiffrement de bout en bout, ainsi que des conseils pour l'activer, veuillez consulter la documentation :",
@@ -495,6 +510,8 @@
"Make a donation": "Faire un don",
"Manage master password": "Gestion du mot de passe maître",
"Manage master password...": "Gestion du mot de passe maître...",
"Manage multiple users": "Gestion de plusieurs utilisateurs",
"Manage profiles": "Gérer les profils",
"Manage your plugins": "Gérer vos modules",
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.": "Gérer la configuration E2EE (Chiffrement de bout à bout). Les commandes sont `enable`, `disable`, `decrypt` et `status`, `decrypt-file` et `target-status`.",
"Manual": "Manuel",
@@ -508,6 +525,7 @@
"Master password:": "Mot de passe maître :",
"Max concurrent connections": "Nombre maxi de connections simultanées",
"Max Item Size": "Taille max des objets",
"Max note or attachment size": "Taille de note maximum",
"Max Total Size": "Taille max totale",
"Missing keys": "Clefs manquantes",
"Missing Master Keys": "Clefs maître manquantes",
@@ -552,6 +570,7 @@
"Not authentified with %s. Please provide any missing credentials.": "Non‑connecté à %s. Veuillez fournir les identifiants et mots de passe manquants.",
"Not downloaded": "Non téléchargé",
"Not generated": "Non-généré",
"Not now": "Pas maintenant",
"note": "note",
"Note": "Note",
"Note area growth factor": "Facteur de croissance de la zone de la note",
@@ -609,6 +628,7 @@
"Paste": "Coller",
"Path:": "Chemin :",
"PDF File": "Fichier PDF",
"Per user. Minimum of %d users.": "Par utilisateur. Minimum %d utilisateurs.",
"Permission needed": "Permission requise",
"Permission to use camera": "Permission d'utiliser l'appareil photo",
"Please click on \"%s\" to proceed, or set the passwords in the \"%s\" list below.": "Veuillez cliquer sur \"%s\" pour continuer, ou entrez les mots de passe dans la liste \"%s\" ci-dessous.",
@@ -640,13 +660,17 @@
"Previous match": "Résultat précédent",
"Previous versions of this note": "Versions précédentes de cette note",
"Print": "Imprimer",
"Priority support": "Support prioritaire",
"Privacy Policy": "Politique de confidentialité",
"Pro": "Pro",
"Process failed payment subscriptions": "Traiter les inscriptions avec échec de paiement",
"Process oversized accounts": "Traiter les comptes ayant excédé leur limite",
"Process user deletions": "Traiter la suppression d'utilisateurs",
"Profile": "Profil",
"Profile name": "Nom du profil",
"Profile name:": "Nom du profil :",
"Profile Version: %s": "Version du profil : %s",
"Profiles": "Profils",
"Properties": "Propriétés",
"Proxy enabled": "Proxy : activé",
"Proxy timeout (seconds)": "Proxy : timeout",
@@ -729,8 +753,10 @@
"Set the password": "Définir le mot de passe",
"Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s": "Assigner la valeur [value] à la propriété <name> de la <note> donnée. Les valeurs possibles sont :\n\n%s",
"Share": "Partager",
"Share and collaborate on a notebook": "Partager et collaborer sur un carnet",
"Share Notebook": "Partager le carnet",
"Share notebook...": "Partager carnet…",
"Sharing access control": "Contrôle d'accès sur le partage",
"Sharing notebook...": "Partage du carnet…",
"Shortcuts are not available in CLI mode.": "Les raccourcis ne sont pas disponible en mode de ligne de commande.",
"Show advanced": "Montrer options avancées",
@@ -779,6 +805,7 @@
"Step 2: Install the extension": "Étape 2 : Installez le module complémentaire",
"Stop": "Stopper",
"Stop external editing": "Arrêter l'édition externe",
"Storage space": "Espace de stockage",
"Strikethrough": "Barrer",
"strong text": "texte en gras",
"Submit": "Envoyer",
@@ -793,6 +820,7 @@
"Switch to profile %d": "Basculer vers le profil %d",
"Switch to to-do type": "Convertir en tâche",
"Switches to [notebook] - all further operations will happen within this notebook.": "Changer de carnet – toutes les opérations à venir se feront dans ce carnet.",
"Sync as many devices as you want": "Synchronisez autant d'appareils que vous le souhaitez",
"Sync Status": "État synchronisation",
"Sync status (synced items / total items)": "Statut de la synchronisation (objets synchro. / total)",
"Sync target must be upgraded! Run `%s` to proceed.": "La cible de synchronisation doit être mise à jour ! Lancez `%s` pour le faire.",
@@ -818,8 +846,10 @@
"Take photo": "Prendre une photo",
"Task list": "Liste de tâches",
"Tasks": "Tâches",
"Teams": "Teams",
"Text editor command": "Commande de l'éditeur de texte",
"Thank you! Your Joplin Cloud account is now setup and ready to use.": "Merci ! Votre compte Joplin Cloud est maintenant configuré et prêt à être utilisé.",
"The active profile cannot be deleted. Switch to a different profile and try again.": "Le profil actif ne peut pas être supprimé. Changez de profil et réessayez.",
"The app is now going to close. Please relaunch it to complete the process.": "L'application va maintenant se fermer. Veuillez la relancer pour terminer l'opération.",
"The application did not close properly. Would you like to start in safe mode?": "L'application ne s'est pas fermée correctement. Voulez-vous démarrer en mode sans échec ?",
"The application has been authorised - you may now close this browser tab.": "Le logiciel a été autorisé. Vous pouvez maintenant fermer cet onglet.",
@@ -831,6 +861,7 @@
"The default admin password is insecure and has not been changed! [Change it now](%s)": "Le mot de passe admin par défaut n'est pas sécurisé et n'a pas été modifié ! [Changez‑le maintenant](%s)",
"The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.": "La méthode de chiffrage par défaut à été renforcée et il est recommandé de l'appliquer à vos données.",
"The default encryption method has been changed, you should re-encrypt your data.": "La méthode de chiffrage par défaut à été renforcée et il est recommandé de l'appliquer à vos données.",
"The default profile cannot be deleted": "Le profil par défaut ne peut pas être supprimé",
"The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.": "La commande de l'éditeur de texte (peut inclure des options) pour ouvrir et modifier les notes. Si non‑spécifiée, elle sera détectée automatiquement.",
"The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes.": "Le facteur de croissance détermine la façon dont la taille des composants se réduit ou augmente par rapport aux autres composants. Ainsi un composant avec un facteur de 2 prendra deux fois plus de place qu'un composant avec un facteur de 1. Veuillez redémarrer l'application pour appliquer les changements.",
"The following attachments are being watched for changes:": "Les changements sur les éléments suivants sont monitorés :",
@@ -854,6 +885,7 @@
"The web clipper service is not enabled.": "Le service du Web Clipper n'est pas activé.",
"Theme": "Apparence",
"There are currently no notes. Create one by clicking on the (+) button.": "Ce carnet ne contient aucune note. Créez‑en une en appuyant sur le bouton (+).",
"There are unsaved changes.": "Il y a des modifications non enregistrées.",
"There is currently no notebook. Create one by clicking on \"New notebook\".": "Il n'y a pour l'instant aucun carnet. Créez‑en un en cliquant sur \"Nouveau carnet\".",
"There is no data to export.": "Il n'y a pas de données à exporter.",
"There was a [conflict](%s) on the attachment below.\n\n%s": "Il y a eu un [conflit](%s) sur la pièce jointe suivante.\n\n%s",
@@ -887,6 +919,7 @@
"To maximise/minimise the console, press \"tc\".": "Pour maximiser ou minimiser la console, pressez \"tc\".",
"To move from one pane to another, press Tab or Shift+Tab.": "Pour aller d'un volet à l'autre, pressez Tab ou Maj+Tab.",
"To retry decryption of these items. Run `e2ee decrypt --retry-failed-items`": "Exécutez `e2ee decrypt --retry-failed-items` pour tenter à nouveau le déchiffrage",
"To switch the profile, the app is going to close and you will need to restart it.": "Pour changer de profil, l'application va se fermer et vous devrez la redémarrer.",
"To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions": "Pour fonctionner correctement, l'appli a besoin des autorisations suivantes. Veuillez les activer dans les paramètres de votre téléphone, dans le menu Applications > Joplin > Autorisations",
"to-do": "tâche",
"to-do: %s": "tâche : %s",
@@ -905,6 +938,7 @@
"Total Size": "Taille totale",
"Total: %d/%d": "Total : %d/%d",
"Try again": "Réessayer",
"Try it now": "Essayer",
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": "Tapez `help [command]` pour plus d'information sur une commande ; ou tapez `help all` pour l'aide complète.",
"Type `joplin help` for usage information.": "Tapez `Joplin help` pour afficher l'aide.",
"Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.": "Entrez le titre d’une note, ou entrez # suivi du nom d’une étiquette, ou @ suivi du nom d’un carnet. Ou entrez : pour chercher une commande.",
@@ -936,16 +970,19 @@
"Upgrade the sync target to the latest version.": "Mettre à jour la cible de synchronisation.",
"URL": "URL",
"Usage: %s": "Utilisation : %s",
"Use biometrics to secure access to the app": "Utilisez la biométrie pour sécuriser l'accès à l'application",
"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE": "Utilise le format de liste longue. Le format est ID, NOMBRE_DE_NOTES (pour les carnets), DATE, TACHE_TERMINE (pour les tâches), TITRE",
"Use spell checker": "Utiliser le correcteur orthographique",
"Use the arrows and page up/down to scroll the lists and text areas (including this console).": "Utilisez les touches fléchées et page précédente/suivante pour faire défiler les listes et zones de texte (y compris cette console).",
"Use the arrows to move the layout items. Press \"Escape\" to exit.": "Utilisez les flèches pour déplacer les éléments de l'interface. Appuyez sur \"Echap\" pour arrêter.",
"Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.": "Utilisez ceci pour corriger l'index du moteur de recherche en cas de problème. Cela peut prendre longtemps selon le nombre de notes.",
"Use your biometrics to secure access to your application. You can always set it up later in Settings.": "Utilisez vos données biométriques pour sécuriser l'accès à votre application. Vous pouvez toujours le configurer plus tard dans les paramètres.",
"Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.": "Utilisée pour la plupart du texte de l'éditeur Markdown. Par défaut, une police proportionnelle sera utilisée.",
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": "Utilisée lorsque une police à taille fixe est nécessaire pour afficher le texte de façon lisible (par ex. pour les tables, code source, etc.).",
"User deletions": "Suppressions d'utilisateurs",
"Users": "Utilisateurs",
"Valid": "Valide",
"Verify your identity": "Vérifiez Votre Identité",
"View": "Affichage",
"View on map": "Voir sur carte",
"View them now": "Les voir maintenant",

View File

@@ -41,45 +41,45 @@ locales['uk_UA'] = require('./uk_UA.json');
locales['vi'] = require('./vi.json');
locales['zh_CN'] = require('./zh_CN.json');
locales['zh_TW'] = require('./zh_TW.json');
stats['ar'] = {"percentDone":82};
stats['ar'] = {"percentDone":81};
stats['eu'] = {"percentDone":23};
stats['bs_BA'] = {"percentDone":59};
stats['bg_BG'] = {"percentDone":46};
stats['ca'] = {"percentDone":92};
stats['hr_HR'] = {"percentDone":92};
stats['cs_CZ'] = {"percentDone":79};
stats['da_DK'] = {"percentDone":99};
stats['de_DE'] = {"percentDone":99};
stats['bs_BA'] = {"percentDone":58};
stats['bg_BG'] = {"percentDone":45};
stats['ca'] = {"percentDone":90};
stats['hr_HR'] = {"percentDone":90};
stats['cs_CZ'] = {"percentDone":78};
stats['da_DK'] = {"percentDone":97};
stats['de_DE'] = {"percentDone":98};
stats['et_EE'] = {"percentDone":45};
stats['en_GB'] = {"percentDone":100};
stats['en_US'] = {"percentDone":100};
stats['es_ES'] = {"percentDone":91};
stats['es_ES'] = {"percentDone":89};
stats['eo'] = {"percentDone":26};
stats['fi_FI'] = {"percentDone":98};
stats['fr_FR'] = {"percentDone":98};
stats['gl_ES'] = {"percentDone":30};
stats['id_ID'] = {"percentDone":92};
stats['it_IT'] = {"percentDone":80};
stats['hu_HU'] = {"percentDone":80};
stats['nl_BE'] = {"percentDone":81};
stats['nl_NL'] = {"percentDone":91};
stats['nb_NO'] = {"percentDone":91};
stats['fa'] = {"percentDone":57};
stats['pl_PL'] = {"percentDone":92};
stats['pt_BR'] = {"percentDone":91};
stats['pt_PT'] = {"percentDone":75};
stats['ro'] = {"percentDone":52};
stats['sl_SI'] = {"percentDone":83};
stats['sv'] = {"percentDone":99};
stats['th_TH'] = {"percentDone":38};
stats['vi'] = {"percentDone":80};
stats['tr_TR'] = {"percentDone":92};
stats['uk_UA'] = {"percentDone":75};
stats['el_GR'] = {"percentDone":91};
stats['ru_RU'] = {"percentDone":83};
stats['sr_RS'] = {"percentDone":67};
stats['zh_CN'] = {"percentDone":97};
stats['zh_TW'] = {"percentDone":92};
stats['ja_JP'] = {"percentDone":92};
stats['ko'] = {"percentDone":92};
stats['fi_FI'] = {"percentDone":96};
stats['fr_FR'] = {"percentDone":100};
stats['gl_ES'] = {"percentDone":29};
stats['id_ID'] = {"percentDone":90};
stats['it_IT'] = {"percentDone":81};
stats['hu_HU'] = {"percentDone":78};
stats['nl_BE'] = {"percentDone":79};
stats['nl_NL'] = {"percentDone":89};
stats['nb_NO'] = {"percentDone":89};
stats['fa'] = {"percentDone":56};
stats['pl_PL'] = {"percentDone":90};
stats['pt_BR'] = {"percentDone":89};
stats['pt_PT'] = {"percentDone":73};
stats['ro'] = {"percentDone":51};
stats['sl_SI'] = {"percentDone":81};
stats['sv'] = {"percentDone":97};
stats['th_TH'] = {"percentDone":37};
stats['vi'] = {"percentDone":79};
stats['tr_TR'] = {"percentDone":90};
stats['uk_UA'] = {"percentDone":73};
stats['el_GR'] = {"percentDone":89};
stats['ru_RU'] = {"percentDone":81};
stats['sr_RS'] = {"percentDone":66};
stats['zh_CN'] = {"percentDone":95};
stats['zh_TW'] = {"percentDone":90};
stats['ja_JP'] = {"percentDone":90};
stats['ko'] = {"percentDone":90};
module.exports = { locales: locales, stats: stats };

View File

@@ -1,8 +1,12 @@
{
"\"%s\" is missing the required \"%s\" property.": "A \"% s\" manca la proprietà \"% s\" richiesta.",
"%d days": "%d giorni",
"%d GB": "%d GB",
"%d GB storage space": "%d GB di spazio di archiviazione",
"%d hour": "%d ora",
"%d hours": "%d ore",
"%d MB": "%d MB",
"%d MB per note or attachment": "%d MB per nota o allegato",
"%d minutes": "%d minuti",
"%d notes match this pattern. Delete them?": "%d note corrispondono. Eliminarle?",
"%s %s (%s, %s)": "%s %s (%s, %s)",
@@ -27,6 +31,8 @@
"&Tools": "&Strumenti",
"&View": "&Vista",
"(%s)": "(%s)",
"(In plugin: %s)": "(Nel plugin: %s)",
"(None)": "(Nessuno)",
"(wysiwyg: %s)": "(wysiwyg: %s)",
"- Camera: to allow taking a picture and attaching it to a note.": "- Fotocamera: per consentire di scattare una foto e allegarla a una nota.",
"- Location: to allow attaching geo-location information to a note.": "- Posizione: per consentire il collegamento di informazioni sulla posizione geografica ad una nota.",
@@ -41,20 +47,28 @@
"Accelerator \"%s\" is not valid.": "L'accelerator \"%s\" non è valido.",
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to unexpected behaviour.": "Accelerator \"%s\" è utilizzato per i comandi \"%s\" e \"%s\". Questo potrebbe portare a comportamenti inaspettati.",
"Accept": "Accetta",
"Account": "Account",
"Action": "Azione",
"Actions": "Azioni",
"Active": "Attivo",
"Actual Size": "Dimensione attuale",
"Add body": "Aggiungi corpo",
"Add new": "Aggiungi nuova",
"Add or remove tags:": "Aggiungi o rimuovi etichetta:",
"Add recipient:": "Aggiungi destinatario:",
"Add title": "Aggiungi titolo",
"Add to dictionary": "Aggiungi al dizionario",
"Admin": "Admin",
"Admin dashboard": "Pannello di controllo admin",
"Advanced options": "Opzioni avanzate",
"Advanced tools": "Strumenti avanzati",
"All notes": "Tutte le note",
"All potential ports are in use - please report the issue at %s": "Tutte le potenziali porte sono in uso - per favore riportare il problema a %s",
"Also displays unset and hidden config variables.": "Mostra anche le variabili di configurazione non impostate o nascoste.",
"Also publish linked notes": "Pubblica anche le note collegate",
"Always": "Sempre",
"Ambiguous notebook \"%s\". Please use notebook id instead - press \"ti\" to see the short notebook id or use $b for current selected notebook": "Taccuino ambiguo \"%s\". Per favore usa l'ID del taccuino - premi \"ti\" per cercare l'ID breve del taccuino o usa $b per il taccuino corrente",
"Ambiguous notebook \"%s\". Please use short notebook id instead - press \"ti\" to see the short notebook id": "Taccuino ambiguo \"%s\". Per favore usa l'ID breve del taccuino - premi \"ti\" per cercare l'ID breve del taccuino",
"An update is available, do you want to download it now?": "È disponibile un aggiornamento, vuoi scaricarlo ora?",
"Appearance": "Aspetto",
"Application": "Applicazione",
@@ -62,10 +76,12 @@
"Are you sure you want to renew the authorisation token?": "Sei sicuro di voler rinnovare il token di autorizzazione?",
"Arguments:": "Argomenti:",
"Aritim Dark": "Scuro Aritim",
"Attach": "Allega",
"Attach file": "Allega file",
"Attach photo": "Allega foto",
"Attach...": "Allega...",
"Attaches the given file to the note.": "Allega il seguente file alla nota.",
"attachment": "allegato",
"Attachment conflict: \"%s\"": "Conflitto tra gli allegati: \"%s\"",
"Attachment download behaviour": "Comportamento scaricamento allegati",
"Attachments": "Allegati",
@@ -74,13 +90,17 @@
"Authentication was not completed (did not receive an authentication token).": "Autenticazione non completata (non è stato ricevuto alcun token di autenticazione).",
"Authorisation token:": "Token autorizzativo:",
"Auto": "Auto",
"Auto-add disabled accounts for deletion": "Aggiungi automaticamente gli account disabilitati per la cancellazione",
"Auto-pair braces, parenthesis, quotations, etc.": "Auto- accoppia parentesi graffe, parentesi, citazione, ecc.",
"Automatically check for updates": "Controlla aggiornamenti automaticamente",
"Automatically switch theme to match system theme": "Usa il tema di sistema",
"Back": "Indietro",
"Basic": "Base",
"Bold": "Grassetto",
"Browse all plugins": "Sfoglia tutti i plugins",
"Browse...": "Naviga...",
"Bulleted List": "Elenco puntato",
"Can Share": "Può condividere",
"Cancel": "Annulla",
"Cancelling background synchronisation... Please wait.": "Annullamento della sincronizzazione in background... Attendere per favore.",
"Cancelling...": "Annullamento...",
@@ -95,6 +115,10 @@
"Cannot move note to \"%s\" notebook": "Non posso spostare la nota nel Taccuino \"%s\"",
"Cannot move notebook to this location": "Impossibile spostare il Taccuino in questa posizione",
"Cannot refresh token: authentication data is missing. Starting the synchronisation again may fix the problem.": "Non è possibile aggiornare il token. mancano i dati di autenticazione. Ricominciare la sincronizzazione da capo potrebbe risolvere il problema.",
"Cannot save %s \"%s\" because it is larger than the allowed limit (%s)": "Impossibile salvare %s \"%s\" perché è più grande del limite consentito (%s)",
"Cannot save %s \"%s\" because it would go over the total allowed size (%s) for this account": "Impossibile salvare %s \"%s\" perché supererebbe la dimensione massima consentita (%s) per questo account",
"Cannot share encrypted notebook with recipient %s because they have not enabled end-to-end encryption. They may do so from the screen Configuration > Encryption.": "Impossibile condividere il taccuino crittato con %s perché non hanno abilitato la crittografia end-to-end. Possono attivarla da Opzioni > Crittografia",
"Case sensitive": "Case sensitive",
"Change application layout": "Modifica il layout dell'applicazione",
"Change language": "Cambia lingua",
"Characters": "Caratteri",
@@ -116,24 +140,32 @@
"Code": "Codice",
"Code Block": "Blocco di codice",
"Code View": "Codice",
"Collaborate on notebooks with others": "Collabora su un taccuino con altri",
"Collapse": "Collassa",
"Coming alarms": "Prossimi avvisi",
"Comma-separated list of paths to directories to load the certificates from, or path to individual cert files. For example: /my/cert_dir, /other/custom.pem. Note that if you make changes to the TLS settings, you must save your changes before clicking on \"Check synchronisation configuration\".": "Elenco separato da virgole di percorsi alle cartelle per caricare i certificati o percorso dei singoli file cert. Ad esempio: /my/cert_dir, /other/custom.pem. Notare che se si apportano modifiche alle impostazioni TLS, è necessario salvare le modifiche prima di fare clic su \"Verifica configurazione sincronizzazione\".",
"command": "comando",
"Command": "Comando",
"Command palette": "Comandi",
"Command palette...": "Comandi...",
"Completed": "Completato",
"Completed decryption.": "Decrittografia completata.",
"Completed: %s (%s)": "Completato: %s (%s)",
"Compress old changes": "Comprimi le vecchie modifiche",
"Configuration": "Configurazione",
"Confirm password cannot be empty": "La password di conferma non può essere vuota",
"Confirm password:": "Conferma password:",
"Confirmation": "Conferma",
"Conflicted: %d": "Conflitti: %d",
"Conflicts": "Conflitti",
"Conflicts (attachments)": "Conflitti (allegati)",
"Content provided by %s": "Contenuto fornito da %s",
"Convert to note": "Converti in nota",
"Convert to todo": "Converti in Todo",
"Copy": "Copia",
"Copy dev mode command to clipboard": "Copia il comando della modalità sviluppatore negli appunti",
"Copy external link": "Copia link esterno",
"Copy image": "Copia immagine",
"Copy Link Address": "Copia l'indirizzo del link",
"Copy Markdown link": "Copia il link Markdown",
"Copy path to clipboard": "Copia il percorso negli appunti",
@@ -141,6 +173,7 @@
"Copy token": "Copia token",
"Could not authorise application:\n\n%s\n\nPlease try again.": "Non è stato possibile autorizzare l'applicazione:\n\n%s\n\nRiprovare per favore.",
"Could not connect to Joplin Server. Please check the Synchronisation options in the config screen. Full error was:\n\n%s": "Non è stato possibile connettersi al Joplin Server. Per favore controllare le opzioni di sincronizzazione nella relativa schermata di configurazione. Errore completo era:\n\n%s",
"Could not connect to plugin repository.": "Non è possibile connettersi al catalogo dei plugin.",
"Could not export notes: %s": "Impossibile esportare le note: %s",
"Could not install plugin: %s": "Non è possibile installare il plugin: %s",
"Could not upgrade master key: %s": "Non è possibile aggiornare la chiave master: %s",

View File

@@ -379,4 +379,19 @@ describe('models/Setting', function() {
}
});
test('values should not be undefined when they are set', async () => {
Setting.setValue('locale', undefined);
expect(Setting.value('locale')).toBe('');
});
test('values should not be undefined when registering a setting', async () => {
await Setting.registerSetting('myCustom', {
public: true,
value: undefined,
type: Setting.TYPE_STRING,
});
expect(Setting.value('myCustom')).toBe('');
});
});

View File

@@ -1382,6 +1382,18 @@ class Setting extends BaseModel {
};
} },
useCustomPdfViewer: {
value: false,
type: SettingItemType.Bool,
public: true,
advanced: true,
appTypes: [AppType.Desktop],
label: () => 'Use custom PDF viewer (Beta)',
description: () => 'The custom PDF viewer remembers the last page that was viewed, however it has some technical issues.',
storage: SettingStorage.File,
isGlobal: true,
},
'editor.keyboardMode': {
value: '',
type: SettingItemType.String,
@@ -1728,31 +1740,45 @@ class Setting extends BaseModel {
if (!key.match(/^[a-zA-Z0-9_\-.]+$/)) throw new Error(`Key must only contain characters /a-zA-Z0-9_-./ : ${key}`);
}
private static validateType(type: SettingItemType) {
if (!Number.isInteger(type)) throw new Error(`Setting type is not an integer: ${type}`);
if (type < 0) throw new Error(`Invalid setting type: ${type}`);
}
static async registerSetting(key: string, metadataItem: SettingItem) {
if (metadataItem.isEnum && !metadataItem.options) throw new Error('The `options` property is required for enum types');
try {
if (metadataItem.isEnum && !metadataItem.options) throw new Error('The `options` property is required for enum types');
this.validateKey(key);
this.validateKey(key);
this.validateType(metadataItem.type);
this.customMetadata_[key] = metadataItem;
this.customMetadata_[key] = {
...metadataItem,
value: this.formatValue(metadataItem.type, metadataItem.value),
};
// Clear cache
this.metadata_ = null;
this.keys_ = null;
// Clear cache
this.metadata_ = null;
this.keys_ = null;
// Reload the value from the database, if it was already present
const valueRow = await this.loadOne(key);
if (valueRow) {
this.cache_.push({
// Reload the value from the database, if it was already present
const valueRow = await this.loadOne(key);
if (valueRow) {
this.cache_.push({
key: key,
value: this.formatValue(key, valueRow.value),
});
}
this.dispatch({
type: 'SETTING_UPDATE_ONE',
key: key,
value: this.formatValue(key, valueRow.value),
value: this.value(key),
});
} catch (error) {
error.message = `Could not register setting "${key}": ${error.message}`;
throw error;
}
this.dispatch({
type: 'SETTING_UPDATE_ONE',
key: key,
value: this.value(key),
});
}
static async registerSection(name: string, source: SettingSectionSource, section: SettingSection) {
@@ -2107,12 +2133,12 @@ class Setting extends BaseModel {
return md.filter ? md.filter(value) : value;
}
static formatValue(key: string, value: any) {
const md = this.settingMetadata(key);
static formatValue(key: string | SettingItemType, value: any) {
const type = typeof key === 'string' ? this.settingMetadata(key).type : key;
if (md.type === SettingItemType.Int) return !value ? 0 : Math.floor(Number(value));
if (type === SettingItemType.Int) return !value ? 0 : Math.floor(Number(value));
if (md.type === SettingItemType.Bool) {
if (type === SettingItemType.Bool) {
if (typeof value === 'string') {
value = value.toLowerCase();
if (value === 'true') return true;
@@ -2122,26 +2148,26 @@ class Setting extends BaseModel {
return !!value;
}
if (md.type === SettingItemType.Array) {
if (type === SettingItemType.Array) {
if (!value) return [];
if (Array.isArray(value)) return value;
if (typeof value === 'string') return JSON.parse(value);
return [];
}
if (md.type === SettingItemType.Object) {
if (type === SettingItemType.Object) {
if (!value) return {};
if (typeof value === 'object') return value;
if (typeof value === 'string') return JSON.parse(value);
return {};
}
if (md.type === SettingItemType.String) {
if (type === SettingItemType.String) {
if (!value) return '';
return `${value}`;
}
throw new Error(`Unhandled value type: ${md.type}`);
throw new Error(`Unhandled value type: ${type}`);
}
static value(key: string) {

View File

@@ -17,11 +17,11 @@
},
"devDependencies": {
"@types/fs-extra": "9.0.13",
"@types/jest": "29.2.5",
"@types/jest": "29.2.6",
"@types/js-yaml": "4.0.5",
"@types/node": "18.11.18",
"@types/node-rsa": "1.1.1",
"@types/react": "17.0.52",
"@types/react": "17.0.53",
"@types/uuid": "^9.0.0",
"clean-html": "1.5.0",
"jest": "29.3.1",

View File

@@ -178,7 +178,7 @@ export default class ReportService {
for (let i = 0; i < disabledItems.length; i++) {
const row = disabledItems[i];
let msg: string = '';
let msg = '';
if (row.location === BaseItem.SYNC_ITEM_LOCATION_LOCAL) {
msg = _('%s (%s) could not be uploaded: %s', row.item.title, row.item.id, row.syncInfo.sync_disabled_reason);
} else {

View File

@@ -15,7 +15,7 @@ interface AdvancedExpression {
function parseAdvancedExpression(advancedExpression: string): AdvancedExpression {
let subExpressionIndex = -1;
let subExpressions: string = '';
let subExpressions = '';
let currentSubExpressionKey = '';
const subContext: any = {};

View File

@@ -100,7 +100,7 @@ export default class MenuUtils {
}
public commandsToMenuItems(commandNames: string[], onClick: Function, locale: string): MenuItems {
const key: string = `${this.keymapService.lastSaveTime}_${commandNames.join('_')}_${locale}`;
const key = `${this.keymapService.lastSaveTime}_${commandNames.join('_')}_${locale}`;
if (this.menuItemCache_[key]) return this.menuItemCache_[key];
const output: MenuItems = {};

View File

@@ -61,7 +61,7 @@ export default async function populateDatabase(db: any, options: Options = null)
const createdTagIds: string[] = [];
const createdFolderDepths: Record<string, number> = {};
const folderDepthToId: Record<number, string[]> = {};
let rootFolderCount: number = 0;
let rootFolderCount = 0;
for (let i = 0; i < options.folderCount; i++) {
const folder: any = {
@@ -72,7 +72,7 @@ export default async function populateDatabase(db: any, options: Options = null)
if (options.rootFolderCount && rootFolderCount >= options.rootFolderCount) isRoot = false;
let depth: number = 0;
let depth = 0;
if (!isRoot) {
let possibleFolderIds: string[] = [];

View File

@@ -110,7 +110,7 @@ export default class InteropService_Importer_Md extends InteropService_Importer_
if (stat && !isDir) {
const supportedFileExtension = this.metadata().fileExtensions;
const resolvedPath = shim.fsDriver().resolve(pathWithExtension);
let id: string = '';
let id = '';
// If the link looks like a note, then import it
if (supportedFileExtension.indexOf(fileExtension(trimmedLink).toLowerCase()) >= 0) {
// If the note hasn't been imported yet, do so now

View File

@@ -188,7 +188,7 @@ describe('ShareService', function() {
const recipientPpk = await generateKeyPair(encryptionService(), '222222');
expect(ppk.id).not.toBe(recipientPpk.id);
let uploadedEmail: string = '';
let uploadedEmail = '';
let uploadedMasterKey: MasterKeyEntity = null;
const service = testShareFolderService({

View File

@@ -141,7 +141,7 @@ async function testMigrationE2EE(migrationVersion: number, maxSyncVersion: numbe
await expectNotThrow(async () => await checkTestData(testData));
}
let previousSyncTargetName: string = '';
let previousSyncTargetName = '';
describe('MigrationHandler', function() {

View File

@@ -637,7 +637,11 @@ function shimInit(options = null) {
}
// Open the file
return shim.openUrl(`file://${filepath}`);
// Don't use openUrl() there.
// The underneath require('electron').shell.openExternal() has a bug
// https://github.com/electron/electron/issues/31347
return shim.electronBridge().openItem(filepath);
};
shim.waitForFrame = () => {};

View File

@@ -19,9 +19,9 @@
"author": "Joplin",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@types/jest": "29.2.5",
"@types/jest": "29.2.6",
"@types/pdfjs-dist": "2.10.378",
"@types/react": "16.14.34",
"@types/react": "16.14.35",
"@types/react-dom": "18.0.10",
"@types/styled-components": "5.1.25",
"babel-jest": "29.3.1",
@@ -29,7 +29,7 @@
"jest": "29.3.1",
"jest-environment-jsdom": "29.3.1",
"style-loader": "3.3.1",
"ts-jest": "29.0.3",
"ts-jest": "29.0.5",
"ts-loader": "9.4.2",
"typescript": "4.9.4",
"webpack": "5.74.0",

View File

@@ -271,7 +271,7 @@ async function commandVersion() {
}
async function main() {
const scriptName: string = 'plugin-repo-cli';
const scriptName = 'plugin-repo-cli';
const commands: Record<string, Function> = {
build: commandBuild,
@@ -279,8 +279,8 @@ async function main() {
updateRelease: commandUpdateRelease,
};
let selectedCommand: string = '';
let selectedCommandArgs: string = '';
let selectedCommand = '';
let selectedCommandArgs = '';
function setSelectedCommand(name: string, args: any) {
selectedCommand = name;

View File

@@ -28,7 +28,7 @@
},
"devDependencies": {
"@types/fs-extra": "9.0.13",
"@types/jest": "29.2.5",
"@types/jest": "29.2.6",
"@types/node": "18.11.18",
"jest": "29.3.1",
"source-map-loader": "4.0.1",

View File

@@ -0,0 +1 @@
*.pbxproj -text

View File

@@ -0,0 +1,44 @@
# OSX
#
.DS_Store
# node.js
#
node_modules/
npm-debug.log
yarn-error.log
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# Android/IntelliJ
#
**/build/
**/.idea
**/.gradle/
**/gradle/
gradle*
local.properties
*.iml
# BUCK
buck-out/
\.buckd/
*.keystore

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Chukwuemeka Ihedoro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,5 @@
# React Native Alarm Notification
This is a fork of [https://github.com/emekalites/react-native-alarm-notification](https://github.com/emekalites/react-native-alarm-notification) with a few bugfixes/improvements for Android.
It's made specifically for [Joplin](https://github.com/laurent22) and while all basic features should work (set/dismiss alarm, set text/icon, etc) it's not fully compatible with the orignal package.

View File

@@ -0,0 +1,145 @@
import groovy.json.JsonSlurper
// android/build.gradle
// based on:
//
// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/build.gradle
// original location:
// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/build.gradle
//
// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/app/build.gradle
// original location:
// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/app/build.gradle
def DEFAULT_COMPILE_SDK_VERSION = 31
def DEFAULT_BUILD_TOOLS_VERSION = '31.0.0'
def DEFAULT_MIN_SDK_VERSION = 21
def DEFAULT_TARGET_SDK_VERSION = 31
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
buildscript {
// The Android Gradle plugin is only required when opening the android folder stand-alone.
// This avoids unnecessary downloads and potential conflicts when the library is included as a
// module dependency in an application project.
// ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies
if (project == rootProject) {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
}
}
}
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
android {
compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION)
targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
versionCode 1
versionName "1.0"
}
lintOptions {
abortOnError false
}
}
repositories {
// ref: https://www.baeldung.com/maven-local-repository
mavenLocal()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
maven {
// Android JSC is installed from npm
url "$rootDir/../node_modules/jsc-android/dist"
}
google()
mavenCentral()
}
dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+' // From node_modules
implementation 'com.google.code.gson:gson:2.8.8'
implementation 'androidx.appcompat:appcompat:1.1.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.6'
}
def configureReactNativePom(def pom) {
def packageJson = new JsonSlurper().parseText(file('../package.json').text)
pom.project {
name packageJson.title
artifactId packageJson.name
version = packageJson.version
group = "com.emekalites.react.alarm.notification"
description packageJson.description
url packageJson.repository.baseUrl
licenses {
license {
name packageJson.license
url packageJson.repository.baseUrl + '/blob/master/' + packageJson.licenseFilename
distribution 'repo'
}
}
}
}
afterEvaluate { project ->
// some Gradle build hooks ref:
// https://www.oreilly.com/library/view/gradle-beyond-the/9781449373801/ch03.html
task androidJavadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += files(android.bootClasspath)
// Must be removed due to error "Configuration with name 'compile' not found."
// classpath += files(project.getConfigurations().getByName('compile').asList())
include '**/*.java'
}
task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
classifier = 'javadoc'
from androidJavadoc.destinationDir
}
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
include '**/*.java'
}
android.libraryVariants.all { variant ->
def name = variant.name.capitalize()
def javaCompileTask = variant.javaCompileProvider.get()
task "jar${name}"(type: Jar, dependsOn: javaCompileTask) {
from javaCompileTask.destinationDir
}
}
artifacts {
archives androidSourcesJar
archives androidJavadocJar
}
task installArchives(type: Upload) {
configuration = configurations.archives
}
}

View File

@@ -0,0 +1,38 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.emekalites.react.alarm.notification">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application>
<receiver
android:name="com.emekalites.react.alarm.notification.AlarmReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="ACTION_DISMISS" />
<action android:name="ACTION_SNOOZE" />
</intent-filter>
</receiver>
<receiver
android:name="com.emekalites.react.alarm.notification.AlarmDismissReceiver"
android:enabled="true"
android:exported="true" />
<receiver
android:name="com.emekalites.react.alarm.notification.AlarmBootReceiver"
android:directBootAware="true"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -0,0 +1,198 @@
package com.emekalites.react.alarm.notification;
import android.app.Activity;
import android.app.Application;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
@SuppressWarnings("unused")
public class ANModule extends ReactContextBaseJavaModule implements ActivityEventListener {
private static final String E_SCHEDULE_ALARM_FAILED = "E_SCHEDULE_ALARM_FAILED";
private static ReactApplicationContext mReactContext;
private final AlarmUtil alarmUtil;
private final AlarmModelCodec codec = new AlarmModelCodec();
ANModule(ReactApplicationContext reactContext) {
super(reactContext);
mReactContext = reactContext;
alarmUtil = new AlarmUtil((Application) reactContext.getApplicationContext());
reactContext.addActivityEventListener(this);
}
static ReactApplicationContext getReactAppContext() {
return mReactContext;
}
@NonNull
@Override
public String getName() {
return "RNAlarmNotification";
}
// Required for rn built in EventEmitter Calls.
@ReactMethod
public void addListener(String eventName) {
}
@ReactMethod
public void removeListeners(Integer count) {
}
private AlarmDatabase getAlarmDB() {
return new AlarmDatabase(mReactContext);
}
@ReactMethod
public void scheduleAlarm(ReadableMap details, Promise promise) {
try {
Bundle bundle = Arguments.toBundle(details);
AlarmModel alarm = AlarmModel.fromBundle(bundle);
// check if alarm has been set at this time
boolean containAlarm = alarmUtil.checkAlarm(getAlarmDB().getAlarmList(1), alarm);
if (!containAlarm) {
int id = getAlarmDB().insert(alarm);
alarm.setId(id);
alarmUtil.setAlarm(alarm);
WritableMap map = Arguments.createMap();
map.putInt("id", id);
promise.resolve(map);
} else {
promise.reject(E_SCHEDULE_ALARM_FAILED, "duplicate alarm set at date");
}
} catch (Exception e) {
Log.e(Constants.TAG, "Could not schedule alarm", e);
promise.reject(E_SCHEDULE_ALARM_FAILED, e);
}
}
@ReactMethod
public void deleteAlarm(int alarmID) {
alarmUtil.deleteAlarm(alarmID);
}
@ReactMethod
public void deleteRepeatingAlarm(int alarmID) {
alarmUtil.deleteRepeatingAlarm(alarmID);
}
@ReactMethod
public void sendNotification(ReadableMap details) {
try {
Bundle bundle = Arguments.toBundle(details);
AlarmModel alarm = AlarmModel.fromBundle(bundle);
int id = getAlarmDB().insert(alarm);
alarm.setId(id);
alarmUtil.sendNotification(alarm);
} catch (Exception e) {
Log.e(Constants.TAG, "Could not send notification", e);
}
}
@ReactMethod
public void removeFiredNotification(int id) {
alarmUtil.removeFiredNotification(id);
}
@ReactMethod
public void removeAllFiredNotifications() {
alarmUtil.removeAllFiredNotifications();
}
@ReactMethod
public void getScheduledAlarms(Promise promise) throws JSONException {
ArrayList<AlarmModel> alarms = alarmUtil.getAlarms();
WritableArray array = Arguments.createArray();
for (AlarmModel alarm : alarms) {
// TODO triple conversion alarm -> string -> json -> map
// this is ugly but I don't have time to fix it now
WritableMap alarmMap = alarmUtil.convertJsonToMap(new JSONObject(codec.toJson(alarm)));
array.pushMap(alarmMap);
}
promise.resolve(array);
}
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
}
@Override
public void onNewIntent(Intent intent) {
if (Constants.NOTIFICATION_ACTION_CLICK.equals(intent.getAction())) {
Bundle bundle = intent.getExtras();
try {
if (bundle != null) {
int alarmId = bundle.getInt(Constants.NOTIFICATION_ID);
alarmUtil.removeFiredNotification(alarmId);
alarmUtil.doCancelAlarm(alarmId);
WritableMap response = Arguments.fromBundle(bundle.getBundle("data"));
mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("OnNotificationOpened", response);
}
} catch (Exception e) {
Log.e(Constants.TAG, "Couldn't convert bundle to JSON", e);
}
}
}
@ReactMethod
public void getAlarmInfo(Promise promise) {
if (getCurrentActivity() == null) {
promise.resolve(null);
return;
}
Intent intent = getCurrentActivity().getIntent();
if (intent != null) {
if (Constants.NOTIFICATION_ACTION_CLICK.equals(intent.getAction()) &&
intent.getExtras() != null) {
Bundle bundle = intent.getExtras();
WritableMap response = Arguments.fromBundle(bundle.getBundle("data"));
promise.resolve(response);
// cleanup
// other libs may not expect the intent to be null so set an empty intent here
getCurrentActivity().setIntent(new Intent());
int alarmId = bundle.getInt(Constants.NOTIFICATION_ID);
alarmUtil.removeFiredNotification(alarmId);
alarmUtil.doCancelAlarm(alarmId);
return;
}
}
promise.resolve(null);
}
}

View File

@@ -0,0 +1,31 @@
package com.emekalites.react.alarm.notification;
import androidx.annotation.NonNull;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Collections;
import java.util.List;
@SuppressWarnings("unused")
public class ANPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
return Collections.<NativeModule>singletonList(new ANModule(reactContext));
}
// Deprecated RN 0.47
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,34 @@
package com.emekalites.react.alarm.notification;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import java.util.ArrayList;
public class AlarmBootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) ||
"android.intent.action.QUICKBOOT_POWERON".equals(intent.getAction()) ||
"android.intent.action.LOCKED_BOOT_COMPLETED".equals(intent.getAction()) ||
"com.htc.intent.action.QUICKBOOT_POWERON".equals(intent.getAction())) {
Log.i(Constants.TAG, "Rescheduling after boot, intent=" + intent);
try (AlarmDatabase alarmDB = new AlarmDatabase(context)) {
ArrayList<AlarmModel> alarms = alarmDB.getAlarmList(1);
AlarmUtil alarmUtil = new AlarmUtil((Application) context.getApplicationContext());
for (AlarmModel alarm : alarms) {
alarmUtil.setAlarm(alarm);
}
} catch (Exception e) {
Log.e(Constants.TAG, "Could not reschedule alarms on boot", e);
}
}
}
}

View File

@@ -0,0 +1,153 @@
package com.emekalites.react.alarm.notification;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import java.util.ArrayList;
public class AlarmDatabase extends SQLiteOpenHelper implements AutoCloseable {
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "rnandb";
private static final String TABLE_NAME = "alarmtbl";
private static final String COL_ID = "id";
private static final String COL_DATA = "gson_data";
private static final String COL_ACTIVE = "active";
private final String CREATE_TABLE_ALARM = "CREATE TABLE " + TABLE_NAME + " ("
+ COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ COL_DATA + " TEXT, "
+ COL_ACTIVE + " INTEGER) ";
private final AlarmModelCodec codec = new AlarmModelCodec();
AlarmDatabase(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_ALARM);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(String.format(" DROP TABLE IF EXISTS %s", CREATE_TABLE_ALARM));
onCreate(db);
}
AlarmModel getAlarm(int _id) {
SQLiteDatabase db = this.getWritableDatabase();
AlarmModel alarm = null;
String selectQuery = "SELECT * FROM " + TABLE_NAME + " WHERE " + COL_ID + " = " + _id;
try (Cursor cursor = db.rawQuery(selectQuery, null)) {
cursor.moveToFirst();
int id = cursor.getInt(0);
String data = cursor.getString(1);
int active = cursor.getInt(2);
Log.d(Constants.TAG, "get alarm -> id:" + id + ", active:" + active + ", " + data);
alarm = codec.fromJson(data);
alarm.setId(id);
alarm.setActive(active);
} catch (Exception e) {
Log.e(Constants.TAG, "getAlarm: exception", e);
}
return alarm;
}
int insert(AlarmModel alarm) {
try (SQLiteDatabase db = this.getWritableDatabase()) {
ContentValues values = new ContentValues();
String data = codec.toJson(alarm);
Log.i(Constants.TAG, "insert alarm: " + data);
values.put(COL_DATA, data);
values.put(COL_ACTIVE, alarm.getActive());
return (int) db.insert(TABLE_NAME, null, values);
} catch (Exception e) {
Log.e(Constants.TAG, "Error inserting into DB", e);
return 0;
}
}
void update(AlarmModel alarm) {
String where = COL_ID + " = " + alarm.getId();
try (SQLiteDatabase db = this.getWritableDatabase()) {
ContentValues values = new ContentValues();
String data = codec.toJson(alarm);
Log.d(Constants.TAG, "update alarm: " + data);
values.put(COL_ID, alarm.getId());
values.put(COL_DATA, data);
values.put(COL_ACTIVE, alarm.getActive());
db.update(TABLE_NAME, values, where, null);
} catch (Exception e) {
Log.e(Constants.TAG, "Error updating alarm " + alarm, e);
}
}
void delete(int id) {
String where = COL_ID + "=" + id;
try (SQLiteDatabase db = this.getWritableDatabase()) {
db.delete(TABLE_NAME, where, null);
} catch (Exception e) {
Log.e(Constants.TAG, "Error deleting alarm with id " + id, e);
}
}
ArrayList<AlarmModel> getAlarmList(int isActive) {
String selectQuery = "SELECT * FROM " + TABLE_NAME;
if (isActive == 1) {
selectQuery += " WHERE " + COL_ACTIVE + " = " + isActive;
}
SQLiteDatabase db = this.getWritableDatabase();
ArrayList<AlarmModel> alarms = new ArrayList<>();
try (Cursor cursor = db.rawQuery(selectQuery, null)) {
if (cursor.moveToFirst()) {
do {
int id = cursor.getInt(0);
String data = cursor.getString(1);
int active = cursor.getInt(2);
Log.d(Constants.TAG, "get alarm -> id:" + id + ", active:" + active + ", " + data);
AlarmModel alarm = codec.fromJson(data);
alarm.setId(id);
alarm.setActive(active);
alarms.add(alarm);
} while (cursor.moveToNext());
}
} catch (Exception e) {
Log.e(Constants.TAG, "getAlarmList: exception cause " + e.getCause() + " message " + e.getMessage());
}
return alarms;
}
ArrayList<AlarmModel> getAlarmList() {
return getAlarmList(0);
}
}

View File

@@ -0,0 +1,28 @@
package com.emekalites.react.alarm.notification;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.facebook.react.modules.core.DeviceEventManagerModule;
public class AlarmDismissReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
AlarmUtil alarmUtil = new AlarmUtil((Application) context.getApplicationContext());
try {
int notificationId = intent.getExtras().getInt(Constants.NOTIFICATION_ID);
if (ANModule.getReactAppContext() != null) {
// TODO also send all user-provided args back
ANModule.getReactAppContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("OnNotificationDismissed", "{\"id\": \"" + notificationId + "\"}");
}
alarmUtil.removeFiredNotification(notificationId);
alarmUtil.doCancelAlarm(notificationId);
} catch (Exception e) {
Log.e(Constants.TAG, "Exception when handling notification dismiss. " + e);
}
}
}

View File

@@ -0,0 +1,444 @@
package com.emekalites.react.alarm.notification;
import android.os.Bundle;
import androidx.annotation.NonNull;
import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
public class AlarmModel implements Serializable {
private int id;
private int minute;
private int hour;
private int second;
private int day;
private int month;
private int year;
private int alarmId;
private String title;
private String message;
private String channel;
private String ticker;
private boolean autoCancel;
private boolean vibrate;
private int vibration;
private String smallIcon;
private String largeIcon;
private boolean playSound;
private String soundName;
private String soundNames; // separate sounds with comma eg (sound1.mp3,sound2.mp3)
private String color;
private String scheduleType;
private String interval; // hourly, daily, weekly
private int intervalValue;
private int snoozeInterval; // in minutes
private String tag;
private Bundle data;
private boolean loopSound;
private boolean useBigText;
private boolean hasButton;
private double volume;
private boolean bypassDnd;
private int active = 1; // 1 = yes, 0 = no
private AlarmModel() {}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getSecond() {
return second;
}
public void setSecond(int second) {
this.second = second;
}
public int getMinute() {
return minute;
}
public void setMinute(int minute) {
this.minute = minute;
}
public int getHour() {
return hour;
}
public void setHour(int hour) {
this.hour = hour;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getAlarmId() {
return alarmId;
}
public void setAlarmId(int alarmId) {
this.alarmId = alarmId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
public String getTicker() {
return ticker;
}
public void setTicker(String ticker) {
this.ticker = ticker;
}
public boolean isAutoCancel() {
return autoCancel;
}
public void setAutoCancel(boolean autoCancel) {
this.autoCancel = autoCancel;
}
public boolean isVibrate() {
return vibrate;
}
public void setVibrate(boolean vibrate) {
this.vibrate = vibrate;
}
public int getVibration() {
return vibration;
}
public void setVibration(int vibration) {
this.vibration = vibration;
}
public String getSmallIcon() {
return smallIcon;
}
public void setSmallIcon(String smallIcon) {
this.smallIcon = smallIcon;
}
public String getLargeIcon() {
return largeIcon;
}
public void setLargeIcon(String largeIcon) {
this.largeIcon = largeIcon;
}
public boolean isPlaySound() {
return playSound;
}
public void setPlaySound(boolean playSound) {
this.playSound = playSound;
}
public String getSoundName() {
return soundName;
}
public void setSoundName(String soundName) {
this.soundName = soundName;
}
public String getSoundNames() {
return soundNames;
}
public void setSoundNames(String soundNames) {
this.soundNames = soundNames;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String getScheduleType() {
return scheduleType;
}
public void setScheduleType(String scheduleType) {
this.scheduleType = scheduleType;
}
public String getInterval() {
return interval;
}
public void setInterval(String interval) {
this.interval = interval;
}
public int getIntervalValue() {
return intervalValue;
}
public void setIntervalValue(int intervalValue) {
this.intervalValue = intervalValue;
}
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
public Bundle getData() {
return data;
}
public void setData(Bundle data) {
this.data = data;
}
public int getActive() {
return active;
}
public void setActive(int active) {
this.active = active;
}
public int getSnoozeInterval() {
return snoozeInterval;
}
public void setSnoozeInterval(int snoozeInterval) {
this.snoozeInterval = snoozeInterval;
}
public boolean isLoopSound() {
return loopSound;
}
public void setLoopSound(boolean loopSound) {
this.loopSound = loopSound;
}
public boolean isUseBigText() {
return useBigText;
}
public void setUseBigText(boolean useBigText) {
this.useBigText = useBigText;
}
public boolean isHasButton() {
return hasButton;
}
public void setHasButton(boolean hasButton) {
this.hasButton = hasButton;
}
public double getVolume() {
return volume;
}
public void setVolume(double volume) {
if (volume > 1 || volume < 0) {
this.volume = 0.5;
} else {
this.volume = volume;
}
}
public boolean isBypassDnd() {
return bypassDnd;
}
public void setBypassDnd(boolean bypassDnd) {
this.bypassDnd = bypassDnd;
}
@NonNull
@Override
public String toString() {
return "AlarmModel{" +
"id=" + id +
", second=" + second +
", minute=" + minute +
", hour=" + hour +
", day=" + day +
", month=" + month +
", year=" + year +
", alarmId=" + alarmId +
", title='" + title + '\'' +
", message='" + message + '\'' +
", channel='" + channel + '\'' +
", ticker='" + ticker + '\'' +
", autoCancel=" + autoCancel +
", vibrate=" + vibrate +
", vibration=" + vibration +
", smallIcon='" + smallIcon + '\'' +
", largeIcon='" + largeIcon + '\'' +
", playSound=" + playSound +
", soundName='" + soundName + '\'' +
", soundNames='" + soundNames + '\'' +
", color='" + color + '\'' +
", scheduleType='" + scheduleType + '\'' +
", interval=" + interval +
", intervalValue=" + intervalValue +
", snoozeInterval=" + snoozeInterval +
", tag='" + tag + '\'' +
", data='" + data + '\'' +
", loopSound=" + loopSound +
", useBigText=" + useBigText +
", hasButton=" + hasButton +
", volume=" + volume +
", bypassDnd=" + bypassDnd +
", active=" + active +
'}';
}
public static AlarmModel fromBundle(@NonNull Bundle bundle) {
AlarmModel alarm = new AlarmModel();
long time = System.currentTimeMillis() / 1000;
alarm.setAlarmId((int) time);
alarm.setActive(1);
alarm.setAutoCancel(bundle.getBoolean("auto_cancel", true));
alarm.setChannel(bundle.getString("channel", "my_channel_id"));
alarm.setColor(bundle.getString("color", "red"));
Bundle data = bundle.getBundle("data");
alarm.setData(data);
alarm.setInterval(bundle.getString("repeat_interval", "hourly"));
alarm.setLargeIcon(bundle.getString("large_icon", ""));
alarm.setLoopSound(bundle.getBoolean("loop_sound", false));
alarm.setMessage(bundle.getString("message", "My Notification Message"));
alarm.setPlaySound(bundle.getBoolean("play_sound", true));
alarm.setScheduleType(bundle.getString("schedule_type", "once"));
alarm.setSmallIcon(bundle.getString("small_icon", "ic_launcher"));
alarm.setSnoozeInterval((int) bundle.getDouble("snooze_interval", 1.0));
alarm.setSoundName(bundle.getString("sound_name", null));
alarm.setSoundNames(bundle.getString("sound_names", null));
alarm.setTag(bundle.getString("tag", ""));
alarm.setTicker(bundle.getString("ticker", ""));
alarm.setTitle(bundle.getString("title", "My Notification Title"));
alarm.setVibrate(bundle.getBoolean("vibrate", true));
alarm.setHasButton(bundle.getBoolean("has_button", false));
alarm.setVibration((int) bundle.getDouble("vibration", 100.0));
alarm.setUseBigText(bundle.getBoolean("use_big_text", false));
alarm.setVolume(bundle.getDouble("volume", 0.5));
alarm.setIntervalValue((int) bundle.getDouble("interval_value", 1));
alarm.setBypassDnd(bundle.getBoolean("bypass_dnd", false));
String datetime = bundle.getString("fire_date");
SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.ENGLISH);
Calendar calendar = new GregorianCalendar();
try {
calendar.setTime(sdf.parse(datetime));
} catch (ParseException e) {
throw new RuntimeException(e);
}
alarm.setAlarmDateTime(calendar);
return alarm;
}
void setAlarmDateTime(Calendar calendar) {
setSecond(calendar.get(Calendar.SECOND));
setMinute(calendar.get(Calendar.MINUTE));
setHour(calendar.get(Calendar.HOUR_OF_DAY));
setDay(calendar.get(Calendar.DAY_OF_MONTH));
setMonth(calendar.get(Calendar.MONTH) + 1);
setYear(calendar.get(Calendar.YEAR));
}
Calendar getAlarmDateTime() {
Calendar calendar = new GregorianCalendar();
calendar.set(Calendar.HOUR_OF_DAY, getHour());
calendar.set(Calendar.MINUTE, getMinute());
calendar.set(Calendar.SECOND, getSecond());
calendar.set(Calendar.DAY_OF_MONTH, getDay());
calendar.set(Calendar.MONTH, getMonth() - 1);
calendar.set(Calendar.YEAR, getYear());
return calendar;
}
Calendar snooze() {
Calendar calendar = getAlarmDateTime();
calendar.add(Calendar.MINUTE, getSnoozeInterval());
setAlarmDateTime(calendar);
return calendar;
}
boolean isSameTime(AlarmModel alarm) {
return this.getHour() == alarm.getHour() && this.getMinute() == alarm.getMinute() &&
this.getSecond() == alarm.getSecond() && this.getDay() == alarm.getDay() &&
this.getMonth() == alarm.getMonth() && this.getYear() == alarm.getYear();
}
}

View File

@@ -0,0 +1,114 @@
package com.emekalites.react.alarm.notification;
import static com.google.gson.stream.JsonToken.END_OBJECT;
import android.os.Bundle;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
public class AlarmModelCodec {
private final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new TypeAdapterFactory() {
@Override
public <T> TypeAdapter<T> create(final Gson gson, TypeToken<T> typeToken) {
if (!Bundle.class.isAssignableFrom(typeToken.getRawType())) {
return null;
}
return (TypeAdapter<T>) new TypeAdapter<Bundle>() {
@Override
public void write(JsonWriter writer, Bundle bundle) throws IOException {
if (bundle == null) {
writer.nullValue();
return;
}
writer.beginObject();
for (String key : bundle.keySet()) {
writer.name(key);
Object value = bundle.get(key);
if (value == null) {
writer.nullValue();
} else {
gson.toJson(value, value.getClass(), writer);
}
}
writer.endObject();
}
@Override
public Bundle read(JsonReader reader) throws IOException {
switch (reader.peek()) {
case NULL:
reader.nextNull();
return null;
case BEGIN_OBJECT:
return readBundle(reader);
default:
throw new IOException("Could not read bundle at " + reader.getPath());
}
}
private Bundle readBundle(JsonReader reader) throws IOException {
reader.beginObject();
Bundle bundle = new Bundle();
JsonToken nextToken;
while ((nextToken = reader.peek()) != END_OBJECT) {
switch (nextToken) {
case NAME:
readNextKeyValue(reader, bundle);
case END_OBJECT:
break;
}
}
reader.endObject();
return bundle;
}
private void readNextKeyValue(JsonReader reader, Bundle bundle) throws IOException {
String name = reader.nextName();
// only support a small subset of possible types, enough for Joplin
switch (reader.peek()) {
case STRING:
bundle.putString(name, reader.nextString());
break;
case BOOLEAN:
bundle.putBoolean(name, reader.nextBoolean());
case NUMBER:
double doubleVal = reader.nextDouble();
if (Math.round(doubleVal) == doubleVal) {
long longVal = (long) doubleVal;
if (longVal >= Integer.MIN_VALUE && longVal <= Integer.MAX_VALUE) {
bundle.putInt(name, (int) longVal);
} else {
bundle.putLong(name, longVal);
}
} else {
bundle.putDouble(name, doubleVal);
}
break;
}
}
};
}
})
.create();
public String toJson(AlarmModel alarmModel) {
return gson.toJson(alarmModel);
}
public AlarmModel fromJson(String json) {
return gson.fromJson(json, AlarmModel.class);
}
}

View File

@@ -0,0 +1,79 @@
package com.emekalites.react.alarm.notification;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import java.util.ArrayList;
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null) {
final AlarmDatabase alarmDB = new AlarmDatabase(context);
AlarmUtil alarmUtil = new AlarmUtil((Application) context.getApplicationContext());
try {
String intentType = intent.getExtras().getString("intentType");
if (Constants.ADD_INTENT.equals(intentType)) {
int id = intent.getExtras().getInt("PendingId");
try {
AlarmModel alarm = alarmDB.getAlarm(id);
alarmUtil.sendNotification(alarm);
alarmUtil.setBootReceiver();
ArrayList<AlarmModel> alarms = alarmDB.getAlarmList(1);
Log.d(Constants.TAG, "alarm start: " + alarm.toString() + ", alarms left: " + alarms.size());
} catch (Exception e) {
Log.e(Constants.TAG, "Failed to add alarm", e);
}
}
} catch (Exception e) {
Log.e(Constants.TAG, "Received invalid intent", e);
}
String action = intent.getAction();
if (action != null) {
Log.i(Constants.TAG, "ACTION: " + action);
switch (action) {
case Constants.NOTIFICATION_ACTION_SNOOZE:
int id = intent.getExtras().getInt("SnoozeAlarmId");
try {
AlarmModel alarm = alarmDB.getAlarm(id);
alarmUtil.snoozeAlarm(alarm);
Log.i(Constants.TAG, "alarm snoozed: " + alarm.toString());
alarmUtil.removeFiredNotification(alarm.getId());
} catch (Exception e) {
Log.e(Constants.TAG, "Failed to snooze alarm", e);
}
break;
case Constants.NOTIFICATION_ACTION_DISMISS:
id = intent.getExtras().getInt("AlarmId");
try {
AlarmModel alarm = alarmDB.getAlarm(id);
Log.i(Constants.TAG, "Cancel alarm: " + alarm.toString());
// emit notification dismissed
// TODO also send all user-provided args back
ANModule.getReactAppContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("OnNotificationDismissed", "{\"id\": \"" + alarm.getId() + "\"}");
alarmUtil.removeFiredNotification(alarm.getId());
alarmUtil.cancelAlarm(alarm, false); // TODO why false?
} catch (Exception e) {
Log.e(Constants.TAG, "Failed to dismiss alarm", e);
}
break;
}
}
}
}
}

View File

@@ -0,0 +1,476 @@
package com.emekalites.react.alarm.notification;
import android.app.AlarmManager;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.RingtoneManager;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import static com.emekalites.react.alarm.notification.Constants.ADD_INTENT;
import static com.emekalites.react.alarm.notification.Constants.NOTIFICATION_ACTION_DISMISS;
import static com.emekalites.react.alarm.notification.Constants.NOTIFICATION_ACTION_SNOOZE;
class AlarmUtil {
private static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250};
private int defaultFlags = 0;
private final Context context;
private final AlarmDatabase alarmDB;
AlarmUtil(Application context) {
this.context = context;
this.alarmDB = new AlarmDatabase(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
defaultFlags = PendingIntent.FLAG_IMMUTABLE;
}
}
private Class<?> getMainActivityClass() {
try {
String packageName = context.getPackageName();
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
String className = launchIntent.getComponent().getClassName();
Log.d(Constants.TAG, "main activity classname: " + className);
return Class.forName(className);
} catch (Exception e) {
Log.e(Constants.TAG, "Could not load main activity class", e);
return null;
}
}
private AlarmManager getAlarmManager() {
return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
}
private NotificationManager getNotificationManager() {
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
boolean checkAlarm(ArrayList<AlarmModel> alarms, AlarmModel alarm) {
for (AlarmModel aAlarm : alarms) {
if (aAlarm.isSameTime(alarm) && aAlarm.getActive() == 1) {
Toast.makeText(context, "You have already set this Alarm", Toast.LENGTH_SHORT).show();
return true;
}
}
return false;
}
void setBootReceiver() {
ArrayList<AlarmModel> alarms = alarmDB.getAlarmList(1);
if (alarms.size() > 0) {
enableBootReceiver(context);
} else {
disableBootReceiver(context);
}
}
void setAlarm(AlarmModel alarm) {
Log.i(Constants.TAG, "Set alarm " + alarm);
Calendar calendar = alarm.getAlarmDateTime();
int alarmId = alarm.getAlarmId();
Intent intent = new Intent(context, AlarmReceiver.class);
intent.putExtra("intentType", ADD_INTENT);
intent.putExtra("PendingId", alarm.getId());
PendingIntent alarmIntent = PendingIntent.getBroadcast(context, alarmId, intent, defaultFlags);
AlarmManager alarmManager = this.getAlarmManager();
String scheduleType = alarm.getScheduleType();
if (scheduleType.equals("once")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
}
} else if (scheduleType.equals("repeat")) {
long interval = this.getInterval(alarm.getInterval(), alarm.getIntervalValue());
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), interval, alarmIntent);
} else {
Log.w(Constants.TAG, "Schedule type should either be once or repeat");
return;
}
this.setBootReceiver();
}
void snoozeAlarm(AlarmModel alarm) {
Log.i(Constants.TAG, "Snooze alarm: " + alarm.toString());
Calendar calendar = alarm.snooze();
long time = System.currentTimeMillis() / 1000;
alarm.setAlarmId((int) time);
// TODO looks like this sets a new id and then tries to update the row in DB
// how's that supposed to work?
alarmDB.update(alarm);
int alarmId = alarm.getAlarmId();
Intent intent = new Intent(context, AlarmReceiver.class);
intent.putExtra("intentType", ADD_INTENT);
intent.putExtra("PendingId", alarm.getId());
PendingIntent alarmIntent = PendingIntent.getBroadcast(context, alarmId, intent, defaultFlags);
AlarmManager alarmManager = this.getAlarmManager();
String scheduleType = alarm.getScheduleType();
if (scheduleType.equals("once")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
}
} else if (scheduleType.equals("repeat")) {
long interval = this.getInterval(alarm.getInterval(), alarm.getIntervalValue());
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), interval, alarmIntent);
} else {
Log.w(Constants.TAG, "Schedule type should either be once or repeat");
}
}
long getInterval(String interval, int value) {
long duration = 1;
switch (interval) {
case "minutely":
duration = value;
break;
case "hourly":
duration = 60 * value;
break;
case "daily":
duration = 60 * 24;
break;
case "weekly":
duration = 60 * 24 * 7;
break;
}
return duration * 60 * 1000;
}
void doCancelAlarm(int id) {
try {
AlarmModel alarm = alarmDB.getAlarm(id);
this.cancelAlarm(alarm, false);
} catch (Exception e) {
Log.e(Constants.TAG, "Could not cancel alarm with id " + id, e);
}
}
void deleteAlarm(int id) {
try {
AlarmModel alarm = alarmDB.getAlarm(id);
this.cancelAlarm(alarm, true);
} catch (Exception e) {
Log.e(Constants.TAG, "Could not delete alarm with id " + id, e);
}
}
void deleteRepeatingAlarm(int id) {
try {
AlarmModel alarm = alarmDB.getAlarm(id);
String scheduleType = alarm.getScheduleType();
if (scheduleType.equals("repeat")) {
this.stopAlarm(alarm);
}
} catch (Exception e) {
Log.e(Constants.TAG, "Could not delete repeating alarm with id " + id, e);
}
}
void cancelAlarm(AlarmModel alarm, boolean delete) {
String scheduleType = alarm.getScheduleType();
if (scheduleType.equals("once") || delete) {
this.stopAlarm(alarm);
}
}
void stopAlarm(AlarmModel alarm) {
AlarmManager alarmManager = this.getAlarmManager();
int alarmId = alarm.getAlarmId();
Intent intent = new Intent(context, AlarmReceiver.class);
PendingIntent alarmIntent = PendingIntent.getBroadcast(context, alarmId, intent, defaultFlags | PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.cancel(alarmIntent);
alarmDB.delete(alarm.getId());
this.setBootReceiver();
}
private void enableBootReceiver(Context context) {
ComponentName receiver = new ComponentName(context, AlarmBootReceiver.class);
PackageManager pm = context.getPackageManager();
int setting = pm.getComponentEnabledSetting(receiver);
if (setting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED ||
setting == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
Log.i(Constants.TAG, "Enable boot receiver");
pm.setComponentEnabledSetting(receiver,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
} else {
Log.i(Constants.TAG, "Boot receiver already enabled");
}
}
private void disableBootReceiver(Context context) {
Log.i(Constants.TAG, "Disable boot receiver");
ComponentName receiver = new ComponentName(context, AlarmBootReceiver.class);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(receiver,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
private PendingIntent createOnDismissedIntent(Context context, int notificationId) {
Intent intent = new Intent(context, AlarmDismissReceiver.class);
intent.putExtra(Constants.NOTIFICATION_ID, notificationId);
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, defaultFlags);
}
void sendNotification(AlarmModel alarm) {
try {
Class<?> intentClass = getMainActivityClass();
if (intentClass == null) {
Log.e(Constants.TAG, "No activity class found for the notification");
return;
}
NotificationManager mNotificationManager = getNotificationManager();
int notificationID = alarm.getAlarmId();
// title
String title = alarm.getTitle();
if (title == null || title.equals("")) {
ApplicationInfo appInfo = context.getApplicationInfo();
title = context.getPackageManager().getApplicationLabel(appInfo).toString();
}
// message
// TODO move to AlarmModel constructor?
String message = alarm.getMessage();
if (message == null || message.equals("")) {
Log.e(Constants.TAG, "Cannot send to notification centre because there is no 'message' found");
return;
}
// channel
// TODO move to AlarmModel constructor?
String channelID = alarm.getChannel();
if (channelID == null || channelID.equals("")) {
Log.e(Constants.TAG, "Cannot send to notification centre because there is no 'channel' found");
return;
}
Resources res = context.getResources();
String packageName = context.getPackageName();
//icon
// TODO move to AlarmModel constructor?
int smallIconResId;
String smallIcon = alarm.getSmallIcon();
if (smallIcon != null && !smallIcon.equals("")) {
smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName);
} else {
smallIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
}
Intent intent = new Intent(context, intentClass);
intent.setAction(Constants.NOTIFICATION_ACTION_CLICK);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(Constants.NOTIFICATION_ID, alarm.getId());
intent.putExtra("data", alarm.getData());
PendingIntent pendingIntent = PendingIntent.getActivity(context, notificationID, intent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, channelID)
.setSmallIcon(smallIconResId)
.setContentTitle(title)
.setContentText(message)
.setTicker(alarm.getTicker())
.setPriority(NotificationCompat.PRIORITY_MAX)
.setAutoCancel(alarm.isAutoCancel())
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setSound(null)
.setDeleteIntent(createOnDismissedIntent(context, alarm.getId()));
if (alarm.isPlaySound()) {
// TODO use user-supplied sound if available
mBuilder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), AudioManager.STREAM_NOTIFICATION);
}
long vibration = alarm.getVibration();
long[] vibrationPattern = vibration == 0 ? DEFAULT_VIBRATE_PATTERN : new long[]{0, vibration, 1000, vibration};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel mChannel = new NotificationChannel(channelID, "Alarm Notify", NotificationManager.IMPORTANCE_HIGH);
mChannel.enableLights(true);
String color = alarm.getColor();
if (color != null && !color.equals("")) {
mChannel.setLightColor(Color.parseColor(color));
}
if (mChannel.canBypassDnd()) {
mChannel.setBypassDnd(alarm.isBypassDnd());
}
if (alarm.isVibrate()) {
mChannel.setVibrationPattern(vibrationPattern);
mChannel.enableVibration(true);
}
mNotificationManager.createNotificationChannel(mChannel);
mBuilder.setChannelId(channelID);
} else {
// set vibration
mBuilder.setVibrate(alarm.isVibrate() ? vibrationPattern : null);
}
//color
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
String color = alarm.getColor();
if (color != null && !color.equals("")) {
mBuilder.setColor(Color.parseColor(color));
}
}
mBuilder.setContentIntent(pendingIntent);
if (alarm.isHasButton()) {
Intent dismissIntent = new Intent(context, AlarmReceiver.class);
dismissIntent.setAction(NOTIFICATION_ACTION_DISMISS);
dismissIntent.putExtra("AlarmId", alarm.getId());
PendingIntent pendingDismiss = PendingIntent.getBroadcast(context, notificationID, dismissIntent, defaultFlags | PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action dismissAction = new NotificationCompat.Action(android.R.drawable.ic_lock_idle_alarm, "DISMISS", pendingDismiss);
mBuilder.addAction(dismissAction);
Intent snoozeIntent = new Intent(context, AlarmReceiver.class);
snoozeIntent.setAction(NOTIFICATION_ACTION_SNOOZE);
snoozeIntent.putExtra("SnoozeAlarmId", alarm.getId());
PendingIntent pendingSnooze = PendingIntent.getBroadcast(context, notificationID, snoozeIntent, defaultFlags | PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action snoozeAction = new NotificationCompat.Action(R.drawable.ic_snooze, "SNOOZE", pendingSnooze);
mBuilder.addAction(snoozeAction);
}
//use big text
if (alarm.isUseBigText()) {
mBuilder = mBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(message));
}
//large icon
String largeIcon = alarm.getLargeIcon();
if (largeIcon != null && !largeIcon.equals("") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName);
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
if (largeIconResId != 0) {
mBuilder.setLargeIcon(largeIconBitmap);
}
}
// set tag and push notification
Notification notification = mBuilder.build();
String tag = alarm.getTag();
if (tag != null && !tag.equals("")) {
mNotificationManager.notify(tag, notificationID, notification);
} else {
Log.i(Constants.TAG, "Notification done");
mNotificationManager.notify(notificationID, notification);
}
} catch (Exception e) {
Log.e(Constants.TAG, "Failed to send notification", e);
}
}
void removeFiredNotification(int id) {
try {
AlarmModel alarm = alarmDB.getAlarm(id);
getNotificationManager().cancel(alarm.getAlarmId());
} catch (Exception e) {
Log.e(Constants.TAG, "Could not remove fired notification with id " + id, e);
}
}
void removeAllFiredNotifications() {
getNotificationManager().cancelAll();
}
ArrayList<AlarmModel> getAlarms() {
return alarmDB.getAlarmList(1);
}
WritableMap convertJsonToMap(JSONObject jsonObject) throws JSONException {
WritableMap map = new WritableNativeMap();
Iterator<String> iterator = jsonObject.keys();
while (iterator.hasNext()) {
String key = iterator.next();
Object value = jsonObject.get(key);
if (value instanceof JSONObject) {
map.putMap(key, convertJsonToMap((JSONObject) value));
} else if (value instanceof Boolean) {
map.putBoolean(key, (Boolean) value);
} else if (value instanceof Integer) {
map.putInt(key, (Integer) value);
} else if (value instanceof Double) {
map.putDouble(key, (Double) value);
} else if (value instanceof String) {
map.putString(key, (String) value);
} else {
map.putString(key, value.toString());
}
}
return map;
}
}

View File

@@ -0,0 +1,14 @@
package com.emekalites.react.alarm.notification;
class Constants {
static final String TAG = "RNAlarmNotification";
// TODO convert to action
static final String ADD_INTENT = "com.emekalites.react.alarm.notification.ADD_INTENT";
static final String NOTIFICATION_ID = "com.emekalites.react.alarm.notification.NOTIFICATION_ID";
static final String NOTIFICATION_ACTION_DISMISS = "com.emekalites.react.alarm.notification.ACTION_DISMISS";
static final String NOTIFICATION_ACTION_SNOOZE = "com.emekalites.react.alarm.notification.ACTION_SNOOZE";
static final String NOTIFICATION_ACTION_CLICK = "com.emekalites.react.alarm.notification.ACTION_CLICK";
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF707070"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.93 6,11v5l-2,2v1h16v-1l-2,-2zM14.5,9.8l-2.8,3.4h2.8L14.5,15h-5v-1.8l2.8,-3.4L9.5,9.8L9.5,8h5v1.8z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More