mirror of
https://github.com/tauri-apps/tauri-plugin-notification.git
synced 2026-01-31 00:55:18 +01:00
chore: Add mirror for notification plugin (#397)
Committed via a GitHub action: https://github.com/tauri-apps/plugins-workspace/actions/runs/5067653223 Co-authored-by: FabianLars <FabianLars@users.noreply.github.com>
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/.tauri
|
||||
32
Cargo.toml
Normal file
32
Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "1.0.0"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
links = "tauri-plugin-notification"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rand = "0.8"
|
||||
time = { version = "0.3", features = ["serde", "parsing", "formatting"] }
|
||||
url = { version = "2", features = ["serde"] }
|
||||
serde_repr = "0.1"
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
notify-rust = "4.5"
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
win7-notifications = { version = "0.3.1", optional = true }
|
||||
|
||||
[features]
|
||||
windows7-compat = [ "win7-notifications" ]
|
||||
20
LICENSE.spdx
Normal file
20
LICENSE.spdx
Normal file
@@ -0,0 +1,20 @@
|
||||
SPDXVersion: SPDX-2.1
|
||||
DataLicense: CC0-1.0
|
||||
PackageName: tauri
|
||||
DataFormat: SPDXRef-1
|
||||
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
|
||||
PackageHomePage: https://tauri.app
|
||||
PackageLicenseDeclared: Apache-2.0
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy
|
||||
PackageSummary: <text>Tauri is a rust project that enables developers to make secure
|
||||
and small desktop applications using a web frontend.
|
||||
</text>
|
||||
PackageComment: <text>The package includes the following libraries; see
|
||||
Relationship information.
|
||||
</text>
|
||||
Created: 2019-05-20T09:00:00Z
|
||||
PackageDownloadLocation: git://github.com/tauri-apps/tauri
|
||||
PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git
|
||||
PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git
|
||||
Creator: Person: Daniel Thompson-Yvetot
|
||||
177
LICENSE_APACHE-2.0
Normal file
177
LICENSE_APACHE-2.0
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
21
LICENSE_MIT
Normal file
21
LICENSE_MIT
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 - Present Tauri Apps Contributors
|
||||
|
||||
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.
|
||||
74
README.md
Normal file
74
README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Notification
|
||||
|
||||
Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API.
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.65**_
|
||||
|
||||
There are three general methods of installation that we can recommend.
|
||||
|
||||
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
|
||||
2. Pull sources directly from Github using git tags / revision hashes (most secure)
|
||||
3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use)
|
||||
|
||||
Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||
|
||||
`src-tauri/Cargo.toml`
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri-plugin-notification = "2.0.0-alpha"
|
||||
# alternatively with Git:
|
||||
tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
```
|
||||
|
||||
You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
|
||||
|
||||
> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
|
||||
|
||||
```sh
|
||||
pnpm add @tauri-apps/plugin-notification
|
||||
# or
|
||||
npm add @tauri-apps/plugin-notification
|
||||
# or
|
||||
yarn add @tauri-apps/plugin-notification
|
||||
|
||||
# alternatively with Git:
|
||||
pnpm add https://github.com/tauri-apps/tauri-plugin-notification#v2
|
||||
# or
|
||||
npm add https://github.com/tauri-apps/tauri-plugin-notification#v2
|
||||
# or
|
||||
yarn add https://github.com/tauri-apps/tauri-plugin-notification#v2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
First you need to register the core plugin with Tauri:
|
||||
|
||||
`src-tauri/src/main.rs`
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
```
|
||||
|
||||
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
|
||||
|
||||
```javascript
|
||||
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
|
||||
|
||||
## License
|
||||
|
||||
Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy.
|
||||
|
||||
MIT or MIT/Apache 2.0 where applicable.
|
||||
2
android/.gitignore
vendored
Normal file
2
android/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.tauri
|
||||
45
android/build.gradle.kts
Normal file
45
android/build.gradle.kts
Normal file
@@ -0,0 +1,45 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.tauri.notification"
|
||||
compileSdk = 33
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
targetSdk = 33
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
implementation(project(":tauri-android"))
|
||||
}
|
||||
21
android/proguard-rules.pro
vendored
Normal file
21
android/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
2
android/settings.gradle
Normal file
2
android/settings.gradle
Normal file
@@ -0,0 +1,2 @@
|
||||
include ':tauri-android'
|
||||
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
||||
28
android/src/androidTest/java/ExampleInstrumentedTest.kt
Normal file
28
android/src/androidTest/java/ExampleInstrumentedTest.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("app.tauri.notification", appContext.packageName)
|
||||
}
|
||||
}
|
||||
19
android/src/main/AndroidManifest.xml
Normal file
19
android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<receiver android:name="app.tauri.notification.TimedNotificationPublisher" />
|
||||
<receiver android:name="app.tauri.notification.NotificationDismissReceiver" />
|
||||
<receiver
|
||||
android:name="app.tauri.notification.NotificationRestoreReceiver"
|
||||
android:directBootAware="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
</manifest>
|
||||
29
android/src/main/java/AssetUtils.kt
Normal file
29
android/src/main/java/AssetUtils.kt
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
|
||||
class AssetUtils {
|
||||
companion object {
|
||||
const val RESOURCE_ID_ZERO_VALUE = 0
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
fun getResourceID(context: Context, resourceName: String?, dir: String?): Int {
|
||||
return context.resources.getIdentifier(resourceName, dir, context.packageName)
|
||||
}
|
||||
|
||||
fun getResourceBaseName(resPath: String?): String? {
|
||||
if (resPath == null) return null
|
||||
if (resPath.contains("/")) {
|
||||
return resPath.substring(resPath.lastIndexOf('/') + 1)
|
||||
}
|
||||
return if (resPath.contains(".")) {
|
||||
resPath.substring(0, resPath.lastIndexOf('.'))
|
||||
} else resPath
|
||||
}
|
||||
}
|
||||
}
|
||||
154
android/src/main/java/ChannelManager.kt
Normal file
154
android/src/main/java/ChannelManager.kt
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.media.AudioAttributes
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import app.tauri.Logger
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSArray
|
||||
import app.tauri.plugin.JSObject
|
||||
|
||||
private const val CHANNEL_ID = "id"
|
||||
private const val CHANNEL_NAME = "name"
|
||||
private const val CHANNEL_DESCRIPTION = "description"
|
||||
private const val CHANNEL_IMPORTANCE = "importance"
|
||||
private const val CHANNEL_VISIBILITY = "visibility"
|
||||
private const val CHANNEL_SOUND = "sound"
|
||||
private const val CHANNEL_VIBRATE = "vibration"
|
||||
private const val CHANNEL_USE_LIGHTS = "lights"
|
||||
private const val CHANNEL_LIGHT_COLOR = "lightColor"
|
||||
|
||||
class ChannelManager(private var context: Context) {
|
||||
private var notificationManager: NotificationManager? = null
|
||||
|
||||
init {
|
||||
notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?
|
||||
}
|
||||
|
||||
fun createChannel(invoke: Invoke) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = JSObject()
|
||||
if (invoke.getString(CHANNEL_ID) != null) {
|
||||
channel.put(CHANNEL_ID, invoke.getString(CHANNEL_ID))
|
||||
} else {
|
||||
invoke.reject("Channel missing identifier")
|
||||
return
|
||||
}
|
||||
if (invoke.getString(CHANNEL_NAME) != null) {
|
||||
channel.put(CHANNEL_NAME, invoke.getString(CHANNEL_NAME))
|
||||
} else {
|
||||
invoke.reject("Channel missing name")
|
||||
return
|
||||
}
|
||||
channel.put(
|
||||
CHANNEL_IMPORTANCE,
|
||||
invoke.getInt(CHANNEL_IMPORTANCE, NotificationManager.IMPORTANCE_DEFAULT)
|
||||
)
|
||||
channel.put(CHANNEL_DESCRIPTION, invoke.getString(CHANNEL_DESCRIPTION, ""))
|
||||
channel.put(
|
||||
CHANNEL_VISIBILITY,
|
||||
invoke.getInt(CHANNEL_VISIBILITY, NotificationCompat.VISIBILITY_PUBLIC)
|
||||
)
|
||||
channel.put(CHANNEL_SOUND, invoke.getString(CHANNEL_SOUND))
|
||||
channel.put(CHANNEL_VIBRATE, invoke.getBoolean(CHANNEL_VIBRATE, false))
|
||||
channel.put(CHANNEL_USE_LIGHTS, invoke.getBoolean(CHANNEL_USE_LIGHTS, false))
|
||||
channel.put(CHANNEL_LIGHT_COLOR, invoke.getString(CHANNEL_LIGHT_COLOR))
|
||||
createChannel(channel)
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("channel not available")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChannel(channel: JSObject) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationChannel = NotificationChannel(
|
||||
channel.getString(CHANNEL_ID),
|
||||
channel.getString(CHANNEL_NAME),
|
||||
channel.getInteger(CHANNEL_IMPORTANCE)!!
|
||||
)
|
||||
notificationChannel.description = channel.getString(CHANNEL_DESCRIPTION)
|
||||
notificationChannel.lockscreenVisibility = channel.getInteger(CHANNEL_VISIBILITY, android.app.Notification.VISIBILITY_PRIVATE)
|
||||
notificationChannel.enableVibration(channel.getBoolean(CHANNEL_VIBRATE, false))
|
||||
notificationChannel.enableLights(channel.getBoolean(CHANNEL_USE_LIGHTS, false))
|
||||
val lightColor = channel.getString(CHANNEL_LIGHT_COLOR)
|
||||
if (lightColor.isNotEmpty()) {
|
||||
try {
|
||||
notificationChannel.lightColor = Color.parseColor(lightColor)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
Logger.error(
|
||||
Logger.tags("NotificationChannel"),
|
||||
"Invalid color provided for light color.",
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
var sound = channel.getString(CHANNEL_SOUND)
|
||||
if (sound.isNotEmpty()) {
|
||||
if (sound.contains(".")) {
|
||||
sound = sound.substring(0, sound.lastIndexOf('.'))
|
||||
}
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.build()
|
||||
val soundUri =
|
||||
Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/" + sound)
|
||||
notificationChannel.setSound(soundUri, audioAttributes)
|
||||
}
|
||||
notificationManager?.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteChannel(invoke: Invoke) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channelId = invoke.getString("id")
|
||||
notificationManager?.deleteNotificationChannel(channelId)
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("channel not available")
|
||||
}
|
||||
}
|
||||
|
||||
fun listChannels(invoke: Invoke) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationChannels: List<NotificationChannel> =
|
||||
notificationManager?.notificationChannels ?: listOf()
|
||||
val channels = JSArray()
|
||||
for (notificationChannel in notificationChannels) {
|
||||
val channel = JSObject()
|
||||
channel.put(CHANNEL_ID, notificationChannel.id)
|
||||
channel.put(CHANNEL_NAME, notificationChannel.name)
|
||||
channel.put(CHANNEL_DESCRIPTION, notificationChannel.description)
|
||||
channel.put(CHANNEL_IMPORTANCE, notificationChannel.importance)
|
||||
channel.put(CHANNEL_VISIBILITY, notificationChannel.lockscreenVisibility)
|
||||
channel.put(CHANNEL_SOUND, notificationChannel.sound)
|
||||
channel.put(CHANNEL_VIBRATE, notificationChannel.shouldVibrate())
|
||||
channel.put(CHANNEL_USE_LIGHTS, notificationChannel.shouldShowLights())
|
||||
channel.put(
|
||||
CHANNEL_LIGHT_COLOR, String.format(
|
||||
"#%06X",
|
||||
0xFFFFFF and notificationChannel.lightColor
|
||||
)
|
||||
)
|
||||
channels.put(channel)
|
||||
}
|
||||
val result = JSObject()
|
||||
result.put("channels", channels)
|
||||
invoke.resolve(result)
|
||||
} else {
|
||||
invoke.reject("channel not available")
|
||||
}
|
||||
}
|
||||
}
|
||||
169
android/src/main/java/Notification.kt
Normal file
169
android/src/main/java/Notification.kt
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import app.tauri.plugin.JSArray
|
||||
import app.tauri.plugin.JSObject
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
class Notification {
|
||||
var title: String? = null
|
||||
var body: String? = null
|
||||
var largeBody: String? = null
|
||||
var summary: String? = null
|
||||
var id: Int = 0
|
||||
private var sound: String? = null
|
||||
private var smallIcon: String? = null
|
||||
private var largeIcon: String? = null
|
||||
var iconColor: String? = null
|
||||
var actionTypeId: String? = null
|
||||
var group: String? = null
|
||||
var inboxLines: List<String>? = null
|
||||
var isGroupSummary = false
|
||||
var isOngoing = false
|
||||
var isAutoCancel = false
|
||||
var extra: JSObject? = null
|
||||
var attachments: List<NotificationAttachment>? = null
|
||||
var schedule: NotificationSchedule? = null
|
||||
var channelId: String? = null
|
||||
var source: JSObject? = null
|
||||
var visibility: Int? = null
|
||||
var number: Int? = null
|
||||
|
||||
fun getSound(context: Context, defaultSound: Int): String? {
|
||||
var soundPath: String? = null
|
||||
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
val name = AssetUtils.getResourceBaseName(sound)
|
||||
if (name != null) {
|
||||
resId = AssetUtils.getResourceID(context, name, "raw")
|
||||
}
|
||||
if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) {
|
||||
resId = defaultSound
|
||||
}
|
||||
if (resId != AssetUtils.RESOURCE_ID_ZERO_VALUE) {
|
||||
soundPath =
|
||||
ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + resId
|
||||
}
|
||||
return soundPath
|
||||
}
|
||||
|
||||
fun setSound(sound: String?) {
|
||||
this.sound = sound
|
||||
}
|
||||
|
||||
fun setSmallIcon(smallIcon: String?) {
|
||||
this.smallIcon = AssetUtils.getResourceBaseName(smallIcon)
|
||||
}
|
||||
|
||||
fun setLargeIcon(largeIcon: String?) {
|
||||
this.largeIcon = AssetUtils.getResourceBaseName(largeIcon)
|
||||
}
|
||||
|
||||
fun getIconColor(globalColor: String): String {
|
||||
// use the one defined local before trying for a globally defined color
|
||||
return iconColor ?: globalColor
|
||||
}
|
||||
|
||||
fun getSmallIcon(context: Context, defaultIcon: Int): Int {
|
||||
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
if (smallIcon != null) {
|
||||
resId = AssetUtils.getResourceID(context, smallIcon, "drawable")
|
||||
}
|
||||
if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) {
|
||||
resId = defaultIcon
|
||||
}
|
||||
return resId
|
||||
}
|
||||
|
||||
fun getLargeIcon(context: Context): Bitmap? {
|
||||
if (largeIcon != null) {
|
||||
val resId: Int = AssetUtils.getResourceID(context, largeIcon, "drawable")
|
||||
return BitmapFactory.decodeResource(context.resources, resId)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val isScheduled = schedule != null
|
||||
|
||||
companion object {
|
||||
fun fromJson(jsonNotification: JSONObject): Notification {
|
||||
val notification: JSObject = try {
|
||||
val identifier = jsonNotification.getLong("id")
|
||||
if (identifier > Int.MAX_VALUE || identifier < Int.MIN_VALUE) {
|
||||
throw Exception("The notification identifier should be a 32-bit integer")
|
||||
}
|
||||
JSObject.fromJSONObject(jsonNotification)
|
||||
} catch (e: JSONException) {
|
||||
throw Exception("Invalid notification JSON object", e)
|
||||
}
|
||||
return fromJSObject(notification)
|
||||
}
|
||||
|
||||
fun fromJSObject(jsonObject: JSObject): Notification {
|
||||
val notification = Notification()
|
||||
notification.source = jsonObject
|
||||
notification.id = jsonObject.getInteger("id") ?: throw Exception("Missing notification identifier")
|
||||
notification.body = jsonObject.getString("body", null)
|
||||
notification.largeBody = jsonObject.getString("largeBody", null)
|
||||
notification.summary = jsonObject.getString("summary", null)
|
||||
notification.actionTypeId = jsonObject.getString("actionTypeId", null)
|
||||
notification.group = jsonObject.getString("group", null)
|
||||
notification.setSound(jsonObject.getString("sound", null))
|
||||
notification.title = jsonObject.getString("title", null)
|
||||
notification.setSmallIcon(jsonObject.getString("icon", null))
|
||||
notification.setLargeIcon(jsonObject.getString("largeIcon", null))
|
||||
notification.iconColor = jsonObject.getString("iconColor", null)
|
||||
notification.attachments = NotificationAttachment.getAttachments(jsonObject)
|
||||
notification.isGroupSummary = jsonObject.getBoolean("groupSummary", false)
|
||||
notification.channelId = jsonObject.getString("channelId", null)
|
||||
val schedule = jsonObject.getJSObject("schedule")
|
||||
if (schedule != null) {
|
||||
notification.schedule = NotificationSchedule(schedule)
|
||||
}
|
||||
notification.extra = jsonObject.getJSObject("extra")
|
||||
notification.isOngoing = jsonObject.getBoolean("ongoing", false)
|
||||
notification.isAutoCancel = jsonObject.getBoolean("autoCancel", true)
|
||||
notification.visibility = jsonObject.getInteger("visibility")
|
||||
notification.number = jsonObject.getInteger("number")
|
||||
try {
|
||||
val inboxLines = jsonObject.getJSONArray("inboxLines")
|
||||
val inboxStringList: MutableList<String> = ArrayList()
|
||||
for (i in 0 until inboxLines.length()) {
|
||||
inboxStringList.add(inboxLines.getString(i))
|
||||
}
|
||||
notification.inboxLines = inboxStringList
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
return notification
|
||||
}
|
||||
|
||||
fun buildNotificationPendingList(notifications: List<Notification>): JSObject {
|
||||
val result = JSObject()
|
||||
val jsArray = JSArray()
|
||||
for (notification in notifications) {
|
||||
val jsNotification = JSObject()
|
||||
jsNotification.put("id", notification.id)
|
||||
jsNotification.put("title", notification.title)
|
||||
jsNotification.put("body", notification.body)
|
||||
val schedule = notification.schedule
|
||||
if (schedule != null) {
|
||||
val jsSchedule = JSObject()
|
||||
jsSchedule.put("kind", schedule.scheduleObj.getString("kind", null))
|
||||
jsSchedule.put("data", schedule.scheduleObj.getJSObject("data"))
|
||||
jsNotification.put("schedule", jsSchedule)
|
||||
}
|
||||
jsNotification.put("extra", notification.extra)
|
||||
jsArray.put(jsNotification)
|
||||
}
|
||||
result.put("notifications", jsArray)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
51
android/src/main/java/NotificationAction.kt
Normal file
51
android/src/main/java/NotificationAction.kt
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import app.tauri.Logger
|
||||
import app.tauri.plugin.JSArray
|
||||
import app.tauri.plugin.JSObject
|
||||
import org.json.JSONObject
|
||||
|
||||
class NotificationAction() {
|
||||
var id: String? = null
|
||||
var title: String? = null
|
||||
var input = false
|
||||
|
||||
constructor(id: String?, title: String?, input: Boolean): this() {
|
||||
this.id = id
|
||||
this.title = title
|
||||
this.input = input
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun buildTypes(types: JSArray): Map<String, List<NotificationAction>> {
|
||||
val actionTypeMap: MutableMap<String, List<NotificationAction>> = HashMap()
|
||||
try {
|
||||
val objects: List<JSONObject> = types.toList()
|
||||
for (obj in objects) {
|
||||
val jsObject = JSObject.fromJSONObject(
|
||||
obj
|
||||
)
|
||||
val actionGroupId = jsObject.getString("id")
|
||||
val actions = jsObject.getJSONArray("actions")
|
||||
val typesArray = mutableListOf<NotificationAction>()
|
||||
for (i in 0 until actions.length()) {
|
||||
val notificationAction = NotificationAction()
|
||||
val action = JSObject.fromJSONObject(actions.getJSONObject(i))
|
||||
notificationAction.id = action.getString("id")
|
||||
notificationAction.title = action.getString("title")
|
||||
notificationAction.input = action.getBoolean("input")
|
||||
typesArray.add(notificationAction)
|
||||
}
|
||||
actionTypeMap[actionGroupId] = typesArray.toList()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.error(Logger.tags("Notification"), "Error when building action types", e)
|
||||
}
|
||||
return actionTypeMap
|
||||
}
|
||||
}
|
||||
}
|
||||
52
android/src/main/java/NotificationAttachment.kt
Normal file
52
android/src/main/java/NotificationAttachment.kt
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import app.tauri.plugin.JSObject
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
class NotificationAttachment {
|
||||
var id: String? = null
|
||||
var url: String? = null
|
||||
var options: JSONObject? = null
|
||||
|
||||
companion object {
|
||||
fun getAttachments(notification: JSObject): List<NotificationAttachment> {
|
||||
val attachmentsList: MutableList<NotificationAttachment> = ArrayList()
|
||||
var attachments: JSONArray? = null
|
||||
try {
|
||||
attachments = notification.getJSONArray("attachments")
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
if (attachments != null) {
|
||||
for (i in 0 until attachments.length()) {
|
||||
val newAttachment = NotificationAttachment()
|
||||
var jsonObject: JSONObject? = null
|
||||
try {
|
||||
jsonObject = attachments.getJSONObject(i)
|
||||
} catch (e: JSONException) {
|
||||
}
|
||||
if (jsonObject != null) {
|
||||
var jsObject: JSObject? = null
|
||||
try {
|
||||
jsObject = JSObject.fromJSONObject(jsonObject)
|
||||
} catch (_: JSONException) {
|
||||
}
|
||||
newAttachment.id = jsObject!!.getString("id")
|
||||
newAttachment.url = jsObject.getString("url")
|
||||
try {
|
||||
newAttachment.options = jsObject.getJSONObject("options")
|
||||
} catch (_: JSONException) {
|
||||
}
|
||||
attachmentsList.add(newAttachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
return attachmentsList
|
||||
}
|
||||
}
|
||||
}
|
||||
267
android/src/main/java/NotificationPlugin.kt
Normal file
267
android/src/main/java/NotificationPlugin.kt
Normal file
@@ -0,0 +1,267 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.webkit.WebView
|
||||
import app.tauri.PermissionState
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.Permission
|
||||
import app.tauri.annotation.PermissionCallback
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSArray
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
const val LOCAL_NOTIFICATIONS = "permissionState"
|
||||
|
||||
@TauriPlugin(
|
||||
permissions = [
|
||||
Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "permissionState")
|
||||
]
|
||||
)
|
||||
class NotificationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
private var webView: WebView? = null
|
||||
private lateinit var manager: TauriNotificationManager
|
||||
private lateinit var notificationManager: NotificationManager
|
||||
private lateinit var notificationStorage: NotificationStorage
|
||||
private var channelManager = ChannelManager(activity)
|
||||
|
||||
companion object {
|
||||
var instance: NotificationPlugin? = null
|
||||
|
||||
fun triggerNotification(notification: JSObject) {
|
||||
instance?.trigger("notification", notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun load(webView: WebView) {
|
||||
instance = this
|
||||
|
||||
super.load(webView)
|
||||
this.webView = webView
|
||||
notificationStorage = NotificationStorage(activity)
|
||||
|
||||
val manager = TauriNotificationManager(
|
||||
notificationStorage,
|
||||
activity,
|
||||
activity,
|
||||
getConfig()
|
||||
)
|
||||
manager.createNotificationChannel()
|
||||
|
||||
this.manager = manager
|
||||
|
||||
notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
if (Intent.ACTION_MAIN != intent.action) {
|
||||
return
|
||||
}
|
||||
val dataJson = manager.handleNotificationActionPerformed(intent, notificationStorage)
|
||||
if (dataJson != null) {
|
||||
trigger("actionPerformed", dataJson)
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun show(invoke: Invoke) {
|
||||
val notification = Notification.fromJSObject(invoke.data)
|
||||
val id = manager.schedule(notification)
|
||||
|
||||
val returnVal = JSObject().put("id", id)
|
||||
invoke.resolve(returnVal)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun batch(invoke: Invoke) {
|
||||
val notificationArray = invoke.getArray("notifications")
|
||||
if (notificationArray == null) {
|
||||
invoke.reject("Missing `notifications` argument")
|
||||
return
|
||||
}
|
||||
|
||||
val notifications: MutableList<Notification> =
|
||||
ArrayList(notificationArray.length())
|
||||
val notificationsInput: List<JSONObject> = try {
|
||||
notificationArray.toList()
|
||||
} catch (e: JSONException) {
|
||||
invoke.reject("Provided notification format is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
for (jsonNotification in notificationsInput) {
|
||||
val notification = Notification.fromJson(jsonNotification)
|
||||
notifications.add(notification)
|
||||
}
|
||||
|
||||
val ids = manager.schedule(notifications)
|
||||
notificationStorage.appendNotifications(notifications)
|
||||
|
||||
val result = JSObject()
|
||||
result.put("notifications", ids)
|
||||
invoke.resolve(result)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun cancel(invoke: Invoke) {
|
||||
val notifications: List<Int> = invoke.getArray("notifications", JSArray()).toList()
|
||||
if (notifications.isEmpty()) {
|
||||
invoke.reject("Must provide notifications array as notifications option")
|
||||
return
|
||||
}
|
||||
|
||||
manager.cancel(notifications)
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@Command
|
||||
fun removeActive(invoke: Invoke) {
|
||||
val notifications = invoke.getArray("notifications")
|
||||
if (notifications == null) {
|
||||
notificationManager.cancelAll()
|
||||
invoke.resolve()
|
||||
} else {
|
||||
try {
|
||||
for (o in notifications.toList<Any>()) {
|
||||
if (o is JSONObject) {
|
||||
val notification = JSObject.fromJSONObject((o))
|
||||
val tag = notification.getString("tag", null)
|
||||
val id = notification.getInteger("id", 0)
|
||||
if (tag == null) {
|
||||
notificationManager.cancel(id)
|
||||
} else {
|
||||
notificationManager.cancel(tag, id)
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Unexpected notification type")
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
invoke.reject(e.message)
|
||||
}
|
||||
invoke.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun getPending(invoke: Invoke) {
|
||||
val notifications= notificationStorage.getSavedNotifications()
|
||||
val result = Notification.buildNotificationPendingList(notifications)
|
||||
invoke.resolve(result)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun registerActionTypes(invoke: Invoke) {
|
||||
val types = invoke.getArray("types", JSArray())
|
||||
val typesArray = NotificationAction.buildTypes(types)
|
||||
notificationStorage.writeActionGroup(typesArray)
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@Command
|
||||
fun getActive(invoke: Invoke) {
|
||||
val notifications = JSArray()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val activeNotifications = notificationManager.activeNotifications
|
||||
for (activeNotification in activeNotifications) {
|
||||
val jsNotification = JSObject()
|
||||
jsNotification.put("id", activeNotification.id)
|
||||
jsNotification.put("tag", activeNotification.tag)
|
||||
val notification = activeNotification.notification
|
||||
if (notification != null) {
|
||||
jsNotification.put("title", notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE))
|
||||
jsNotification.put("body", notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT))
|
||||
jsNotification.put("group", notification.group)
|
||||
jsNotification.put(
|
||||
"groupSummary",
|
||||
0 != notification.flags and android.app.Notification.FLAG_GROUP_SUMMARY
|
||||
)
|
||||
val extras = JSObject()
|
||||
for (key in notification.extras.keySet()) {
|
||||
extras.put(key!!, notification.extras.getString(key))
|
||||
}
|
||||
jsNotification.put("data", extras)
|
||||
}
|
||||
notifications.put(jsNotification)
|
||||
}
|
||||
}
|
||||
val result = JSObject()
|
||||
result.put("notifications", notifications)
|
||||
invoke.resolve(result)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun createChannel(invoke: Invoke) {
|
||||
channelManager.createChannel(invoke)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun deleteChannel(invoke: Invoke) {
|
||||
channelManager.deleteChannel(invoke)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun listChannels(invoke: Invoke) {
|
||||
channelManager.listChannels(invoke)
|
||||
}
|
||||
|
||||
@Command
|
||||
override fun checkPermissions(invoke: Invoke) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
val permissionsResultJSON = JSObject()
|
||||
permissionsResultJSON.put("permissionState", getPermissionState())
|
||||
invoke.resolve(permissionsResultJSON)
|
||||
} else {
|
||||
super.checkPermissions(invoke)
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
override fun requestPermissions(invoke: Invoke) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
permissionState(invoke)
|
||||
} else {
|
||||
if (getPermissionState(LOCAL_NOTIFICATIONS) !== PermissionState.GRANTED) {
|
||||
requestPermissionForAlias(LOCAL_NOTIFICATIONS, invoke, "permissionsCallback")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun permissionState(invoke: Invoke) {
|
||||
val permissionsResultJSON = JSObject()
|
||||
permissionsResultJSON.put("permissionState", getPermissionState())
|
||||
invoke.resolve(permissionsResultJSON)
|
||||
}
|
||||
|
||||
@PermissionCallback
|
||||
private fun permissionsCallback(invoke: Invoke) {
|
||||
val permissionsResultJSON = JSObject()
|
||||
permissionsResultJSON.put("display", getPermissionState())
|
||||
invoke.resolve(permissionsResultJSON)
|
||||
}
|
||||
|
||||
private fun getPermissionState(): String {
|
||||
return if (manager.areNotificationsEnabled()) {
|
||||
"granted"
|
||||
} else {
|
||||
"denied"
|
||||
}
|
||||
}
|
||||
}
|
||||
309
android/src/main/java/NotificationSchedule.kt
Normal file
309
android/src/main/java/NotificationSchedule.kt
Normal file
@@ -0,0 +1,309 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.text.format.DateUtils
|
||||
import app.tauri.plugin.JSObject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
const val JS_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||
|
||||
enum class NotificationInterval {
|
||||
Year, Month, TwoWeeks, Week, Day, Hour, Minute, Second
|
||||
}
|
||||
|
||||
fun getIntervalTime(interval: NotificationInterval, count: Int): Long {
|
||||
return when (interval) {
|
||||
// This case is just approximation as not all years have the same number of days
|
||||
NotificationInterval.Year -> count * DateUtils.WEEK_IN_MILLIS * 52
|
||||
// This case is just approximation as months have different number of days
|
||||
NotificationInterval.Month -> count * 30 * DateUtils.DAY_IN_MILLIS
|
||||
NotificationInterval.TwoWeeks -> count * 2 * DateUtils.WEEK_IN_MILLIS
|
||||
NotificationInterval.Week -> count * DateUtils.WEEK_IN_MILLIS
|
||||
NotificationInterval.Day -> count * DateUtils.DAY_IN_MILLIS
|
||||
NotificationInterval.Hour -> count * DateUtils.HOUR_IN_MILLIS
|
||||
NotificationInterval.Minute -> count * DateUtils.MINUTE_IN_MILLIS
|
||||
NotificationInterval.Second -> count * DateUtils.SECOND_IN_MILLIS
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ScheduleKind {
|
||||
// At specific moment of time (with repeating option)
|
||||
class At(var date: Date, val repeating: Boolean): ScheduleKind()
|
||||
class Interval(val interval: DateMatch): ScheduleKind()
|
||||
class Every(val interval: NotificationInterval, val count: Int): ScheduleKind()
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
class NotificationSchedule(val scheduleObj: JSObject) {
|
||||
val kind: ScheduleKind
|
||||
// Schedule this notification to fire even if app is idled (Doze)
|
||||
var whileIdle: Boolean = false
|
||||
|
||||
init {
|
||||
val payload = scheduleObj.getJSObject("data", JSObject())
|
||||
|
||||
when (val scheduleKind = scheduleObj.getString("kind", "")) {
|
||||
"At" -> {
|
||||
val dateString = payload.getString("date")
|
||||
if (dateString.isNotEmpty()) {
|
||||
val sdf = SimpleDateFormat(JS_DATE_FORMAT)
|
||||
sdf.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val at = sdf.parse(dateString)
|
||||
if (at == null) {
|
||||
throw Exception("could not parse `at` date")
|
||||
} else {
|
||||
kind = ScheduleKind.At(at, payload.getBoolean("repeating"))
|
||||
}
|
||||
} else {
|
||||
throw Exception("`at` date cannot be empty")
|
||||
}
|
||||
}
|
||||
"Interval" -> {
|
||||
val dateMatch = onFromJson(payload)
|
||||
kind = ScheduleKind.Interval(dateMatch)
|
||||
}
|
||||
"Every" -> {
|
||||
val interval = NotificationInterval.valueOf(payload.getString("interval"))
|
||||
kind = ScheduleKind.Every(interval, payload.getInteger("count", 1))
|
||||
}
|
||||
else -> {
|
||||
throw Exception("Unknown schedule kind $scheduleKind")
|
||||
}
|
||||
}
|
||||
whileIdle = scheduleObj.getBoolean("allowWhileIdle", false)
|
||||
}
|
||||
|
||||
private fun onFromJson(onJson: JSObject): DateMatch {
|
||||
val match = DateMatch()
|
||||
match.year = onJson.getInteger("year")
|
||||
match.month = onJson.getInteger("month")
|
||||
match.day = onJson.getInteger("day")
|
||||
match.weekday = onJson.getInteger("weekday")
|
||||
match.hour = onJson.getInteger("hour")
|
||||
match.minute = onJson.getInteger("minute")
|
||||
match.second = onJson.getInteger("second")
|
||||
return match
|
||||
}
|
||||
|
||||
fun isRemovable(): Boolean {
|
||||
return when (kind) {
|
||||
is ScheduleKind.At -> !kind.repeating
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DateMatch {
|
||||
var year: Int? = null
|
||||
var month: Int? = null
|
||||
var day: Int? = null
|
||||
var weekday: Int? = null
|
||||
var hour: Int? = null
|
||||
var minute: Int? = null
|
||||
var second: Int? = null
|
||||
|
||||
// Unit used to save the last used unit for a trigger.
|
||||
// One of the Calendar constants values
|
||||
var unit: Int? = -1
|
||||
|
||||
/**
|
||||
* Gets a calendar instance pointing to the specified date.
|
||||
*
|
||||
* @param date The date to point.
|
||||
*/
|
||||
private fun buildCalendar(date: Date): Calendar {
|
||||
val cal: Calendar = Calendar.getInstance()
|
||||
cal.time = date
|
||||
cal.set(Calendar.MILLISECOND, 0)
|
||||
return cal
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates next trigger date for
|
||||
*
|
||||
* @param date base date used to calculate trigger
|
||||
* @return next trigger timestamp
|
||||
*/
|
||||
fun nextTrigger(date: Date): Long {
|
||||
val current: Calendar = buildCalendar(date)
|
||||
val next: Calendar = buildNextTriggerTime(date)
|
||||
return postponeTriggerIfNeeded(current, next)
|
||||
}
|
||||
|
||||
/**
|
||||
* Postpone trigger if first schedule matches the past
|
||||
*/
|
||||
private fun postponeTriggerIfNeeded(current: Calendar, next: Calendar): Long {
|
||||
if (next.timeInMillis <= current.timeInMillis && unit != -1) {
|
||||
var incrementUnit = -1
|
||||
if (unit == Calendar.YEAR || unit == Calendar.MONTH) {
|
||||
incrementUnit = Calendar.YEAR
|
||||
} else if (unit == Calendar.DAY_OF_MONTH) {
|
||||
incrementUnit = Calendar.MONTH
|
||||
} else if (unit == Calendar.DAY_OF_WEEK) {
|
||||
incrementUnit = Calendar.WEEK_OF_MONTH
|
||||
} else if (unit == Calendar.HOUR_OF_DAY) {
|
||||
incrementUnit = Calendar.DAY_OF_MONTH
|
||||
} else if (unit == Calendar.MINUTE) {
|
||||
incrementUnit = Calendar.HOUR_OF_DAY
|
||||
} else if (unit == Calendar.SECOND) {
|
||||
incrementUnit = Calendar.MINUTE
|
||||
}
|
||||
if (incrementUnit != -1) {
|
||||
next.set(incrementUnit, next.get(incrementUnit) + 1)
|
||||
}
|
||||
}
|
||||
return next.timeInMillis
|
||||
}
|
||||
|
||||
private fun buildNextTriggerTime(date: Date): Calendar {
|
||||
val next: Calendar = buildCalendar(date)
|
||||
if (year != null) {
|
||||
next.set(Calendar.YEAR, year ?: 0)
|
||||
if (unit == -1) unit = Calendar.YEAR
|
||||
}
|
||||
if (month != null) {
|
||||
next.set(Calendar.MONTH, month ?: 0)
|
||||
if (unit == -1) unit = Calendar.MONTH
|
||||
}
|
||||
if (day != null) {
|
||||
next.set(Calendar.DAY_OF_MONTH, day ?: 0)
|
||||
if (unit == -1) unit = Calendar.DAY_OF_MONTH
|
||||
}
|
||||
if (weekday != null) {
|
||||
next.set(Calendar.DAY_OF_WEEK, weekday ?: 0)
|
||||
if (unit == -1) unit = Calendar.DAY_OF_WEEK
|
||||
}
|
||||
if (hour != null) {
|
||||
next.set(Calendar.HOUR_OF_DAY, hour ?: 0)
|
||||
if (unit == -1) unit = Calendar.HOUR_OF_DAY
|
||||
}
|
||||
if (minute != null) {
|
||||
next.set(Calendar.MINUTE, minute ?: 0)
|
||||
if (unit == -1) unit = Calendar.MINUTE
|
||||
}
|
||||
if (second != null) {
|
||||
next.set(Calendar.SECOND, second ?: 0)
|
||||
if (unit == -1) unit = Calendar.SECOND
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "DateMatch{" +
|
||||
"year=" +
|
||||
year +
|
||||
", month=" +
|
||||
month +
|
||||
", day=" +
|
||||
day +
|
||||
", weekday=" +
|
||||
weekday +
|
||||
", hour=" +
|
||||
hour +
|
||||
", minute=" +
|
||||
minute +
|
||||
", second=" +
|
||||
second +
|
||||
'}'
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
val dateMatch = other as DateMatch
|
||||
if (if (year != null) year != dateMatch.year else dateMatch.year != null) return false
|
||||
if (if (month != null) month != dateMatch.month else dateMatch.month != null) return false
|
||||
if (if (day != null) day != dateMatch.day else dateMatch.day != null) return false
|
||||
if (if (weekday != null) weekday != dateMatch.weekday else dateMatch.weekday != null) return false
|
||||
if (if (hour != null) hour != dateMatch.hour else dateMatch.hour != null) return false
|
||||
if (if (minute != null) minute != dateMatch.minute else dateMatch.minute != null) return false
|
||||
return if (second != null) second == dateMatch.second else dateMatch.second == null
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = if (year != null) year.hashCode() else 0
|
||||
result = 31 * result + if (month != null) month.hashCode() else 0
|
||||
result = 31 * result + if (day != null) day.hashCode() else 0
|
||||
result = 31 * result + if (weekday != null) weekday.hashCode() else 0
|
||||
result = 31 * result + if (hour != null) hour.hashCode() else 0
|
||||
result = 31 * result + if (minute != null) minute.hashCode() else 0
|
||||
result += 31 + if (second != null) second.hashCode() else 0
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform DateMatch object to CronString
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
fun toMatchString(): String {
|
||||
val matchString = year.toString() +
|
||||
separator +
|
||||
month +
|
||||
separator +
|
||||
day +
|
||||
separator +
|
||||
weekday +
|
||||
separator +
|
||||
hour +
|
||||
separator +
|
||||
minute +
|
||||
separator +
|
||||
second +
|
||||
separator +
|
||||
unit
|
||||
return matchString.replace("null", "*")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val separator = " "
|
||||
|
||||
/**
|
||||
* Create DateMatch object from stored string
|
||||
*
|
||||
* @param matchString
|
||||
* @return
|
||||
*/
|
||||
fun fromMatchString(matchString: String): DateMatch {
|
||||
val date = DateMatch()
|
||||
val split = matchString.split(separator.toRegex()).dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray()
|
||||
if (split.size == 7) {
|
||||
date.year = getValueFromCronElement(split[0])
|
||||
date.month = getValueFromCronElement(split[1])
|
||||
date.day = getValueFromCronElement(split[2])
|
||||
date.weekday = getValueFromCronElement(split[3])
|
||||
date.hour = getValueFromCronElement(split[4])
|
||||
date.minute = getValueFromCronElement(split[5])
|
||||
date.unit = getValueFromCronElement(split[6])
|
||||
}
|
||||
if (split.size == 8) {
|
||||
date.year = getValueFromCronElement(split[0])
|
||||
date.month = getValueFromCronElement(split[1])
|
||||
date.day = getValueFromCronElement(split[2])
|
||||
date.weekday = getValueFromCronElement(split[3])
|
||||
date.hour = getValueFromCronElement(split[4])
|
||||
date.minute = getValueFromCronElement(split[5])
|
||||
date.second = getValueFromCronElement(split[6])
|
||||
date.unit = getValueFromCronElement(split[7])
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
private fun getValueFromCronElement(token: String): Int? {
|
||||
return try {
|
||||
token.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
android/src/main/java/NotificationStorage.kt
Normal file
135
android/src/main/java/NotificationStorage.kt
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import app.tauri.plugin.JSObject
|
||||
import org.json.JSONException
|
||||
import java.text.ParseException
|
||||
|
||||
// Key for private preferences
|
||||
private const val NOTIFICATION_STORE_ID = "NOTIFICATION_STORE"
|
||||
// Key used to save action types
|
||||
private const val ACTION_TYPES_ID = "ACTION_TYPE_STORE"
|
||||
|
||||
class NotificationStorage(private val context: Context) {
|
||||
fun appendNotifications(localNotifications: List<Notification>) {
|
||||
val storage = getStorage(NOTIFICATION_STORE_ID)
|
||||
val editor = storage.edit()
|
||||
for (request in localNotifications) {
|
||||
if (request.isScheduled) {
|
||||
val key: String = request.id.toString()
|
||||
editor.putString(key, request.source.toString())
|
||||
}
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun getSavedNotificationIds(): List<String> {
|
||||
val storage = getStorage(NOTIFICATION_STORE_ID)
|
||||
val all = storage.all
|
||||
return if (all != null) {
|
||||
ArrayList(all.keys)
|
||||
} else ArrayList()
|
||||
}
|
||||
|
||||
fun getSavedNotifications(): List<Notification> {
|
||||
val storage = getStorage(NOTIFICATION_STORE_ID)
|
||||
val all = storage.all
|
||||
if (all != null) {
|
||||
val notifications = ArrayList<Notification>()
|
||||
for (key in all.keys) {
|
||||
val notificationString = all[key] as String?
|
||||
val jsNotification = getNotificationFromJSONString(notificationString)
|
||||
if (jsNotification != null) {
|
||||
try {
|
||||
val notification =
|
||||
Notification.fromJSObject(jsNotification)
|
||||
notifications.add(notification)
|
||||
} catch (_: ParseException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return notifications
|
||||
}
|
||||
return ArrayList()
|
||||
}
|
||||
|
||||
private fun getNotificationFromJSONString(notificationString: String?): JSObject? {
|
||||
if (notificationString == null) {
|
||||
return null
|
||||
}
|
||||
val jsNotification = try {
|
||||
JSObject(notificationString)
|
||||
} catch (ex: JSONException) {
|
||||
return null
|
||||
}
|
||||
return jsNotification
|
||||
}
|
||||
|
||||
fun getSavedNotificationAsJSObject(key: String?): JSObject? {
|
||||
val storage = getStorage(NOTIFICATION_STORE_ID)
|
||||
val notificationString = try {
|
||||
storage.getString(key, null)
|
||||
} catch (ex: ClassCastException) {
|
||||
return null
|
||||
} ?: return null
|
||||
|
||||
val jsNotification = try {
|
||||
JSObject(notificationString)
|
||||
} catch (ex: JSONException) {
|
||||
return null
|
||||
}
|
||||
return jsNotification
|
||||
}
|
||||
|
||||
fun getSavedNotification(key: String?): Notification? {
|
||||
val jsNotification = getSavedNotificationAsJSObject(key) ?: return null
|
||||
val notification = try {
|
||||
Notification.fromJSObject(jsNotification)
|
||||
} catch (ex: ParseException) {
|
||||
return null
|
||||
}
|
||||
return notification
|
||||
}
|
||||
|
||||
fun deleteNotification(id: String?) {
|
||||
val editor = getStorage(NOTIFICATION_STORE_ID).edit()
|
||||
editor.remove(id)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
private fun getStorage(key: String): SharedPreferences {
|
||||
return context.getSharedPreferences(key, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
fun writeActionGroup(typesMap: Map<String, List<NotificationAction>>) {
|
||||
for ((id, notificationActions) in typesMap) {
|
||||
val editor = getStorage(ACTION_TYPES_ID + id).edit()
|
||||
editor.clear()
|
||||
editor.putInt("count", notificationActions.size)
|
||||
for (i in notificationActions.indices) {
|
||||
editor.putString("id$i", notificationActions[i].id)
|
||||
editor.putString("title$i", notificationActions[i].title)
|
||||
editor.putBoolean("input$i", notificationActions[i].input)
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun getActionGroup(forId: String): Array<NotificationAction?> {
|
||||
val storage = getStorage(ACTION_TYPES_ID + forId)
|
||||
val count = storage.getInt("count", 0)
|
||||
val actions: Array<NotificationAction?> = arrayOfNulls(count)
|
||||
for (i in 0 until count) {
|
||||
val id = storage.getString("id$i", "")
|
||||
val title = storage.getString("title$i", "")
|
||||
val input = storage.getBoolean("input$i", false)
|
||||
actions[i] = NotificationAction(id, title, input)
|
||||
}
|
||||
return actions
|
||||
}
|
||||
}
|
||||
573
android/src/main/java/TauriNotificationManager.kt
Normal file
573
android/src/main/java/TauriNotificationManager.kt
Normal file
@@ -0,0 +1,573 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlarmManager
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.media.AudioAttributes
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.UserManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.RemoteInput
|
||||
import app.tauri.Logger
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.PluginManager
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
// Action constants
|
||||
const val NOTIFICATION_INTENT_KEY = "NotificationId"
|
||||
const val NOTIFICATION_OBJ_INTENT_KEY = "LocalNotficationObject"
|
||||
const val ACTION_INTENT_KEY = "NotificationUserAction"
|
||||
const val NOTIFICATION_IS_REMOVABLE_KEY = "NotificationRepeating"
|
||||
const val REMOTE_INPUT_KEY = "NotificationRemoteInput"
|
||||
const val DEFAULT_NOTIFICATION_CHANNEL_ID = "default"
|
||||
const val DEFAULT_PRESS_ACTION = "tap"
|
||||
|
||||
class TauriNotificationManager(
|
||||
private val storage: NotificationStorage,
|
||||
private val activity: Activity?,
|
||||
private val context: Context,
|
||||
private val config: JSObject
|
||||
) {
|
||||
private var defaultSoundID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
private var defaultSmallIconID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
|
||||
fun handleNotificationActionPerformed(
|
||||
data: Intent,
|
||||
notificationStorage: NotificationStorage
|
||||
): JSObject? {
|
||||
Logger.debug(Logger.tags("Notification"), "Notification received: " + data.dataString)
|
||||
val notificationId =
|
||||
data.getIntExtra(NOTIFICATION_INTENT_KEY, Int.MIN_VALUE)
|
||||
if (notificationId == Int.MIN_VALUE) {
|
||||
Logger.debug(Logger.tags("Notification"), "Activity started without notification attached")
|
||||
return null
|
||||
}
|
||||
val isRemovable =
|
||||
data.getBooleanExtra(NOTIFICATION_IS_REMOVABLE_KEY, true)
|
||||
if (isRemovable) {
|
||||
notificationStorage.deleteNotification(notificationId.toString())
|
||||
}
|
||||
val dataJson = JSObject()
|
||||
val results = RemoteInput.getResultsFromIntent(data)
|
||||
val input = results?.getCharSequence(REMOTE_INPUT_KEY)
|
||||
dataJson.put("inputValue", input?.toString())
|
||||
val menuAction = data.getStringExtra(ACTION_INTENT_KEY)
|
||||
dismissVisibleNotification(notificationId)
|
||||
dataJson.put("actionId", menuAction)
|
||||
var request: JSONObject? = null
|
||||
try {
|
||||
val notificationJsonString =
|
||||
data.getStringExtra(NOTIFICATION_OBJ_INTENT_KEY)
|
||||
if (notificationJsonString != null) {
|
||||
request = JSObject(notificationJsonString)
|
||||
}
|
||||
} catch (_: JSONException) {
|
||||
}
|
||||
dataJson.put("notification", request)
|
||||
return dataJson
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notification channel
|
||||
*/
|
||||
fun createNotificationChannel() {
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val name: CharSequence = "Default"
|
||||
val description = "Default"
|
||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val channel = NotificationChannel(DEFAULT_NOTIFICATION_CHANNEL_ID, name, importance)
|
||||
channel.description = description
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ALARM)
|
||||
.build()
|
||||
val soundUri = getDefaultSoundUrl(context)
|
||||
if (soundUri != null) {
|
||||
channel.setSound(soundUri, audioAttributes)
|
||||
}
|
||||
// Register the channel with the system; you can't change the importance
|
||||
// or other notification behaviors after this
|
||||
val notificationManager = context.getSystemService(
|
||||
NotificationManager::class.java
|
||||
)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun trigger(notificationManager: NotificationManagerCompat, notification: Notification): Int {
|
||||
dismissVisibleNotification(notification.id)
|
||||
cancelTimerForNotification(notification.id)
|
||||
buildNotification(notificationManager, notification)
|
||||
|
||||
return notification.id
|
||||
}
|
||||
|
||||
fun schedule(notification: Notification): Int {
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
return trigger(notificationManager, notification)
|
||||
}
|
||||
|
||||
fun schedule(notifications: List<Notification>): List<Int> {
|
||||
val ids = mutableListOf<Int>()
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
for (notification in notifications) {
|
||||
val id = trigger(notificationManager, notification)
|
||||
ids.add(id)
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
// TODO Progressbar support
|
||||
// TODO System categories (DO_NOT_DISTURB etc.)
|
||||
// TODO use NotificationCompat.MessagingStyle for latest API
|
||||
// TODO expandable notification NotificationCompat.MessagingStyle
|
||||
// TODO media style notification support NotificationCompat.MediaStyle
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun buildNotification(
|
||||
notificationManager: NotificationManagerCompat,
|
||||
notification: Notification,
|
||||
) {
|
||||
val channelId = notification.channelId ?: DEFAULT_NOTIFICATION_CHANNEL_ID
|
||||
val mBuilder = NotificationCompat.Builder(
|
||||
context, channelId
|
||||
)
|
||||
.setContentTitle(notification.title)
|
||||
.setContentText(notification.body)
|
||||
.setAutoCancel(notification.isAutoCancel)
|
||||
.setOngoing(notification.isOngoing)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setGroupSummary(notification.isGroupSummary)
|
||||
if (notification.largeBody != null) {
|
||||
// support multiline text
|
||||
mBuilder.setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.bigText(notification.largeBody)
|
||||
.setSummaryText(notification.summary)
|
||||
)
|
||||
} else if (notification.inboxLines != null) {
|
||||
val inboxStyle = NotificationCompat.InboxStyle()
|
||||
for (line in notification.inboxLines ?: listOf()) {
|
||||
inboxStyle.addLine(line)
|
||||
}
|
||||
inboxStyle.setBigContentTitle(notification.title)
|
||||
inboxStyle.setSummaryText(notification.summary)
|
||||
mBuilder.setStyle(inboxStyle)
|
||||
}
|
||||
val sound = notification.getSound(context, getDefaultSound(context))
|
||||
if (sound != null) {
|
||||
val soundUri = Uri.parse(sound)
|
||||
// Grant permission to use sound
|
||||
context.grantUriPermission(
|
||||
"com.android.systemui",
|
||||
soundUri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
mBuilder.setSound(soundUri)
|
||||
mBuilder.setDefaults(android.app.Notification.DEFAULT_VIBRATE or android.app.Notification.DEFAULT_LIGHTS)
|
||||
} else {
|
||||
mBuilder.setDefaults(android.app.Notification.DEFAULT_ALL)
|
||||
}
|
||||
val group = notification.group
|
||||
if (group != null) {
|
||||
mBuilder.setGroup(group)
|
||||
if (notification.isGroupSummary) {
|
||||
mBuilder.setSubText(notification.summary)
|
||||
}
|
||||
}
|
||||
mBuilder.setVisibility(notification.visibility ?: NotificationCompat.VISIBILITY_PRIVATE)
|
||||
mBuilder.setOnlyAlertOnce(true)
|
||||
mBuilder.setSmallIcon(notification.getSmallIcon(context, getDefaultSmallIcon(context)))
|
||||
mBuilder.setLargeIcon(notification.getLargeIcon(context))
|
||||
val iconColor = notification.getIconColor(config.getString("iconColor"))
|
||||
if (iconColor.isNotEmpty()) {
|
||||
try {
|
||||
mBuilder.color = Color.parseColor(iconColor)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
throw Exception("Invalid color provided. Must be a hex string (ex: #ff0000")
|
||||
}
|
||||
}
|
||||
createActionIntents(notification, mBuilder)
|
||||
// notificationId is a unique int for each notification that you must define
|
||||
val buildNotification = mBuilder.build()
|
||||
if (notification.isScheduled) {
|
||||
triggerScheduledNotification(buildNotification, notification)
|
||||
} else {
|
||||
notificationManager.notify(notification.id, buildNotification)
|
||||
try {
|
||||
NotificationPlugin.triggerNotification(notification.source ?: JSObject())
|
||||
} catch (_: JSONException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create intents for open/dismiss actions
|
||||
private fun createActionIntents(
|
||||
notification: Notification,
|
||||
mBuilder: NotificationCompat.Builder
|
||||
) {
|
||||
// Open intent
|
||||
val intent = buildIntent(notification, DEFAULT_PRESS_ACTION)
|
||||
var flags = PendingIntent.FLAG_CANCEL_CURRENT
|
||||
if (SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags = flags or PendingIntent.FLAG_MUTABLE
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(context, notification.id, intent, flags)
|
||||
mBuilder.setContentIntent(pendingIntent)
|
||||
|
||||
// Build action types
|
||||
val actionTypeId = notification.actionTypeId
|
||||
if (actionTypeId != null) {
|
||||
val actionGroup = storage.getActionGroup(actionTypeId)
|
||||
for (notificationAction in actionGroup) {
|
||||
// TODO Add custom icons to actions
|
||||
val actionIntent = buildIntent(notification, notificationAction!!.id)
|
||||
val actionPendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
(notification.id) + notificationAction.id.hashCode(),
|
||||
actionIntent,
|
||||
flags
|
||||
)
|
||||
val actionBuilder: NotificationCompat.Action.Builder = NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_transparent,
|
||||
notificationAction.title,
|
||||
actionPendingIntent
|
||||
)
|
||||
if (notificationAction.input) {
|
||||
val remoteInput = RemoteInput.Builder(REMOTE_INPUT_KEY).setLabel(
|
||||
notificationAction.title
|
||||
).build()
|
||||
actionBuilder.addRemoteInput(remoteInput)
|
||||
}
|
||||
mBuilder.addAction(actionBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss intent
|
||||
val dissmissIntent = Intent(
|
||||
context,
|
||||
NotificationDismissReceiver::class.java
|
||||
)
|
||||
dissmissIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
dissmissIntent.putExtra(NOTIFICATION_INTENT_KEY, notification.id)
|
||||
dissmissIntent.putExtra(ACTION_INTENT_KEY, "dismiss")
|
||||
val schedule = notification.schedule
|
||||
dissmissIntent.putExtra(
|
||||
NOTIFICATION_IS_REMOVABLE_KEY,
|
||||
schedule == null || schedule.isRemovable()
|
||||
)
|
||||
flags = 0
|
||||
if (SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags = PendingIntent.FLAG_MUTABLE
|
||||
}
|
||||
val deleteIntent =
|
||||
PendingIntent.getBroadcast(context, notification.id, dissmissIntent, flags)
|
||||
mBuilder.setDeleteIntent(deleteIntent)
|
||||
}
|
||||
|
||||
private fun buildIntent(notification: Notification, action: String?): Intent {
|
||||
val intent = if (activity != null) {
|
||||
Intent(context, activity.javaClass)
|
||||
} else {
|
||||
val packageName = context.packageName
|
||||
context.packageManager.getLaunchIntentForPackage(packageName)!!
|
||||
}
|
||||
intent.action = Intent.ACTION_MAIN
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
intent.putExtra(NOTIFICATION_INTENT_KEY, notification.id)
|
||||
intent.putExtra(ACTION_INTENT_KEY, action)
|
||||
intent.putExtra(NOTIFICATION_OBJ_INTENT_KEY, notification.source.toString())
|
||||
val schedule = notification.schedule
|
||||
intent.putExtra(NOTIFICATION_IS_REMOVABLE_KEY, schedule == null || schedule.isRemovable())
|
||||
return intent
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a notification trigger, such as triggering each N seconds, or
|
||||
* on a certain date "shape" (such as every first of the month)
|
||||
*/
|
||||
// TODO support different AlarmManager.RTC modes depending on priority
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private fun triggerScheduledNotification(notification: android.app.Notification, request: Notification) {
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val schedule = request.schedule
|
||||
val notificationIntent = Intent(
|
||||
context,
|
||||
TimedNotificationPublisher::class.java
|
||||
)
|
||||
notificationIntent.putExtra(NOTIFICATION_INTENT_KEY, request.id)
|
||||
notificationIntent.putExtra(TimedNotificationPublisher.NOTIFICATION_KEY, notification)
|
||||
var flags = PendingIntent.FLAG_CANCEL_CURRENT
|
||||
if (SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags = flags or PendingIntent.FLAG_MUTABLE
|
||||
}
|
||||
var pendingIntent =
|
||||
PendingIntent.getBroadcast(context, request.id, notificationIntent, flags)
|
||||
|
||||
when (val scheduleKind = schedule?.kind) {
|
||||
is ScheduleKind.At -> {
|
||||
val at = scheduleKind.date
|
||||
if (at.time < Date().time) {
|
||||
Logger.error(Logger.tags("Notification"), "Scheduled time must be *after* current time", null)
|
||||
return
|
||||
}
|
||||
if (scheduleKind.repeating) {
|
||||
val interval: Long = at.time - Date().time
|
||||
alarmManager.setRepeating(AlarmManager.RTC, at.time, interval, pendingIntent)
|
||||
} else {
|
||||
setExactIfPossible(alarmManager, schedule, at.time, pendingIntent)
|
||||
}
|
||||
}
|
||||
is ScheduleKind.Interval -> {
|
||||
val trigger = scheduleKind.interval.nextTrigger(Date())
|
||||
notificationIntent.putExtra(TimedNotificationPublisher.CRON_KEY, scheduleKind.interval.toMatchString())
|
||||
pendingIntent =
|
||||
PendingIntent.getBroadcast(context, request.id, notificationIntent, flags)
|
||||
setExactIfPossible(alarmManager, schedule, trigger, pendingIntent)
|
||||
val sdf = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
|
||||
Logger.debug(
|
||||
Logger.tags("Notification"),
|
||||
"notification " + request.id + " will next fire at " + sdf.format(Date(trigger))
|
||||
)
|
||||
}
|
||||
is ScheduleKind.Every -> {
|
||||
val everyInterval = getIntervalTime(scheduleKind.interval, scheduleKind.count)
|
||||
val startTime: Long = Date().time + everyInterval
|
||||
alarmManager.setRepeating(AlarmManager.RTC, startTime, everyInterval, pendingIntent)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt", "MissingPermission")
|
||||
private fun setExactIfPossible(
|
||||
alarmManager: AlarmManager,
|
||||
schedule: NotificationSchedule,
|
||||
trigger: Long,
|
||||
pendingIntent: PendingIntent
|
||||
) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.M && schedule.whileIdle) {
|
||||
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent)
|
||||
} else {
|
||||
alarmManager[AlarmManager.RTC, trigger] = pendingIntent
|
||||
}
|
||||
} else {
|
||||
if (SDK_INT >= Build.VERSION_CODES.M && schedule.whileIdle) {
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent)
|
||||
} else {
|
||||
alarmManager.setExact(AlarmManager.RTC, trigger, pendingIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel(notifications: List<Int>) {
|
||||
for (id in notifications) {
|
||||
dismissVisibleNotification(id)
|
||||
cancelTimerForNotification(id)
|
||||
storage.deleteNotification(id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelTimerForNotification(notificationId: Int) {
|
||||
val intent = Intent(context, TimedNotificationPublisher::class.java)
|
||||
var flags = 0
|
||||
if (SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags = PendingIntent.FLAG_MUTABLE
|
||||
}
|
||||
val pi = PendingIntent.getBroadcast(context, notificationId, intent, flags)
|
||||
if (pi != null) {
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
alarmManager.cancel(pi)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissVisibleNotification(notificationId: Int) {
|
||||
val notificationManager = NotificationManagerCompat.from(
|
||||
context
|
||||
)
|
||||
notificationManager.cancel(notificationId)
|
||||
}
|
||||
|
||||
fun areNotificationsEnabled(): Boolean {
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
return notificationManager.areNotificationsEnabled()
|
||||
}
|
||||
|
||||
private fun getDefaultSoundUrl(context: Context): Uri? {
|
||||
val soundId = getDefaultSound(context)
|
||||
return if (soundId != AssetUtils.RESOURCE_ID_ZERO_VALUE) {
|
||||
Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + soundId)
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun getDefaultSound(context: Context): Int {
|
||||
if (defaultSoundID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSoundID
|
||||
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
val soundConfigResourceName = AssetUtils.getResourceBaseName(config.getString("sound"))
|
||||
if (soundConfigResourceName != null) {
|
||||
resId = AssetUtils.getResourceID(context, soundConfigResourceName, "raw")
|
||||
}
|
||||
defaultSoundID = resId
|
||||
return resId
|
||||
}
|
||||
|
||||
private fun getDefaultSmallIcon(context: Context): Int {
|
||||
if (defaultSmallIconID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSmallIconID
|
||||
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
val smallIconConfigResourceName = AssetUtils.getResourceBaseName(config.getString("icon"))
|
||||
if (smallIconConfigResourceName != null) {
|
||||
resId = AssetUtils.getResourceID(context, smallIconConfigResourceName, "drawable")
|
||||
}
|
||||
if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) {
|
||||
resId = android.R.drawable.ic_dialog_info
|
||||
}
|
||||
defaultSmallIconID = resId
|
||||
return resId
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationDismissReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val intExtra =
|
||||
intent.getIntExtra(NOTIFICATION_INTENT_KEY, Int.MIN_VALUE)
|
||||
if (intExtra == Int.MIN_VALUE) {
|
||||
Logger.error(Logger.tags("Notification"), "Invalid notification dismiss operation", null)
|
||||
return
|
||||
}
|
||||
val isRemovable =
|
||||
intent.getBooleanExtra(NOTIFICATION_IS_REMOVABLE_KEY, true)
|
||||
if (isRemovable) {
|
||||
val notificationStorage = NotificationStorage(context)
|
||||
notificationStorage.deleteNotification(intExtra.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TimedNotificationPublisher : BroadcastReceiver() {
|
||||
/**
|
||||
* Restore and present notification
|
||||
*/
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(
|
||||
NOTIFICATION_KEY,
|
||||
android.app.Notification::class.java
|
||||
)
|
||||
} else {
|
||||
getParcelableExtraLegacy(intent, NOTIFICATION_KEY)
|
||||
}
|
||||
notification?.`when` = System.currentTimeMillis()
|
||||
val id = intent.getIntExtra(NOTIFICATION_INTENT_KEY, Int.MIN_VALUE)
|
||||
if (id == Int.MIN_VALUE) {
|
||||
Logger.error(Logger.tags("Notification"), "No valid id supplied", null)
|
||||
}
|
||||
val storage = NotificationStorage(context)
|
||||
val notificationJson = storage.getSavedNotificationAsJSObject(id.toString())
|
||||
if (notificationJson != null) {
|
||||
NotificationPlugin.triggerNotification(notificationJson)
|
||||
}
|
||||
notificationManager.notify(id, notification)
|
||||
if (!rescheduleNotificationIfNeeded(context, intent, id)) {
|
||||
storage.deleteNotification(id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getParcelableExtraLegacy(intent: Intent, string: String): android.app.Notification? {
|
||||
return intent.getParcelableExtra(string)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission", "SimpleDateFormat")
|
||||
private fun rescheduleNotificationIfNeeded(context: Context, intent: Intent, id: Int): Boolean {
|
||||
val dateString = intent.getStringExtra(CRON_KEY)
|
||||
if (dateString != null) {
|
||||
val date = DateMatch.fromMatchString(dateString)
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val trigger = date.nextTrigger(Date())
|
||||
val clone = intent.clone() as Intent
|
||||
var flags = PendingIntent.FLAG_CANCEL_CURRENT
|
||||
if (SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags = flags or PendingIntent.FLAG_MUTABLE
|
||||
}
|
||||
val pendingIntent = PendingIntent.getBroadcast(context, id, clone, flags)
|
||||
if (SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager[AlarmManager.RTC, trigger] = pendingIntent
|
||||
} else {
|
||||
alarmManager.setExact(AlarmManager.RTC, trigger, pendingIntent)
|
||||
}
|
||||
val sdf = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
|
||||
Logger.debug(
|
||||
Logger.tags("Notification"),
|
||||
"notification " + id + " will next fire at " + sdf.format(Date(trigger))
|
||||
)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
var NOTIFICATION_KEY = "NotificationPublisher.notification"
|
||||
var CRON_KEY = "NotificationPublisher.cron"
|
||||
}
|
||||
}
|
||||
|
||||
class LocalNotificationRestoreReceiver : BroadcastReceiver() {
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val um = context.getSystemService(
|
||||
UserManager::class.java
|
||||
)
|
||||
if (um == null || !um.isUserUnlocked) return
|
||||
}
|
||||
val storage = NotificationStorage(context)
|
||||
val ids = storage.getSavedNotificationIds()
|
||||
val notifications = mutableListOf<Notification>()
|
||||
val updatedNotifications = mutableListOf<Notification>()
|
||||
for (id in ids) {
|
||||
val notification = storage.getSavedNotification(id) ?: continue
|
||||
val schedule = notification.schedule
|
||||
if (schedule != null && schedule.kind is ScheduleKind.At) {
|
||||
val at: Date = schedule.kind.date
|
||||
if (at.before(Date())) {
|
||||
// modify the scheduled date in order to show notifications that would have been delivered while device was off.
|
||||
val newDateTime = Date().time + 15 * 1000
|
||||
schedule.kind.date = Date(newDateTime)
|
||||
updatedNotifications.add(notification)
|
||||
}
|
||||
}
|
||||
notifications.add(notification)
|
||||
}
|
||||
if (updatedNotifications.size > 0) {
|
||||
storage.appendNotifications(updatedNotifications)
|
||||
}
|
||||
|
||||
val notificationManager = TauriNotificationManager(storage, null, context, PluginManager.loadConfig(context, "notification"))
|
||||
notificationManager.schedule(notifications)
|
||||
}
|
||||
}
|
||||
12
android/src/main/res/drawable/ic_transparent.xml
Normal file
12
android/src/main/res/drawable/ic_transparent.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
|
||||
<path
|
||||
android:width="1dp"
|
||||
android:color="@android:color/transparent" />
|
||||
|
||||
</vector>
|
||||
21
android/src/test/java/ExampleUnitTest.kt
Normal file
21
android/src/test/java/ExampleUnitTest.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
16
build.rs
Normal file
16
build.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::process::exit;
|
||||
|
||||
fn main() {
|
||||
if let Err(error) = tauri_build::mobile::PluginBuilder::new()
|
||||
.android_path("android")
|
||||
.ios_path("ios")
|
||||
.run()
|
||||
{
|
||||
println!("{error:#}");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
445
dist-js/index.d.ts
vendored
Normal file
445
dist-js/index.d.ts
vendored
Normal file
@@ -0,0 +1,445 @@
|
||||
/**
|
||||
* Send toast notifications (brief auto-expiring OS window element) to your user.
|
||||
* Can also be used with the Notification Web API.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
import { PluginListener } from "@tauri-apps/api/tauri";
|
||||
/**
|
||||
* Options to send a notification.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
interface Options {
|
||||
/**
|
||||
* The notification identifier to reference this object later. Must be a 32-bit integer.
|
||||
*/
|
||||
id?: number;
|
||||
/**
|
||||
* Identifier of the {@link Channel} that deliveres this notification.
|
||||
*
|
||||
* If the channel does not exist, the notification won't fire.
|
||||
* Make sure the channel exists with {@link listChannels} and {@link createChannel}.
|
||||
*/
|
||||
channelId?: string;
|
||||
/**
|
||||
* Notification title.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Optional notification body.
|
||||
* */
|
||||
body?: string;
|
||||
/**
|
||||
* Schedule this notification to fire on a later time or a fixed interval.
|
||||
*/
|
||||
schedule?: Schedule;
|
||||
/**
|
||||
* Multiline text.
|
||||
* Changes the notification style to big text.
|
||||
* Cannot be used with `inboxLines`.
|
||||
*/
|
||||
largeBody?: string;
|
||||
/**
|
||||
* Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`.
|
||||
*/
|
||||
summary?: string;
|
||||
/**
|
||||
* Defines an action type for this notification.
|
||||
*/
|
||||
actionTypeId?: string;
|
||||
/**
|
||||
* Identifier used to group multiple notifications.
|
||||
*
|
||||
* https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier
|
||||
*/
|
||||
group?: string;
|
||||
/**
|
||||
* Instructs the system that this notification is the summary of a group on Android.
|
||||
*/
|
||||
groupSummary?: boolean;
|
||||
/**
|
||||
* The sound resource name. Only available on mobile.
|
||||
*/
|
||||
sound?: string;
|
||||
/**
|
||||
* List of lines to add to the notification.
|
||||
* Changes the notification style to inbox.
|
||||
* Cannot be used with `largeBody`.
|
||||
*
|
||||
* Only supports up to 5 lines.
|
||||
*/
|
||||
inboxLines?: string[];
|
||||
/**
|
||||
* Notification icon.
|
||||
*
|
||||
* On Android the icon must be placed in the app's `res/drawable` folder.
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* Notification large icon (Android).
|
||||
*
|
||||
* The icon must be placed in the app's `res/drawable` folder.
|
||||
*/
|
||||
largeIcon?: string;
|
||||
/**
|
||||
* Icon color on Android.
|
||||
*/
|
||||
iconColor?: string;
|
||||
/**
|
||||
* Notification attachments.
|
||||
*/
|
||||
attachments?: Attachment[];
|
||||
/**
|
||||
* Extra payload to store in the notification.
|
||||
*/
|
||||
extra?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* If true, the notification cannot be dismissed by the user on Android.
|
||||
*
|
||||
* An application service must manage the dismissal of the notification.
|
||||
* It is typically used to indicate a background task that is pending (e.g. a file download)
|
||||
* or the user is engaged with (e.g. playing music).
|
||||
*/
|
||||
ongoing?: boolean;
|
||||
/**
|
||||
* Automatically cancel the notification when the user clicks on it.
|
||||
*/
|
||||
autoCancel?: boolean;
|
||||
/**
|
||||
* Changes the notification presentation to be silent on iOS (no badge, no sound, not listed).
|
||||
*/
|
||||
silent?: boolean;
|
||||
/**
|
||||
* Notification visibility.
|
||||
*/
|
||||
visibility?: Visibility;
|
||||
/**
|
||||
* Sets the number of items this notification represents on Android.
|
||||
*/
|
||||
number?: number;
|
||||
}
|
||||
type ScheduleInterval = {
|
||||
year?: number;
|
||||
month?: number;
|
||||
day?: number;
|
||||
/**
|
||||
* 1 - Sunday
|
||||
* 2 - Monday
|
||||
* 3 - Tuesday
|
||||
* 4 - Wednesday
|
||||
* 5 - Thursday
|
||||
* 6 - Friday
|
||||
* 7 - Saturday
|
||||
*/
|
||||
weekday?: number;
|
||||
hour?: number;
|
||||
minute?: number;
|
||||
second?: number;
|
||||
};
|
||||
declare enum ScheduleEvery {
|
||||
Year = "Year",
|
||||
Month = "Month",
|
||||
TwoWeeks = "TwoWeeks",
|
||||
Week = "Week",
|
||||
Day = "Day",
|
||||
Hour = "Hour",
|
||||
Minute = "Minute",
|
||||
/**
|
||||
* Not supported on iOS.
|
||||
*/
|
||||
Second = "Second"
|
||||
}
|
||||
declare class Schedule {
|
||||
kind: string;
|
||||
data: unknown;
|
||||
private constructor();
|
||||
static at(date: Date, repeating?: boolean): Schedule;
|
||||
static interval(interval: ScheduleInterval): Schedule;
|
||||
static every(kind: ScheduleEvery): Schedule;
|
||||
}
|
||||
/**
|
||||
* Attachment of a notification.
|
||||
*/
|
||||
interface Attachment {
|
||||
/** Attachment identifier. */
|
||||
id: string;
|
||||
/** Attachment URL. Accepts the `asset` and `file` protocols. */
|
||||
url: string;
|
||||
}
|
||||
interface Action {
|
||||
id: string;
|
||||
title: string;
|
||||
requiresAuthentication?: boolean;
|
||||
foreground?: boolean;
|
||||
destructive?: boolean;
|
||||
input?: boolean;
|
||||
inputButtonTitle?: string;
|
||||
inputPlaceholder?: string;
|
||||
}
|
||||
interface ActionType {
|
||||
/**
|
||||
* The identifier of this action type
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The list of associated actions
|
||||
*/
|
||||
actions: Action[];
|
||||
hiddenPreviewsBodyPlaceholder?: string;
|
||||
customDismissAction?: boolean;
|
||||
allowInCarPlay?: boolean;
|
||||
hiddenPreviewsShowTitle?: boolean;
|
||||
hiddenPreviewsShowSubtitle?: boolean;
|
||||
}
|
||||
interface PendingNotification {
|
||||
id: number;
|
||||
title?: string;
|
||||
body?: string;
|
||||
schedule: Schedule;
|
||||
}
|
||||
interface ActiveNotification {
|
||||
id: number;
|
||||
tag?: string;
|
||||
title?: string;
|
||||
body?: string;
|
||||
group?: string;
|
||||
groupSummary: boolean;
|
||||
data: Record<string, string>;
|
||||
extra: Record<string, unknown>;
|
||||
attachments: Attachment[];
|
||||
actionTypeId?: string;
|
||||
schedule?: Schedule;
|
||||
sound?: string;
|
||||
}
|
||||
declare enum Importance {
|
||||
None = 0,
|
||||
Min = 1,
|
||||
Low = 2,
|
||||
Default = 3,
|
||||
High = 4
|
||||
}
|
||||
declare enum Visibility {
|
||||
Secret = -1,
|
||||
Private = 0,
|
||||
Public = 1
|
||||
}
|
||||
interface Channel {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
sound?: string;
|
||||
lights?: boolean;
|
||||
lightColor?: string;
|
||||
vibration?: boolean;
|
||||
importance?: Importance;
|
||||
visibility?: Visibility;
|
||||
}
|
||||
/** Possible permission values. */
|
||||
type Permission = "granted" | "denied" | "default";
|
||||
/**
|
||||
* Checks if the permission to send notifications is granted.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted } from '@tauri-apps/api/notification';
|
||||
* const permissionGranted = await isPermissionGranted();
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function isPermissionGranted(): Promise<boolean>;
|
||||
/**
|
||||
* Requests the permission to send notifications.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted, requestPermission } from '@tauri-apps/api/notification';
|
||||
* let permissionGranted = await isPermissionGranted();
|
||||
* if (!permissionGranted) {
|
||||
* const permission = await requestPermission();
|
||||
* permissionGranted = permission === 'granted';
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to whether the user granted the permission or not.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function requestPermission(): Promise<Permission>;
|
||||
/**
|
||||
* Sends a notification to the user.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/api/notification';
|
||||
* let permissionGranted = await isPermissionGranted();
|
||||
* if (!permissionGranted) {
|
||||
* const permission = await requestPermission();
|
||||
* permissionGranted = permission === 'granted';
|
||||
* }
|
||||
* if (permissionGranted) {
|
||||
* sendNotification('Tauri is awesome!');
|
||||
* sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function sendNotification(options: Options | string): void;
|
||||
/**
|
||||
* Register actions that are performed when the user clicks on the notification.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { registerActionTypes } from '@tauri-apps/api/notification';
|
||||
* await registerActionTypes([{
|
||||
* id: 'tauri',
|
||||
* actions: [{
|
||||
* id: 'my-action',
|
||||
* title: 'Settings'
|
||||
* }]
|
||||
* }])
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function registerActionTypes(types: ActionType[]): Promise<void>;
|
||||
/**
|
||||
* Retrieves the list of pending notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { pending } from '@tauri-apps/api/notification';
|
||||
* const pendingNotifications = await pending();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of pending notifications.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function pending(): Promise<PendingNotification[]>;
|
||||
/**
|
||||
* Cancels the pending notifications with the given list of identifiers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancel } from '@tauri-apps/api/notification';
|
||||
* await cancel([-34234, 23432, 4311]);
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function cancel(notifications: number[]): Promise<void>;
|
||||
/**
|
||||
* Cancels all pending notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancelAll } from '@tauri-apps/api/notification';
|
||||
* await cancelAll();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function cancelAll(): Promise<void>;
|
||||
/**
|
||||
* Retrieves the list of active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { active } from '@tauri-apps/api/notification';
|
||||
* const activeNotifications = await active();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of active notifications.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function active(): Promise<ActiveNotification[]>;
|
||||
/**
|
||||
* Removes the active notifications with the given list of identifiers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancel } from '@tauri-apps/api/notification';
|
||||
* await cancel([-34234, 23432, 4311])
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function removeActive(notifications: number[]): Promise<void>;
|
||||
/**
|
||||
* Removes all active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { removeAllActive } from '@tauri-apps/api/notification';
|
||||
* await removeAllActive()
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function removeAllActive(): Promise<void>;
|
||||
/**
|
||||
* Removes all active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createChannel, Importance, Visibility } from '@tauri-apps/api/notification';
|
||||
* await createChannel({
|
||||
* id: 'new-messages',
|
||||
* name: 'New Messages',
|
||||
* lights: true,
|
||||
* vibration: true,
|
||||
* importance: Importance.Default,
|
||||
* visibility: Visibility.Private
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function createChannel(channel: Channel): Promise<void>;
|
||||
/**
|
||||
* Removes the channel with the given identifier.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { removeChannel } from '@tauri-apps/api/notification';
|
||||
* await removeChannel();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function removeChannel(id: string): Promise<void>;
|
||||
/**
|
||||
* Retrieves the list of notification channels.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { channels } from '@tauri-apps/api/notification';
|
||||
* const notificationChannels = await channels();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of notification channels.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
declare function channels(): Promise<Channel[]>;
|
||||
declare function onNotificationReceived(cb: (notification: Options) => void): Promise<PluginListener>;
|
||||
declare function onAction(cb: (notification: Options) => void): Promise<PluginListener>;
|
||||
export type { Attachment, Options, Permission, Action, ActionType, PendingNotification, ActiveNotification, Channel, };
|
||||
export { Importance, Visibility, sendNotification, requestPermission, isPermissionGranted, registerActionTypes, pending, cancel, cancelAll, active, removeActive, removeAllActive, createChannel, removeChannel, channels, onNotificationReceived, onAction, };
|
||||
278
dist-js/index.min.js
vendored
Normal file
278
dist-js/index.min.js
vendored
Normal file
@@ -0,0 +1,278 @@
|
||||
var f$1=Object.defineProperty;var g=(a,b)=>{for(var c in b)f$1(a,c,{get:b[c],enumerable:!0});};var e=(a,b,c)=>{if(!b.has(a))throw TypeError("Cannot "+c)};var h$1=(a,b,c)=>(e(a,b,"read from private field"),c?c.call(a):b.get(a)),i=(a,b,c)=>{if(b.has(a))throw TypeError("Cannot add the same private member more than once");b instanceof WeakSet?b.add(a):b.set(a,c);},j=(a,b,c,d)=>(e(a,b,"write to private field"),d?d.call(a,c):b.set(a,c),c);
|
||||
|
||||
var f={};g(f,{Channel:()=>a,PluginListener:()=>c,addPluginListener:()=>v,convertFileSrc:()=>m,invoke:()=>u,transformCallback:()=>o});function h(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function o(t,e=!1){let n=h(),r=`_${n}`;return Object.defineProperty(window,r,{value:i=>(e&&Reflect.deleteProperty(window,r),t?.(i)),writable:!1,configurable:!0}),n}var s,a=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0;i(this,s,()=>{});this.id=o(e=>{h$1(this,s).call(this,e);});}set onmessage(e){j(this,s,e);}get onmessage(){return h$1(this,s)}toJSON(){return `__CHANNEL__:${this.id}`}};s=new WeakMap;var c=class{constructor(e,n,r){this.plugin=e,this.event=n,this.channelId=r;}async unregister(){return u(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function v(t,e,n){let r=new a;return r.onmessage=n,u(`plugin:${t}|register_listener`,{event:e,handler:r}).then(()=>new c(t,e,r.id))}async function u(t,e={}){return new Promise((n,r)=>{let i=o(d=>{n(d),Reflect.deleteProperty(window,`_${g}`);},!0),g=o(d=>{r(d),Reflect.deleteProperty(window,`_${i}`);},!0);window.__TAURI_IPC__({cmd:t,callback:i,error:g,...e});})}function m(t,e="asset"){let n=encodeURIComponent(t);return navigator.userAgent.includes("Windows")?`https://${e}.localhost/${n}`:`${e}://localhost/${n}`}
|
||||
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
var ScheduleEvery;
|
||||
(function (ScheduleEvery) {
|
||||
ScheduleEvery["Year"] = "Year";
|
||||
ScheduleEvery["Month"] = "Month";
|
||||
ScheduleEvery["TwoWeeks"] = "TwoWeeks";
|
||||
ScheduleEvery["Week"] = "Week";
|
||||
ScheduleEvery["Day"] = "Day";
|
||||
ScheduleEvery["Hour"] = "Hour";
|
||||
ScheduleEvery["Minute"] = "Minute";
|
||||
/**
|
||||
* Not supported on iOS.
|
||||
*/
|
||||
ScheduleEvery["Second"] = "Second";
|
||||
})(ScheduleEvery || (ScheduleEvery = {}));
|
||||
var Importance;
|
||||
(function (Importance) {
|
||||
Importance[Importance["None"] = 0] = "None";
|
||||
Importance[Importance["Min"] = 1] = "Min";
|
||||
Importance[Importance["Low"] = 2] = "Low";
|
||||
Importance[Importance["Default"] = 3] = "Default";
|
||||
Importance[Importance["High"] = 4] = "High";
|
||||
})(Importance || (Importance = {}));
|
||||
var Visibility;
|
||||
(function (Visibility) {
|
||||
Visibility[Visibility["Secret"] = -1] = "Secret";
|
||||
Visibility[Visibility["Private"] = 0] = "Private";
|
||||
Visibility[Visibility["Public"] = 1] = "Public";
|
||||
})(Visibility || (Visibility = {}));
|
||||
/**
|
||||
* Checks if the permission to send notifications is granted.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted } from '@tauri-apps/api/notification';
|
||||
* const permissionGranted = await isPermissionGranted();
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function isPermissionGranted() {
|
||||
if (window.Notification.permission !== "default") {
|
||||
return Promise.resolve(window.Notification.permission === "granted");
|
||||
}
|
||||
return u("plugin:notification|is_permission_granted");
|
||||
}
|
||||
/**
|
||||
* Requests the permission to send notifications.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted, requestPermission } from '@tauri-apps/api/notification';
|
||||
* let permissionGranted = await isPermissionGranted();
|
||||
* if (!permissionGranted) {
|
||||
* const permission = await requestPermission();
|
||||
* permissionGranted = permission === 'granted';
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to whether the user granted the permission or not.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function requestPermission() {
|
||||
return window.Notification.requestPermission();
|
||||
}
|
||||
/**
|
||||
* Sends a notification to the user.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/api/notification';
|
||||
* let permissionGranted = await isPermissionGranted();
|
||||
* if (!permissionGranted) {
|
||||
* const permission = await requestPermission();
|
||||
* permissionGranted = permission === 'granted';
|
||||
* }
|
||||
* if (permissionGranted) {
|
||||
* sendNotification('Tauri is awesome!');
|
||||
* sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
function sendNotification(options) {
|
||||
if (typeof options === "string") {
|
||||
// eslint-disable-next-line no-new
|
||||
new window.Notification(options);
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line no-new
|
||||
new window.Notification(options.title, options);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Register actions that are performed when the user clicks on the notification.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { registerActionTypes } from '@tauri-apps/api/notification';
|
||||
* await registerActionTypes([{
|
||||
* id: 'tauri',
|
||||
* actions: [{
|
||||
* id: 'my-action',
|
||||
* title: 'Settings'
|
||||
* }]
|
||||
* }])
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function registerActionTypes(types) {
|
||||
return u("plugin:notification|register_action_types", { types });
|
||||
}
|
||||
/**
|
||||
* Retrieves the list of pending notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { pending } from '@tauri-apps/api/notification';
|
||||
* const pendingNotifications = await pending();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of pending notifications.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function pending() {
|
||||
return u("plugin:notification|get_pending");
|
||||
}
|
||||
/**
|
||||
* Cancels the pending notifications with the given list of identifiers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancel } from '@tauri-apps/api/notification';
|
||||
* await cancel([-34234, 23432, 4311]);
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function cancel(notifications) {
|
||||
return u("plugin:notification|cancel", { notifications });
|
||||
}
|
||||
/**
|
||||
* Cancels all pending notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancelAll } from '@tauri-apps/api/notification';
|
||||
* await cancelAll();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function cancelAll() {
|
||||
return u("plugin:notification|cancel");
|
||||
}
|
||||
/**
|
||||
* Retrieves the list of active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { active } from '@tauri-apps/api/notification';
|
||||
* const activeNotifications = await active();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of active notifications.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function active() {
|
||||
return u("plugin:notification|get_active");
|
||||
}
|
||||
/**
|
||||
* Removes the active notifications with the given list of identifiers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancel } from '@tauri-apps/api/notification';
|
||||
* await cancel([-34234, 23432, 4311])
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function removeActive(notifications) {
|
||||
return u("plugin:notification|remove_active", { notifications });
|
||||
}
|
||||
/**
|
||||
* Removes all active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { removeAllActive } from '@tauri-apps/api/notification';
|
||||
* await removeAllActive()
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function removeAllActive() {
|
||||
return u("plugin:notification|remove_active");
|
||||
}
|
||||
/**
|
||||
* Removes all active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createChannel, Importance, Visibility } from '@tauri-apps/api/notification';
|
||||
* await createChannel({
|
||||
* id: 'new-messages',
|
||||
* name: 'New Messages',
|
||||
* lights: true,
|
||||
* vibration: true,
|
||||
* importance: Importance.Default,
|
||||
* visibility: Visibility.Private
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function createChannel(channel) {
|
||||
return u("plugin:notification|create_channel", { ...channel });
|
||||
}
|
||||
/**
|
||||
* Removes the channel with the given identifier.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { removeChannel } from '@tauri-apps/api/notification';
|
||||
* await removeChannel();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function removeChannel(id) {
|
||||
return u("plugin:notification|delete_channel", { id });
|
||||
}
|
||||
/**
|
||||
* Retrieves the list of notification channels.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { channels } from '@tauri-apps/api/notification';
|
||||
* const notificationChannels = await channels();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of notification channels.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function channels() {
|
||||
return u("plugin:notification|getActive");
|
||||
}
|
||||
async function onNotificationReceived(cb) {
|
||||
return v("notification", "notification", cb);
|
||||
}
|
||||
async function onAction(cb) {
|
||||
return v("notification", "actionPerformed", cb);
|
||||
}
|
||||
|
||||
export { Importance, Visibility, active, cancel, cancelAll, channels, createChannel, isPermissionGranted, onAction, onNotificationReceived, pending, registerActionTypes, removeActive, removeAllActive, removeChannel, requestPermission, sendNotification };
|
||||
//# sourceMappingURL=index.min.js.map
|
||||
1
dist-js/index.min.js.map
Normal file
1
dist-js/index.min.js.map
Normal file
File diff suppressed because one or more lines are too long
276
dist-js/index.mjs
Normal file
276
dist-js/index.mjs
Normal file
@@ -0,0 +1,276 @@
|
||||
import { invoke, addPluginListener } from '@tauri-apps/api/tauri';
|
||||
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
var ScheduleEvery;
|
||||
(function (ScheduleEvery) {
|
||||
ScheduleEvery["Year"] = "Year";
|
||||
ScheduleEvery["Month"] = "Month";
|
||||
ScheduleEvery["TwoWeeks"] = "TwoWeeks";
|
||||
ScheduleEvery["Week"] = "Week";
|
||||
ScheduleEvery["Day"] = "Day";
|
||||
ScheduleEvery["Hour"] = "Hour";
|
||||
ScheduleEvery["Minute"] = "Minute";
|
||||
/**
|
||||
* Not supported on iOS.
|
||||
*/
|
||||
ScheduleEvery["Second"] = "Second";
|
||||
})(ScheduleEvery || (ScheduleEvery = {}));
|
||||
var Importance;
|
||||
(function (Importance) {
|
||||
Importance[Importance["None"] = 0] = "None";
|
||||
Importance[Importance["Min"] = 1] = "Min";
|
||||
Importance[Importance["Low"] = 2] = "Low";
|
||||
Importance[Importance["Default"] = 3] = "Default";
|
||||
Importance[Importance["High"] = 4] = "High";
|
||||
})(Importance || (Importance = {}));
|
||||
var Visibility;
|
||||
(function (Visibility) {
|
||||
Visibility[Visibility["Secret"] = -1] = "Secret";
|
||||
Visibility[Visibility["Private"] = 0] = "Private";
|
||||
Visibility[Visibility["Public"] = 1] = "Public";
|
||||
})(Visibility || (Visibility = {}));
|
||||
/**
|
||||
* Checks if the permission to send notifications is granted.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted } from '@tauri-apps/api/notification';
|
||||
* const permissionGranted = await isPermissionGranted();
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function isPermissionGranted() {
|
||||
if (window.Notification.permission !== "default") {
|
||||
return Promise.resolve(window.Notification.permission === "granted");
|
||||
}
|
||||
return invoke("plugin:notification|is_permission_granted");
|
||||
}
|
||||
/**
|
||||
* Requests the permission to send notifications.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted, requestPermission } from '@tauri-apps/api/notification';
|
||||
* let permissionGranted = await isPermissionGranted();
|
||||
* if (!permissionGranted) {
|
||||
* const permission = await requestPermission();
|
||||
* permissionGranted = permission === 'granted';
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to whether the user granted the permission or not.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function requestPermission() {
|
||||
return window.Notification.requestPermission();
|
||||
}
|
||||
/**
|
||||
* Sends a notification to the user.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/api/notification';
|
||||
* let permissionGranted = await isPermissionGranted();
|
||||
* if (!permissionGranted) {
|
||||
* const permission = await requestPermission();
|
||||
* permissionGranted = permission === 'granted';
|
||||
* }
|
||||
* if (permissionGranted) {
|
||||
* sendNotification('Tauri is awesome!');
|
||||
* sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
function sendNotification(options) {
|
||||
if (typeof options === "string") {
|
||||
// eslint-disable-next-line no-new
|
||||
new window.Notification(options);
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line no-new
|
||||
new window.Notification(options.title, options);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Register actions that are performed when the user clicks on the notification.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { registerActionTypes } from '@tauri-apps/api/notification';
|
||||
* await registerActionTypes([{
|
||||
* id: 'tauri',
|
||||
* actions: [{
|
||||
* id: 'my-action',
|
||||
* title: 'Settings'
|
||||
* }]
|
||||
* }])
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function registerActionTypes(types) {
|
||||
return invoke("plugin:notification|register_action_types", { types });
|
||||
}
|
||||
/**
|
||||
* Retrieves the list of pending notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { pending } from '@tauri-apps/api/notification';
|
||||
* const pendingNotifications = await pending();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of pending notifications.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function pending() {
|
||||
return invoke("plugin:notification|get_pending");
|
||||
}
|
||||
/**
|
||||
* Cancels the pending notifications with the given list of identifiers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancel } from '@tauri-apps/api/notification';
|
||||
* await cancel([-34234, 23432, 4311]);
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function cancel(notifications) {
|
||||
return invoke("plugin:notification|cancel", { notifications });
|
||||
}
|
||||
/**
|
||||
* Cancels all pending notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancelAll } from '@tauri-apps/api/notification';
|
||||
* await cancelAll();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function cancelAll() {
|
||||
return invoke("plugin:notification|cancel");
|
||||
}
|
||||
/**
|
||||
* Retrieves the list of active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { active } from '@tauri-apps/api/notification';
|
||||
* const activeNotifications = await active();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of active notifications.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function active() {
|
||||
return invoke("plugin:notification|get_active");
|
||||
}
|
||||
/**
|
||||
* Removes the active notifications with the given list of identifiers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancel } from '@tauri-apps/api/notification';
|
||||
* await cancel([-34234, 23432, 4311])
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function removeActive(notifications) {
|
||||
return invoke("plugin:notification|remove_active", { notifications });
|
||||
}
|
||||
/**
|
||||
* Removes all active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { removeAllActive } from '@tauri-apps/api/notification';
|
||||
* await removeAllActive()
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function removeAllActive() {
|
||||
return invoke("plugin:notification|remove_active");
|
||||
}
|
||||
/**
|
||||
* Removes all active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createChannel, Importance, Visibility } from '@tauri-apps/api/notification';
|
||||
* await createChannel({
|
||||
* id: 'new-messages',
|
||||
* name: 'New Messages',
|
||||
* lights: true,
|
||||
* vibration: true,
|
||||
* importance: Importance.Default,
|
||||
* visibility: Visibility.Private
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function createChannel(channel) {
|
||||
return invoke("plugin:notification|create_channel", { ...channel });
|
||||
}
|
||||
/**
|
||||
* Removes the channel with the given identifier.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { removeChannel } from '@tauri-apps/api/notification';
|
||||
* await removeChannel();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function removeChannel(id) {
|
||||
return invoke("plugin:notification|delete_channel", { id });
|
||||
}
|
||||
/**
|
||||
* Retrieves the list of notification channels.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { channels } from '@tauri-apps/api/notification';
|
||||
* const notificationChannels = await channels();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of notification channels.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function channels() {
|
||||
return invoke("plugin:notification|getActive");
|
||||
}
|
||||
async function onNotificationReceived(cb) {
|
||||
return addPluginListener("notification", "notification", cb);
|
||||
}
|
||||
async function onAction(cb) {
|
||||
return addPluginListener("notification", "actionPerformed", cb);
|
||||
}
|
||||
|
||||
export { Importance, Visibility, active, cancel, cancelAll, channels, createChannel, isPermissionGranted, onAction, onNotificationReceived, pending, registerActionTypes, removeActive, removeAllActive, removeChannel, requestPermission, sendNotification };
|
||||
//# sourceMappingURL=index.mjs.map
|
||||
1
dist-js/index.mjs.map
Normal file
1
dist-js/index.mjs.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.mjs","sources":["../guest-js/index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAAA;AAuJA,IAAK,aAYJ,CAAA;AAZD,CAAA,UAAK,aAAa,EAAA;AAChB,IAAA,aAAA,CAAA,MAAA,CAAA,GAAA,MAAa,CAAA;AACb,IAAA,aAAA,CAAA,OAAA,CAAA,GAAA,OAAe,CAAA;AACf,IAAA,aAAA,CAAA,UAAA,CAAA,GAAA,UAAqB,CAAA;AACrB,IAAA,aAAA,CAAA,MAAA,CAAA,GAAA,MAAa,CAAA;AACb,IAAA,aAAA,CAAA,KAAA,CAAA,GAAA,KAAW,CAAA;AACX,IAAA,aAAA,CAAA,MAAA,CAAA,GAAA,MAAa,CAAA;AACb,IAAA,aAAA,CAAA,QAAA,CAAA,GAAA,QAAiB,CAAA;AACjB;;AAEG;AACH,IAAA,aAAA,CAAA,QAAA,CAAA,GAAA,QAAiB,CAAA;AACnB,CAAC,EAZI,aAAa,KAAb,aAAa,GAYjB,EAAA,CAAA,CAAA,CAAA;AAsGD,IAAK,WAMJ;AAND,CAAA,UAAK,UAAU,EAAA;AACb,IAAA,UAAA,CAAA,UAAA,CAAA,MAAA,CAAA,GAAA,CAAA,CAAA,GAAA,MAAQ,CAAA;AACR,IAAA,UAAA,CAAA,UAAA,CAAA,KAAA,CAAA,GAAA,CAAA,CAAA,GAAA,KAAG,CAAA;AACH,IAAA,UAAA,CAAA,UAAA,CAAA,KAAA,CAAA,GAAA,CAAA,CAAA,GAAA,KAAG,CAAA;AACH,IAAA,UAAA,CAAA,UAAA,CAAA,SAAA,CAAA,GAAA,CAAA,CAAA,GAAA,SAAO,CAAA;AACP,IAAA,UAAA,CAAA,UAAA,CAAA,MAAA,CAAA,GAAA,CAAA,CAAA,GAAA,MAAI,CAAA;AACN,CAAC,EANI,UAAU,KAAV,UAAU,GAMd,EAAA,CAAA,CAAA,CAAA;AAED,IAAK,WAIJ;AAJD,CAAA,UAAK,UAAU,EAAA;AACb,IAAA,UAAA,CAAA,UAAA,CAAA,QAAA,CAAA,GAAA,CAAA,CAAA,CAAA,GAAA,QAAW,CAAA;AACX,IAAA,UAAA,CAAA,UAAA,CAAA,SAAA,CAAA,GAAA,CAAA,CAAA,GAAA,SAAO,CAAA;AACP,IAAA,UAAA,CAAA,UAAA,CAAA,QAAA,CAAA,GAAA,CAAA,CAAA,GAAA,QAAM,CAAA;AACR,CAAC,EAJI,UAAU,KAAV,UAAU,GAId,EAAA,CAAA,CAAA,CAAA;AAiBD;;;;;;;;;AASG;AACH,eAAe,mBAAmB,GAAA;AAChC,IAAA,IAAI,MAAM,CAAC,YAAY,CAAC,UAAU,KAAK,SAAS,EAAE;AAChD,QAAA,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC;AACtE,KAAA;AACD,IAAA,OAAO,MAAM,CAAC,2CAA2C,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;;;;;AAeG;AACH,eAAe,iBAAiB,GAAA;AAC9B,IAAA,OAAO,MAAM,CAAC,YAAY,CAAC,iBAAiB,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;;;;;;;;;;;;AAiBG;AACH,SAAS,gBAAgB,CAAC,OAAyB,EAAA;AACjD,IAAA,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE;;AAE/B,QAAA,IAAI,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;AAClC,KAAA;AAAM,SAAA;;QAEL,IAAI,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;AACjD,KAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;AAkBG;AACH,eAAe,mBAAmB,CAAC,KAAmB,EAAA;IACpD,OAAO,MAAM,CAAC,2CAA2C,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;;;;;;;AAYG;AACH,eAAe,OAAO,GAAA;AACpB,IAAA,OAAO,MAAM,CAAC,iCAAiC,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;;;;;;AAYG;AACH,eAAe,MAAM,CAAC,aAAuB,EAAA;IAC3C,OAAO,MAAM,CAAC,4BAA4B,EAAE,EAAE,aAAa,EAAE,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;;;;;;;AAYG;AACH,eAAe,SAAS,GAAA;AACtB,IAAA,OAAO,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;AAYG;AACH,eAAe,MAAM,GAAA;AACnB,IAAA,OAAO,MAAM,CAAC,gCAAgC,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;;;;;;AAYG;AACH,eAAe,YAAY,CAAC,aAAuB,EAAA;IACjD,OAAO,MAAM,CAAC,mCAAmC,EAAE,EAAE,aAAa,EAAE,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;;;;;;;AAYG;AACH,eAAe,eAAe,GAAA;AAC5B,IAAA,OAAO,MAAM,CAAC,mCAAmC,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;;AAmBG;AACH,eAAe,aAAa,CAAC,OAAgB,EAAA;IAC3C,OAAO,MAAM,CAAC,oCAAoC,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;;AAYG;AACH,eAAe,aAAa,CAAC,EAAU,EAAA;IACrC,OAAO,MAAM,CAAC,oCAAoC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;AAYG;AACH,eAAe,QAAQ,GAAA;AACrB,IAAA,OAAO,MAAM,CAAC,+BAA+B,CAAC,CAAC;AACjD,CAAC;AAED,eAAe,sBAAsB,CACnC,EAAmC,EAAA;IAEnC,OAAO,iBAAiB,CAAC,cAAc,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,eAAe,QAAQ,CACrB,EAAmC,EAAA;IAEnC,OAAO,iBAAiB,CAAC,cAAc,EAAE,iBAAiB,EAAE,EAAE,CAAC,CAAC;AAClE;;;;"}
|
||||
584
guest-js/index.ts
Normal file
584
guest-js/index.ts
Normal file
@@ -0,0 +1,584 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Send toast notifications (brief auto-expiring OS window element) to your user.
|
||||
* Can also be used with the Notification Web API.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
invoke,
|
||||
PluginListener,
|
||||
addPluginListener,
|
||||
} from "@tauri-apps/api/tauri";
|
||||
|
||||
/**
|
||||
* Options to send a notification.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
interface Options {
|
||||
/**
|
||||
* The notification identifier to reference this object later. Must be a 32-bit integer.
|
||||
*/
|
||||
id?: number;
|
||||
/**
|
||||
* Identifier of the {@link Channel} that deliveres this notification.
|
||||
*
|
||||
* If the channel does not exist, the notification won't fire.
|
||||
* Make sure the channel exists with {@link listChannels} and {@link createChannel}.
|
||||
*/
|
||||
channelId?: string;
|
||||
/**
|
||||
* Notification title.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Optional notification body.
|
||||
* */
|
||||
body?: string;
|
||||
/**
|
||||
* Schedule this notification to fire on a later time or a fixed interval.
|
||||
*/
|
||||
schedule?: Schedule;
|
||||
/**
|
||||
* Multiline text.
|
||||
* Changes the notification style to big text.
|
||||
* Cannot be used with `inboxLines`.
|
||||
*/
|
||||
largeBody?: string;
|
||||
/**
|
||||
* Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`.
|
||||
*/
|
||||
summary?: string;
|
||||
/**
|
||||
* Defines an action type for this notification.
|
||||
*/
|
||||
actionTypeId?: string;
|
||||
/**
|
||||
* Identifier used to group multiple notifications.
|
||||
*
|
||||
* https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier
|
||||
*/
|
||||
group?: string;
|
||||
/**
|
||||
* Instructs the system that this notification is the summary of a group on Android.
|
||||
*/
|
||||
groupSummary?: boolean;
|
||||
/**
|
||||
* The sound resource name. Only available on mobile.
|
||||
*/
|
||||
sound?: string;
|
||||
/**
|
||||
* List of lines to add to the notification.
|
||||
* Changes the notification style to inbox.
|
||||
* Cannot be used with `largeBody`.
|
||||
*
|
||||
* Only supports up to 5 lines.
|
||||
*/
|
||||
inboxLines?: string[];
|
||||
/**
|
||||
* Notification icon.
|
||||
*
|
||||
* On Android the icon must be placed in the app's `res/drawable` folder.
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* Notification large icon (Android).
|
||||
*
|
||||
* The icon must be placed in the app's `res/drawable` folder.
|
||||
*/
|
||||
largeIcon?: string;
|
||||
/**
|
||||
* Icon color on Android.
|
||||
*/
|
||||
iconColor?: string;
|
||||
/**
|
||||
* Notification attachments.
|
||||
*/
|
||||
attachments?: Attachment[];
|
||||
/**
|
||||
* Extra payload to store in the notification.
|
||||
*/
|
||||
extra?: { [key: string]: unknown };
|
||||
/**
|
||||
* If true, the notification cannot be dismissed by the user on Android.
|
||||
*
|
||||
* An application service must manage the dismissal of the notification.
|
||||
* It is typically used to indicate a background task that is pending (e.g. a file download)
|
||||
* or the user is engaged with (e.g. playing music).
|
||||
*/
|
||||
ongoing?: boolean;
|
||||
/**
|
||||
* Automatically cancel the notification when the user clicks on it.
|
||||
*/
|
||||
autoCancel?: boolean;
|
||||
/**
|
||||
* Changes the notification presentation to be silent on iOS (no badge, no sound, not listed).
|
||||
*/
|
||||
silent?: boolean;
|
||||
/**
|
||||
* Notification visibility.
|
||||
*/
|
||||
visibility?: Visibility;
|
||||
/**
|
||||
* Sets the number of items this notification represents on Android.
|
||||
*/
|
||||
number?: number;
|
||||
}
|
||||
|
||||
type ScheduleInterval = {
|
||||
year?: number;
|
||||
month?: number;
|
||||
day?: number;
|
||||
/**
|
||||
* 1 - Sunday
|
||||
* 2 - Monday
|
||||
* 3 - Tuesday
|
||||
* 4 - Wednesday
|
||||
* 5 - Thursday
|
||||
* 6 - Friday
|
||||
* 7 - Saturday
|
||||
*/
|
||||
weekday?: number;
|
||||
hour?: number;
|
||||
minute?: number;
|
||||
second?: number;
|
||||
};
|
||||
|
||||
enum ScheduleEvery {
|
||||
Year = "Year",
|
||||
Month = "Month",
|
||||
TwoWeeks = "TwoWeeks",
|
||||
Week = "Week",
|
||||
Day = "Day",
|
||||
Hour = "Hour",
|
||||
Minute = "Minute",
|
||||
/**
|
||||
* Not supported on iOS.
|
||||
*/
|
||||
Second = "Second",
|
||||
}
|
||||
|
||||
type ScheduleData =
|
||||
| {
|
||||
kind: "At";
|
||||
data: {
|
||||
date: Date;
|
||||
repeating: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
kind: "Interval";
|
||||
data: ScheduleInterval;
|
||||
}
|
||||
| {
|
||||
kind: "Every";
|
||||
data: {
|
||||
interval: ScheduleEvery;
|
||||
};
|
||||
};
|
||||
|
||||
class Schedule {
|
||||
kind: string;
|
||||
data: unknown;
|
||||
|
||||
private constructor(schedule: ScheduleData) {
|
||||
this.kind = schedule.kind;
|
||||
this.data = schedule.data;
|
||||
}
|
||||
|
||||
static at(date: Date, repeating = false) {
|
||||
return new Schedule({ kind: "At", data: { date, repeating } });
|
||||
}
|
||||
|
||||
static interval(interval: ScheduleInterval) {
|
||||
return new Schedule({ kind: "Interval", data: interval });
|
||||
}
|
||||
|
||||
static every(kind: ScheduleEvery) {
|
||||
return new Schedule({ kind: "Every", data: { interval: kind } });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachment of a notification.
|
||||
*/
|
||||
interface Attachment {
|
||||
/** Attachment identifier. */
|
||||
id: string;
|
||||
/** Attachment URL. Accepts the `asset` and `file` protocols. */
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface Action {
|
||||
id: string;
|
||||
title: string;
|
||||
requiresAuthentication?: boolean;
|
||||
foreground?: boolean;
|
||||
destructive?: boolean;
|
||||
input?: boolean;
|
||||
inputButtonTitle?: string;
|
||||
inputPlaceholder?: string;
|
||||
}
|
||||
|
||||
interface ActionType {
|
||||
/**
|
||||
* The identifier of this action type
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The list of associated actions
|
||||
*/
|
||||
actions: Action[];
|
||||
hiddenPreviewsBodyPlaceholder?: string;
|
||||
customDismissAction?: boolean;
|
||||
allowInCarPlay?: boolean;
|
||||
hiddenPreviewsShowTitle?: boolean;
|
||||
hiddenPreviewsShowSubtitle?: boolean;
|
||||
}
|
||||
|
||||
interface PendingNotification {
|
||||
id: number;
|
||||
title?: string;
|
||||
body?: string;
|
||||
schedule: Schedule;
|
||||
}
|
||||
|
||||
interface ActiveNotification {
|
||||
id: number;
|
||||
tag?: string;
|
||||
title?: string;
|
||||
body?: string;
|
||||
group?: string;
|
||||
groupSummary: boolean;
|
||||
data: Record<string, string>;
|
||||
extra: Record<string, unknown>;
|
||||
attachments: Attachment[];
|
||||
actionTypeId?: string;
|
||||
schedule?: Schedule;
|
||||
sound?: string;
|
||||
}
|
||||
|
||||
enum Importance {
|
||||
None = 0,
|
||||
Min,
|
||||
Low,
|
||||
Default,
|
||||
High,
|
||||
}
|
||||
|
||||
enum Visibility {
|
||||
Secret = -1,
|
||||
Private,
|
||||
Public,
|
||||
}
|
||||
|
||||
interface Channel {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
sound?: string;
|
||||
lights?: boolean;
|
||||
lightColor?: string;
|
||||
vibration?: boolean;
|
||||
importance?: Importance;
|
||||
visibility?: Visibility;
|
||||
}
|
||||
|
||||
/** Possible permission values. */
|
||||
type Permission = "granted" | "denied" | "default";
|
||||
|
||||
/**
|
||||
* Checks if the permission to send notifications is granted.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted } from '@tauri-apps/api/notification';
|
||||
* const permissionGranted = await isPermissionGranted();
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function isPermissionGranted(): Promise<boolean> {
|
||||
if (window.Notification.permission !== "default") {
|
||||
return Promise.resolve(window.Notification.permission === "granted");
|
||||
}
|
||||
return invoke("plugin:notification|is_permission_granted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the permission to send notifications.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted, requestPermission } from '@tauri-apps/api/notification';
|
||||
* let permissionGranted = await isPermissionGranted();
|
||||
* if (!permissionGranted) {
|
||||
* const permission = await requestPermission();
|
||||
* permissionGranted = permission === 'granted';
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to whether the user granted the permission or not.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function requestPermission(): Promise<Permission> {
|
||||
return window.Notification.requestPermission();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the user.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/api/notification';
|
||||
* let permissionGranted = await isPermissionGranted();
|
||||
* if (!permissionGranted) {
|
||||
* const permission = await requestPermission();
|
||||
* permissionGranted = permission === 'granted';
|
||||
* }
|
||||
* if (permissionGranted) {
|
||||
* sendNotification('Tauri is awesome!');
|
||||
* sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
function sendNotification(options: Options | string): void {
|
||||
if (typeof options === "string") {
|
||||
// eslint-disable-next-line no-new
|
||||
new window.Notification(options);
|
||||
} else {
|
||||
// eslint-disable-next-line no-new
|
||||
new window.Notification(options.title, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register actions that are performed when the user clicks on the notification.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { registerActionTypes } from '@tauri-apps/api/notification';
|
||||
* await registerActionTypes([{
|
||||
* id: 'tauri',
|
||||
* actions: [{
|
||||
* id: 'my-action',
|
||||
* title: 'Settings'
|
||||
* }]
|
||||
* }])
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function registerActionTypes(types: ActionType[]): Promise<void> {
|
||||
return invoke("plugin:notification|register_action_types", { types });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of pending notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { pending } from '@tauri-apps/api/notification';
|
||||
* const pendingNotifications = await pending();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of pending notifications.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function pending(): Promise<PendingNotification[]> {
|
||||
return invoke("plugin:notification|get_pending");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the pending notifications with the given list of identifiers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancel } from '@tauri-apps/api/notification';
|
||||
* await cancel([-34234, 23432, 4311]);
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function cancel(notifications: number[]): Promise<void> {
|
||||
return invoke("plugin:notification|cancel", { notifications });
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all pending notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancelAll } from '@tauri-apps/api/notification';
|
||||
* await cancelAll();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function cancelAll(): Promise<void> {
|
||||
return invoke("plugin:notification|cancel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { active } from '@tauri-apps/api/notification';
|
||||
* const activeNotifications = await active();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of active notifications.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function active(): Promise<ActiveNotification[]> {
|
||||
return invoke("plugin:notification|get_active");
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the active notifications with the given list of identifiers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cancel } from '@tauri-apps/api/notification';
|
||||
* await cancel([-34234, 23432, 4311])
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function removeActive(notifications: number[]): Promise<void> {
|
||||
return invoke("plugin:notification|remove_active", { notifications });
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { removeAllActive } from '@tauri-apps/api/notification';
|
||||
* await removeAllActive()
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function removeAllActive(): Promise<void> {
|
||||
return invoke("plugin:notification|remove_active");
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all active notifications.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createChannel, Importance, Visibility } from '@tauri-apps/api/notification';
|
||||
* await createChannel({
|
||||
* id: 'new-messages',
|
||||
* name: 'New Messages',
|
||||
* lights: true,
|
||||
* vibration: true,
|
||||
* importance: Importance.Default,
|
||||
* visibility: Visibility.Private
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function createChannel(channel: Channel): Promise<void> {
|
||||
return invoke("plugin:notification|create_channel", { ...channel });
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the channel with the given identifier.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { removeChannel } from '@tauri-apps/api/notification';
|
||||
* await removeChannel();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function removeChannel(id: string): Promise<void> {
|
||||
return invoke("plugin:notification|delete_channel", { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of notification channels.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { channels } from '@tauri-apps/api/notification';
|
||||
* const notificationChannels = await channels();
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the list of notification channels.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function channels(): Promise<Channel[]> {
|
||||
return invoke("plugin:notification|getActive");
|
||||
}
|
||||
|
||||
async function onNotificationReceived(
|
||||
cb: (notification: Options) => void
|
||||
): Promise<PluginListener> {
|
||||
return addPluginListener("notification", "notification", cb);
|
||||
}
|
||||
|
||||
async function onAction(
|
||||
cb: (notification: Options) => void
|
||||
): Promise<PluginListener> {
|
||||
return addPluginListener("notification", "actionPerformed", cb);
|
||||
}
|
||||
|
||||
export type {
|
||||
Attachment,
|
||||
Options,
|
||||
Permission,
|
||||
Action,
|
||||
ActionType,
|
||||
PendingNotification,
|
||||
ActiveNotification,
|
||||
Channel,
|
||||
};
|
||||
|
||||
export {
|
||||
Importance,
|
||||
Visibility,
|
||||
sendNotification,
|
||||
requestPermission,
|
||||
isPermissionGranted,
|
||||
registerActionTypes,
|
||||
pending,
|
||||
cancel,
|
||||
cancelAll,
|
||||
active,
|
||||
removeActive,
|
||||
removeAllActive,
|
||||
createChannel,
|
||||
removeChannel,
|
||||
channels,
|
||||
onNotificationReceived,
|
||||
onAction,
|
||||
};
|
||||
10
ios/.gitignore
vendored
Normal file
10
ios/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
||||
35
ios/Package.swift
Normal file
35
ios/Package.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
// swift-tools-version:5.3
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-notification",
|
||||
platforms: [
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-notification",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-notification"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-notification",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
||||
3
ios/README.md
Normal file
3
ios/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Tauri Plugin {{ plugin_name_original }}
|
||||
|
||||
A description of this package.
|
||||
276
ios/Sources/Notification.swift
Normal file
276
ios/Sources/Notification.swift
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Tauri
|
||||
import UserNotifications
|
||||
|
||||
enum NotificationError: LocalizedError {
|
||||
case contentNoId
|
||||
case contentNoTitle
|
||||
case contentNoBody
|
||||
case triggerRepeatIntervalTooShort
|
||||
case attachmentNoId
|
||||
case attachmentNoUrl
|
||||
case attachmentFileNotFound(path: String)
|
||||
case attachmentUnableToCreate(String)
|
||||
case pastScheduledTime
|
||||
case invalidDate(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .contentNoId:
|
||||
return "Missing notification identifier"
|
||||
case .contentNoTitle:
|
||||
return "Missing notification title"
|
||||
case .contentNoBody:
|
||||
return "Missing notification body"
|
||||
case .triggerRepeatIntervalTooShort:
|
||||
return "Schedule interval too short, must be a least 1 minute"
|
||||
case .attachmentNoId:
|
||||
return "Missing attachment identifier"
|
||||
case .attachmentNoUrl:
|
||||
return "Missing attachment URL"
|
||||
case .attachmentFileNotFound(let path):
|
||||
return "Unable to find file \(path) for attachment"
|
||||
case .attachmentUnableToCreate(let error):
|
||||
return "Failed to create attachment: \(error)"
|
||||
case .pastScheduledTime:
|
||||
return "Scheduled time must be *after* current time"
|
||||
case .invalidDate(let date):
|
||||
return "Could not parse date \(date)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeNotificationContent(_ notification: JSObject) throws -> UNNotificationContent {
|
||||
guard let title = notification["title"] as? String else {
|
||||
throw NotificationError.contentNoTitle
|
||||
}
|
||||
guard let body = notification["body"] as? String else {
|
||||
throw NotificationError.contentNoBody
|
||||
}
|
||||
|
||||
let extra = notification["extra"] as? JSObject ?? [:]
|
||||
let schedule = notification["schedule"] as? JSObject ?? [:]
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSString.localizedUserNotificationString(forKey: title, arguments: nil)
|
||||
content.body = NSString.localizedUserNotificationString(
|
||||
forKey: body,
|
||||
arguments: nil)
|
||||
|
||||
content.userInfo = [
|
||||
"__EXTRA__": extra,
|
||||
"__SCHEDULE__": schedule,
|
||||
]
|
||||
|
||||
if let actionTypeId = notification["actionTypeId"] as? String {
|
||||
content.categoryIdentifier = actionTypeId
|
||||
}
|
||||
|
||||
if let threadIdentifier = notification["group"] as? String {
|
||||
content.threadIdentifier = threadIdentifier
|
||||
}
|
||||
|
||||
if let summaryArgument = notification["summary"] as? String {
|
||||
content.summaryArgument = summaryArgument
|
||||
}
|
||||
|
||||
if let sound = notification["sound"] as? String {
|
||||
content.sound = UNNotificationSound(named: UNNotificationSoundName(sound))
|
||||
}
|
||||
|
||||
if let attachments = notification["attachments"] as? [JSObject] {
|
||||
content.attachments = try makeAttachments(attachments)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
func makeAttachments(_ attachments: [JSObject]) throws -> [UNNotificationAttachment] {
|
||||
var createdAttachments = [UNNotificationAttachment]()
|
||||
|
||||
for attachment in attachments {
|
||||
guard let id = attachment["id"] as? String else {
|
||||
throw NotificationError.attachmentNoId
|
||||
}
|
||||
guard let url = attachment["url"] as? String else {
|
||||
throw NotificationError.attachmentNoUrl
|
||||
}
|
||||
guard let urlObject = makeAttachmentUrl(url) else {
|
||||
throw NotificationError.attachmentFileNotFound(path: url)
|
||||
}
|
||||
|
||||
let options = attachment["options"] as? JSObject ?? [:]
|
||||
|
||||
do {
|
||||
let newAttachment = try UNNotificationAttachment(
|
||||
identifier: id, url: urlObject, options: makeAttachmentOptions(options))
|
||||
createdAttachments.append(newAttachment)
|
||||
} catch {
|
||||
throw NotificationError.attachmentUnableToCreate(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
return createdAttachments
|
||||
}
|
||||
|
||||
func makeAttachmentUrl(_ path: String) -> URL? {
|
||||
return URL(string: path)
|
||||
}
|
||||
|
||||
func makeAttachmentOptions(_ options: JSObject) -> JSObject {
|
||||
var opts: JSObject = [:]
|
||||
|
||||
if let iosUNNotificationAttachmentOptionsTypeHintKey = options[
|
||||
"iosUNNotificationAttachmentOptionsTypeHintKey"] as? String
|
||||
{
|
||||
opts[UNNotificationAttachmentOptionsTypeHintKey] = iosUNNotificationAttachmentOptionsTypeHintKey
|
||||
}
|
||||
if let iosUNNotificationAttachmentOptionsThumbnailHiddenKey = options[
|
||||
"iosUNNotificationAttachmentOptionsThumbnailHiddenKey"] as? String
|
||||
{
|
||||
opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] =
|
||||
iosUNNotificationAttachmentOptionsThumbnailHiddenKey
|
||||
}
|
||||
if let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey = options[
|
||||
"iosUNNotificationAttachmentOptionsThumbnailClippingRectKey"] as? String
|
||||
{
|
||||
opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] =
|
||||
iosUNNotificationAttachmentOptionsThumbnailClippingRectKey
|
||||
}
|
||||
if let iosUNNotificationAttachmentOptionsThumbnailTimeKey = options[
|
||||
"iosUNNotificationAttachmentOptionsThumbnailTimeKey"] as? String
|
||||
{
|
||||
opts[UNNotificationAttachmentOptionsThumbnailTimeKey] =
|
||||
iosUNNotificationAttachmentOptionsThumbnailTimeKey
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func handleScheduledNotification(_ schedule: JSObject) throws
|
||||
-> UNNotificationTrigger?
|
||||
{
|
||||
let kind = schedule["kind"] as? String ?? ""
|
||||
let payload = schedule["data"] as? JSObject ?? [:]
|
||||
switch kind {
|
||||
case "At":
|
||||
let date = payload["date"] as? String ?? ""
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||
|
||||
if let at = dateFormatter.date(from: date) {
|
||||
let repeats = payload["repeats"] as? Bool ?? false
|
||||
|
||||
let dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: at)
|
||||
|
||||
if dateInfo.date! < Date() {
|
||||
throw NotificationError.pastScheduledTime
|
||||
}
|
||||
|
||||
let dateInterval = DateInterval(start: Date(), end: dateInfo.date!)
|
||||
|
||||
// Notifications that repeat have to be at least a minute between each other
|
||||
if repeats && dateInterval.duration < 60 {
|
||||
throw NotificationError.triggerRepeatIntervalTooShort
|
||||
}
|
||||
|
||||
return UNTimeIntervalNotificationTrigger(
|
||||
timeInterval: dateInterval.duration, repeats: repeats)
|
||||
|
||||
} else {
|
||||
throw NotificationError.invalidDate(date)
|
||||
}
|
||||
case "Interval":
|
||||
let dateComponents = getDateComponents(payload)
|
||||
return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
|
||||
case "Every":
|
||||
let interval = payload["interval"] as? String ?? ""
|
||||
let count = schedule["count"] as? Int ?? 1
|
||||
|
||||
if let repeatDateInterval = getRepeatDateInterval(interval, count) {
|
||||
// Notifications that repeat have to be at least a minute between each other
|
||||
if repeatDateInterval.duration < 60 {
|
||||
throw NotificationError.triggerRepeatIntervalTooShort
|
||||
}
|
||||
|
||||
return UNTimeIntervalNotificationTrigger(
|
||||
timeInterval: repeatDateInterval.duration, repeats: true)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Given our schedule format, return a DateComponents object
|
||||
/// that only contains the components passed in.
|
||||
|
||||
func getDateComponents(_ at: JSObject) -> DateComponents {
|
||||
// var dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: Date())
|
||||
// dateInfo.calendar = Calendar.current
|
||||
var dateInfo = DateComponents()
|
||||
|
||||
if let year = at["year"] as? Int {
|
||||
dateInfo.year = year
|
||||
}
|
||||
if let month = at["month"] as? Int {
|
||||
dateInfo.month = month
|
||||
}
|
||||
if let day = at["day"] as? Int {
|
||||
dateInfo.day = day
|
||||
}
|
||||
if let hour = at["hour"] as? Int {
|
||||
dateInfo.hour = hour
|
||||
}
|
||||
if let minute = at["minute"] as? Int {
|
||||
dateInfo.minute = minute
|
||||
}
|
||||
if let second = at["second"] as? Int {
|
||||
dateInfo.second = second
|
||||
}
|
||||
if let weekday = at["weekday"] as? Int {
|
||||
dateInfo.weekday = weekday
|
||||
}
|
||||
return dateInfo
|
||||
}
|
||||
|
||||
/// Compute the difference between the string representation of a date
|
||||
/// interval and today. For example, if every is "month", then we
|
||||
/// return the interval between today and a month from today.
|
||||
|
||||
func getRepeatDateInterval(_ every: String, _ count: Int) -> DateInterval? {
|
||||
let cal = Calendar.current
|
||||
let now = Date()
|
||||
switch every {
|
||||
case "Year":
|
||||
let newDate = cal.date(byAdding: .year, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Month":
|
||||
let newDate = cal.date(byAdding: .month, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "TwoWeeks":
|
||||
let newDate = cal.date(byAdding: .weekOfYear, value: 2 * count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Week":
|
||||
let newDate = cal.date(byAdding: .weekOfYear, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Day":
|
||||
let newDate = cal.date(byAdding: .day, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Hour":
|
||||
let newDate = cal.date(byAdding: .hour, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Minute":
|
||||
let newDate = cal.date(byAdding: .minute, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Second":
|
||||
let newDate = cal.date(byAdding: .second, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
135
ios/Sources/NotificationCategory.swift
Normal file
135
ios/Sources/NotificationCategory.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Tauri
|
||||
import UserNotifications
|
||||
|
||||
enum CategoryError: LocalizedError {
|
||||
case noId
|
||||
case noActionId
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noId:
|
||||
return "Action type `id` missing"
|
||||
case .noActionId:
|
||||
return "Action `id` missing"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func makeCategories(_ actionTypes: [JSObject]) throws {
|
||||
var createdCategories = [UNNotificationCategory]()
|
||||
|
||||
let generalCategory = UNNotificationCategory(
|
||||
identifier: "GENERAL",
|
||||
actions: [],
|
||||
intentIdentifiers: [],
|
||||
options: .customDismissAction)
|
||||
|
||||
createdCategories.append(generalCategory)
|
||||
for type in actionTypes {
|
||||
guard let id = type["id"] as? String else {
|
||||
throw CategoryError.noId
|
||||
}
|
||||
let hiddenBodyPlaceholder = type["hiddenPreviewsBodyPlaceholder"] as? String ?? ""
|
||||
let actions = type["actions"] as? [JSObject] ?? []
|
||||
|
||||
let newActions = try makeActions(actions)
|
||||
|
||||
// Create the custom actions for the TIMER_EXPIRED category.
|
||||
var newCategory: UNNotificationCategory?
|
||||
|
||||
newCategory = UNNotificationCategory(
|
||||
identifier: id,
|
||||
actions: newActions,
|
||||
intentIdentifiers: [],
|
||||
hiddenPreviewsBodyPlaceholder: hiddenBodyPlaceholder,
|
||||
options: makeCategoryOptions(type))
|
||||
|
||||
createdCategories.append(newCategory!)
|
||||
}
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.setNotificationCategories(Set(createdCategories))
|
||||
}
|
||||
|
||||
func makeActions(_ actions: [JSObject]) throws -> [UNNotificationAction] {
|
||||
var createdActions = [UNNotificationAction]()
|
||||
|
||||
for action in actions {
|
||||
guard let id = action["id"] as? String else {
|
||||
throw CategoryError.noActionId
|
||||
}
|
||||
let title = action["title"] as? String ?? ""
|
||||
let input = action["input"] as? Bool ?? false
|
||||
|
||||
var newAction: UNNotificationAction
|
||||
if input {
|
||||
let inputButtonTitle = action["inputButtonTitle"] as? String
|
||||
let inputPlaceholder = action["inputPlaceholder"] as? String ?? ""
|
||||
|
||||
if inputButtonTitle != nil {
|
||||
newAction = UNTextInputNotificationAction(
|
||||
identifier: id,
|
||||
title: title,
|
||||
options: makeActionOptions(action),
|
||||
textInputButtonTitle: inputButtonTitle!,
|
||||
textInputPlaceholder: inputPlaceholder)
|
||||
} else {
|
||||
newAction = UNTextInputNotificationAction(
|
||||
identifier: id, title: title, options: makeActionOptions(action))
|
||||
}
|
||||
} else {
|
||||
// Create the custom actions for the TIMER_EXPIRED category.
|
||||
newAction = UNNotificationAction(
|
||||
identifier: id,
|
||||
title: title,
|
||||
options: makeActionOptions(action))
|
||||
}
|
||||
createdActions.append(newAction)
|
||||
}
|
||||
|
||||
return createdActions
|
||||
}
|
||||
|
||||
func makeActionOptions(_ action: JSObject) -> UNNotificationActionOptions {
|
||||
let foreground = action["foreground"] as? Bool ?? false
|
||||
let destructive = action["destructive"] as? Bool ?? false
|
||||
let requiresAuthentication = action["requiresAuthentication"] as? Bool ?? false
|
||||
|
||||
if foreground {
|
||||
return .foreground
|
||||
}
|
||||
if destructive {
|
||||
return .destructive
|
||||
}
|
||||
if requiresAuthentication {
|
||||
return .authenticationRequired
|
||||
}
|
||||
return UNNotificationActionOptions(rawValue: 0)
|
||||
}
|
||||
|
||||
func makeCategoryOptions(_ type: JSObject) -> UNNotificationCategoryOptions {
|
||||
let customDismiss = type["customDismissAction"] as? Bool ?? false
|
||||
let carPlay = type["allowInCarPlay"] as? Bool ?? false
|
||||
let hiddenPreviewsShowTitle = type["hiddenPreviewsShowTitle"] as? Bool ?? false
|
||||
let hiddenPreviewsShowSubtitle = type["hiddenPreviewsShowSubtitle"] as? Bool ?? false
|
||||
|
||||
if customDismiss {
|
||||
return .customDismissAction
|
||||
}
|
||||
if carPlay {
|
||||
return .allowInCarPlay
|
||||
}
|
||||
|
||||
if hiddenPreviewsShowTitle {
|
||||
return .hiddenPreviewsShowTitle
|
||||
}
|
||||
if hiddenPreviewsShowSubtitle {
|
||||
return .hiddenPreviewsShowSubtitle
|
||||
}
|
||||
|
||||
return UNNotificationCategoryOptions(rawValue: 0)
|
||||
}
|
||||
120
ios/Sources/NotificationHandler.swift
Normal file
120
ios/Sources/NotificationHandler.swift
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Tauri
|
||||
import UserNotifications
|
||||
|
||||
public class NotificationHandler: NSObject, NotificationHandlerProtocol {
|
||||
|
||||
public weak var plugin: Plugin?
|
||||
|
||||
private var notificationsMap = [String: JSObject]()
|
||||
|
||||
public func saveNotification(_ key: String, _ notification: JSObject) {
|
||||
notificationsMap.updateValue(notification, forKey: key)
|
||||
}
|
||||
|
||||
public func requestPermissions(with completion: ((Bool, Error?) -> Void)? = nil) {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.badge, .alert, .sound]) { (granted, error) in
|
||||
completion?(granted, error)
|
||||
}
|
||||
}
|
||||
|
||||
public func checkPermissions(with completion: ((UNAuthorizationStatus) -> Void)? = nil) {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.getNotificationSettings { settings in
|
||||
completion?(settings.authorizationStatus)
|
||||
}
|
||||
}
|
||||
|
||||
public func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions {
|
||||
let notificationData = makeNotificationRequestJSObject(notification.request)
|
||||
self.plugin?.trigger("notification", data: notificationData)
|
||||
|
||||
if let options = notificationsMap[notification.request.identifier] {
|
||||
let silent = options["silent"] as? Bool ?? false
|
||||
if silent {
|
||||
return UNNotificationPresentationOptions.init(rawValue: 0)
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
.badge,
|
||||
.sound,
|
||||
.alert,
|
||||
]
|
||||
}
|
||||
|
||||
public func didReceive(response: UNNotificationResponse) {
|
||||
var data = JSObject()
|
||||
|
||||
let originalNotificationRequest = response.notification.request
|
||||
let actionId = response.actionIdentifier
|
||||
|
||||
// We turn the two default actions (open/dismiss) into generic strings
|
||||
if actionId == UNNotificationDefaultActionIdentifier {
|
||||
data["actionId"] = "tap"
|
||||
} else if actionId == UNNotificationDismissActionIdentifier {
|
||||
data["actionId"] = "dismiss"
|
||||
} else {
|
||||
data["actionId"] = actionId
|
||||
}
|
||||
|
||||
// If the type of action was for an input type, get the value
|
||||
if let inputType = response as? UNTextInputNotificationResponse {
|
||||
data["inputValue"] = inputType.userText
|
||||
}
|
||||
|
||||
data["notification"] = makeNotificationRequestJSObject(originalNotificationRequest)
|
||||
|
||||
self.plugin?.trigger("actionPerformed", data: data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a UNNotificationRequest into a JSObject to return back to the client.
|
||||
*/
|
||||
func makeNotificationRequestJSObject(_ request: UNNotificationRequest) -> JSObject {
|
||||
let notificationRequest = notificationsMap[request.identifier] ?? [:]
|
||||
var notification = makePendingNotificationRequestJSObject(request)
|
||||
notification["sound"] = notificationRequest["sound"] ?? ""
|
||||
notification["actionTypeId"] = request.content.categoryIdentifier
|
||||
notification["attachments"] = notificationRequest["attachments"] ?? [JSObject]()
|
||||
return notification
|
||||
}
|
||||
|
||||
func makePendingNotificationRequestJSObject(_ request: UNNotificationRequest) -> JSObject {
|
||||
var notification: JSObject = [
|
||||
"id": Int(request.identifier) ?? -1,
|
||||
"title": request.content.title,
|
||||
"body": request.content.body,
|
||||
]
|
||||
|
||||
if let userInfo = JSTypes.coerceDictionaryToJSObject(request.content.userInfo) {
|
||||
var extra = userInfo["__EXTRA__"] as? JSObject ?? userInfo
|
||||
|
||||
// check for any dates and convert them to strings
|
||||
for (key, value) in extra {
|
||||
if let date = value as? Date {
|
||||
let dateString = ISO8601DateFormatter().string(from: date)
|
||||
extra[key] = dateString
|
||||
}
|
||||
}
|
||||
|
||||
notification["extra"] = extra
|
||||
|
||||
if var schedule = userInfo["__SCHEDULE__"] as? JSObject {
|
||||
// convert schedule at date to string
|
||||
if let date = schedule["at"] as? Date {
|
||||
let dateString = ISO8601DateFormatter().string(from: date)
|
||||
schedule["at"] = dateString
|
||||
}
|
||||
|
||||
notification["schedule"] = schedule
|
||||
}
|
||||
}
|
||||
|
||||
return notification
|
||||
}
|
||||
}
|
||||
43
ios/Sources/NotificationManager.swift
Normal file
43
ios/Sources/NotificationManager.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
@objc public protocol NotificationHandlerProtocol {
|
||||
func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions
|
||||
func didReceive(response: UNNotificationResponse)
|
||||
}
|
||||
|
||||
@objc public class NotificationManager: NSObject, UNUserNotificationCenterDelegate {
|
||||
public weak var notificationHandler: NotificationHandlerProtocol?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.delegate = self
|
||||
}
|
||||
|
||||
public func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
var presentationOptions: UNNotificationPresentationOptions? = nil
|
||||
|
||||
if notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true {
|
||||
presentationOptions = notificationHandler?.willPresent(notification: notification)
|
||||
}
|
||||
|
||||
completionHandler(presentationOptions ?? [])
|
||||
}
|
||||
|
||||
public func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
if response.notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true {
|
||||
notificationHandler?.didReceive(response: response)
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
213
ios/Sources/NotificationPlugin.swift
Normal file
213
ios/Sources/NotificationPlugin.swift
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import SwiftRs
|
||||
import Tauri
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import WebKit
|
||||
|
||||
enum ShowNotificationError: LocalizedError {
|
||||
case noId
|
||||
case make(Error)
|
||||
case create(Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noId:
|
||||
return "notification `id` missing"
|
||||
case .make(let error):
|
||||
return "Unable to make notification: \(error)"
|
||||
case .create(let error):
|
||||
return "Unable to create notification: \(error)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showNotification(invoke: Invoke, notification: JSObject)
|
||||
throws -> UNNotificationRequest
|
||||
{
|
||||
guard let identifier = notification["id"] as? Int else {
|
||||
throw ShowNotificationError.noId
|
||||
}
|
||||
|
||||
var content: UNNotificationContent
|
||||
do {
|
||||
content = try makeNotificationContent(notification)
|
||||
} catch {
|
||||
throw ShowNotificationError.make(error)
|
||||
}
|
||||
|
||||
var trigger: UNNotificationTrigger?
|
||||
|
||||
do {
|
||||
if let schedule = notification["schedule"] as? JSObject {
|
||||
try trigger = handleScheduledNotification(schedule)
|
||||
}
|
||||
} catch {
|
||||
throw ShowNotificationError.create(error)
|
||||
}
|
||||
|
||||
// Schedule the request.
|
||||
let request = UNNotificationRequest(
|
||||
identifier: "\(identifier)", content: content, trigger: trigger
|
||||
)
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.add(request) { (error: Error?) in
|
||||
if let theError = error {
|
||||
invoke.reject(theError.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
class NotificationPlugin: Plugin {
|
||||
let notificationHandler = NotificationHandler()
|
||||
let notificationManager = NotificationManager()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
notificationManager.notificationHandler = notificationHandler
|
||||
notificationHandler.plugin = self
|
||||
}
|
||||
|
||||
@objc public func show(_ invoke: Invoke) throws {
|
||||
let request = try showNotification(invoke: invoke, notification: invoke.data)
|
||||
notificationHandler.saveNotification(request.identifier, invoke.data)
|
||||
invoke.resolve([
|
||||
"id": Int(request.identifier) ?? -1
|
||||
])
|
||||
}
|
||||
|
||||
@objc public func batch(_ invoke: Invoke) throws {
|
||||
guard let notifications = invoke.getArray("notifications", JSObject.self) else {
|
||||
invoke.reject("`notifications` array is required")
|
||||
return
|
||||
}
|
||||
var ids = [Int]()
|
||||
|
||||
for notification in notifications {
|
||||
let request = try showNotification(invoke: invoke, notification: notification)
|
||||
notificationHandler.saveNotification(request.identifier, notification)
|
||||
ids.append(Int(request.identifier) ?? -1)
|
||||
}
|
||||
|
||||
invoke.resolve([
|
||||
"notifications": ids
|
||||
])
|
||||
}
|
||||
|
||||
@objc public override func requestPermissions(_ invoke: Invoke) {
|
||||
notificationHandler.requestPermissions { granted, error in
|
||||
guard error == nil else {
|
||||
invoke.reject(error!.localizedDescription)
|
||||
return
|
||||
}
|
||||
invoke.resolve(["permissionState": granted ? "granted" : "denied"])
|
||||
}
|
||||
}
|
||||
|
||||
@objc public override func checkPermissions(_ invoke: Invoke) {
|
||||
notificationHandler.checkPermissions { status in
|
||||
let permission: String
|
||||
|
||||
switch status {
|
||||
case .authorized, .ephemeral, .provisional:
|
||||
permission = "granted"
|
||||
case .denied:
|
||||
permission = "denied"
|
||||
case .notDetermined:
|
||||
permission = "default"
|
||||
@unknown default:
|
||||
permission = "default"
|
||||
}
|
||||
|
||||
invoke.resolve(["permissionState": permission])
|
||||
}
|
||||
}
|
||||
|
||||
@objc func cancel(_ invoke: Invoke) {
|
||||
guard let notifications = invoke.getArray("notifications", NSNumber.self),
|
||||
notifications.count > 0
|
||||
else {
|
||||
invoke.reject("`notifications` input is required")
|
||||
return
|
||||
}
|
||||
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(
|
||||
withIdentifiers: notifications.map({ (id) -> String in
|
||||
return id.stringValue
|
||||
})
|
||||
)
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@objc func getPending(_ invoke: Invoke) {
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: {
|
||||
(notifications) in
|
||||
let ret = notifications.compactMap({ [weak self] (notification) -> JSObject? in
|
||||
return self?.notificationHandler.makePendingNotificationRequestJSObject(notification)
|
||||
})
|
||||
|
||||
invoke.resolve([
|
||||
"notifications": ret
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
@objc func registerActionTypes(_ invoke: Invoke) throws {
|
||||
guard let types = invoke.getArray("types", JSObject.self) else {
|
||||
return
|
||||
}
|
||||
try makeCategories(types)
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@objc func removeActive(_ invoke: Invoke) {
|
||||
if let notifications = invoke.getArray("notifications", JSObject.self) {
|
||||
let ids = notifications.map { "\($0["id"] ?? "")" }
|
||||
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids)
|
||||
invoke.resolve()
|
||||
} else {
|
||||
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
|
||||
DispatchQueue.main.async(execute: {
|
||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||
})
|
||||
invoke.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getActive(_ invoke: Invoke) {
|
||||
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: {
|
||||
(notifications) in
|
||||
let ret = notifications.map({ (notification) -> [String: Any] in
|
||||
return self.notificationHandler.makeNotificationRequestJSObject(
|
||||
notification.request)
|
||||
})
|
||||
invoke.resolve([
|
||||
"notifications": ret
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
@objc func createChannel(_ invoke: Invoke) {
|
||||
invoke.reject("not implemented")
|
||||
}
|
||||
|
||||
@objc func deleteChannel(_ invoke: Invoke) {
|
||||
invoke.reject("not implemented")
|
||||
}
|
||||
|
||||
@objc func listChannels(_ invoke: Invoke) {
|
||||
invoke.reject("not implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_notification")
|
||||
func initPlugin() -> Plugin {
|
||||
return NotificationPlugin()
|
||||
}
|
||||
12
ios/Tests/PluginTests/PluginTests.swift
Normal file
12
ios/Tests/PluginTests/PluginTests.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import XCTest
|
||||
@testable import ExamplePlugin
|
||||
|
||||
final class ExamplePluginTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
let plugin = ExamplePlugin()
|
||||
}
|
||||
}
|
||||
1
node_modules/@tauri-apps/api
generated
vendored
Symbolic link
1
node_modules/@tauri-apps/api
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/@tauri-apps+api@2.0.0-alpha.4/node_modules/@tauri-apps/api
|
||||
1
node_modules/tslib
generated
vendored
Symbolic link
1
node_modules/tslib
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../node_modules/.pnpm/tslib@2.4.1/node_modules/tslib
|
||||
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@tauri-apps/plugin-notification",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"authors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"type": "module",
|
||||
"browser": "dist-js/index.min.js",
|
||||
"module": "dist-js/index.mjs",
|
||||
"types": "dist-js/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist-js/index.mjs",
|
||||
"types": "./dist-js/index.d.ts",
|
||||
"browser": "./dist-js/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist-js",
|
||||
"!dist-js/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.0.0-alpha.4"
|
||||
}
|
||||
}
|
||||
11
rollup.config.mjs
Normal file
11
rollup.config.mjs
Normal file
@@ -0,0 +1,11 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import { createConfig } from "../../shared/rollup.config.mjs";
|
||||
|
||||
export default createConfig({
|
||||
input: "guest-js/index.ts",
|
||||
pkg: JSON.parse(
|
||||
readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
||||
),
|
||||
external: [/^@tauri-apps\/api/],
|
||||
});
|
||||
1
src/api-iife.js
Normal file
1
src/api-iife.js
Normal file
@@ -0,0 +1 @@
|
||||
if("__TAURI__"in window){var __TAURI_NOTIFICATION__=function(n){"use strict";var e=Object.defineProperty,i=(n,e,i)=>{if(!e.has(n))throw TypeError("Cannot "+i)},t=(n,e,t)=>(i(n,e,"read from private field"),t?t.call(n):e.get(n)),o=(n,e,t,o)=>(i(n,e,"write to private field"),o?o.call(n,t):e.set(n,t),t);function r(n,e=!1){let i=window.crypto.getRandomValues(new Uint32Array(1))[0],t=`_${i}`;return Object.defineProperty(window,t,{value:i=>(e&&Reflect.deleteProperty(window,t),n?.(i)),writable:!1,configurable:!0}),i}((n,i)=>{for(var t in i)e(n,t,{get:i[t],enumerable:!0})})({},{Channel:()=>a,PluginListener:()=>f,addPluginListener:()=>d,convertFileSrc:()=>p,invoke:()=>_,transformCallback:()=>r});var c,a=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((n,e,i)=>{if(e.has(n))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(n):e.set(n,i)})(this,c,(()=>{})),this.id=r((n=>{t(this,c).call(this,n)}))}set onmessage(n){o(this,c,n)}get onmessage(){return t(this,c)}toJSON(){return`__CHANNEL__:${this.id}`}};c=new WeakMap;var s,l,u,f=class{constructor(n,e,i){this.plugin=n,this.event=e,this.channelId=i}async unregister(){return _(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function d(n,e,i){let t=new a;return t.onmessage=i,_(`plugin:${n}|register_listener`,{event:e,handler:t}).then((()=>new f(n,e,t.id)))}async function _(n,e={}){return new Promise(((i,t)=>{let o=r((n=>{i(n),Reflect.deleteProperty(window,`_${c}`)}),!0),c=r((n=>{t(n),Reflect.deleteProperty(window,`_${o}`)}),!0);window.__TAURI_IPC__({cmd:n,callback:o,error:c,...e})}))}function p(n,e="asset"){let i=encodeURIComponent(n);return navigator.userAgent.includes("Windows")?`https://${e}.localhost/${i}`:`${e}://localhost/${i}`}return function(n){n.Year="Year",n.Month="Month",n.TwoWeeks="TwoWeeks",n.Week="Week",n.Day="Day",n.Hour="Hour",n.Minute="Minute",n.Second="Second"}(s||(s={})),n.Importance=void 0,(l=n.Importance||(n.Importance={}))[l.None=0]="None",l[l.Min=1]="Min",l[l.Low=2]="Low",l[l.Default=3]="Default",l[l.High=4]="High",n.Visibility=void 0,(u=n.Visibility||(n.Visibility={}))[u.Secret=-1]="Secret",u[u.Private=0]="Private",u[u.Public=1]="Public",n.active=async function(){return _("plugin:notification|get_active")},n.cancel=async function(n){return _("plugin:notification|cancel",{notifications:n})},n.cancelAll=async function(){return _("plugin:notification|cancel")},n.channels=async function(){return _("plugin:notification|getActive")},n.createChannel=async function(n){return _("plugin:notification|create_channel",{...n})},n.isPermissionGranted=async function(){return"default"!==window.Notification.permission?Promise.resolve("granted"===window.Notification.permission):_("plugin:notification|is_permission_granted")},n.onAction=async function(n){return d("notification","actionPerformed",n)},n.onNotificationReceived=async function(n){return d("notification","notification",n)},n.pending=async function(){return _("plugin:notification|get_pending")},n.registerActionTypes=async function(n){return _("plugin:notification|register_action_types",{types:n})},n.removeActive=async function(n){return _("plugin:notification|remove_active",{notifications:n})},n.removeAllActive=async function(){return _("plugin:notification|remove_active")},n.removeChannel=async function(n){return _("plugin:notification|delete_channel",{id:n})},n.requestPermission=async function(){return window.Notification.requestPermission()},n.sendNotification=function(n){"string"==typeof n?new window.Notification(n):new window.Notification(n.title,n)},n}({});Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_NOTIFICATION__})}
|
||||
39
src/commands.rs
Normal file
39
src/commands.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use tauri::{command, AppHandle, Runtime, State};
|
||||
|
||||
use crate::{Notification, NotificationData, PermissionState, Result};
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn is_permission_granted<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
notification: State<'_, Notification<R>>,
|
||||
) -> Result<Option<bool>> {
|
||||
let state = notification.permission_state()?;
|
||||
match state {
|
||||
PermissionState::Granted => Ok(Some(true)),
|
||||
PermissionState::Denied => Ok(Some(false)),
|
||||
PermissionState::Unknown => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn request_permission<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
notification: State<'_, Notification<R>>,
|
||||
) -> Result<PermissionState> {
|
||||
notification.request_permission()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn notify<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
notification: State<'_, Notification<R>>,
|
||||
options: NotificationData,
|
||||
) -> Result<()> {
|
||||
let mut builder = notification.builder();
|
||||
builder.data = options;
|
||||
builder.show()
|
||||
}
|
||||
269
src/desktop.rs
Normal file
269
src/desktop.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{plugin::PluginApi, AppHandle, Runtime};
|
||||
|
||||
use crate::{models::*, NotificationBuilder};
|
||||
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
app: &AppHandle<R>,
|
||||
_api: PluginApi<R, C>,
|
||||
) -> crate::Result<Notification<R>> {
|
||||
Ok(Notification(app.clone()))
|
||||
}
|
||||
|
||||
/// Access to the {{ plugin_name }} APIs.
|
||||
pub struct Notification<R: Runtime>(AppHandle<R>);
|
||||
|
||||
impl<R: Runtime> crate::NotificationBuilder<R> {
|
||||
pub fn show(self) -> crate::Result<()> {
|
||||
let mut notification =
|
||||
imp::Notification::new(self.app.config().tauri.bundle.identifier.clone());
|
||||
|
||||
if let Some(title) = self
|
||||
.data
|
||||
.title
|
||||
.or_else(|| self.app.config().package.product_name.clone())
|
||||
{
|
||||
notification = notification.title(title);
|
||||
}
|
||||
if let Some(body) = self.data.body {
|
||||
notification = notification.body(body);
|
||||
}
|
||||
if let Some(icon) = self.data.icon {
|
||||
notification = notification.icon(icon);
|
||||
}
|
||||
#[cfg(feature = "windows7-compat")]
|
||||
{
|
||||
notification.notify(&self.app)?;
|
||||
}
|
||||
#[cfg(not(feature = "windows7-compat"))]
|
||||
notification.show()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Notification<R> {
|
||||
pub fn builder(&self) -> NotificationBuilder<R> {
|
||||
NotificationBuilder::new(self.0.clone())
|
||||
}
|
||||
|
||||
pub fn request_permission(&self) -> crate::Result<PermissionState> {
|
||||
Ok(PermissionState::Granted)
|
||||
}
|
||||
|
||||
pub fn permission_state(&self) -> crate::Result<PermissionState> {
|
||||
Ok(PermissionState::Granted)
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
//! Types and functions related to desktop notifications.
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::path::MAIN_SEPARATOR as SEP;
|
||||
|
||||
/// The desktop notification definition.
|
||||
///
|
||||
/// Allows you to construct a Notification data and send it.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust,no_run
|
||||
/// use tauri_plugin_notification::NotificationExt;
|
||||
/// // first we build the application to access the Tauri configuration
|
||||
/// let app = tauri::Builder::default()
|
||||
/// // on an actual app, remove the string argument
|
||||
/// .build(tauri::generate_context!("test/tauri.conf.json"))
|
||||
/// .expect("error while building tauri application");
|
||||
///
|
||||
/// // shows a notification with the given title and body
|
||||
/// app.notification()
|
||||
/// .builder()
|
||||
/// .title("New message")
|
||||
/// .body("You've got a new message.")
|
||||
/// .show();
|
||||
///
|
||||
/// // run the app
|
||||
/// app.run(|_app_handle, _event| {});
|
||||
/// ```
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Notification {
|
||||
/// The notification body.
|
||||
body: Option<String>,
|
||||
/// The notification title.
|
||||
title: Option<String>,
|
||||
/// The notification icon.
|
||||
icon: Option<String>,
|
||||
/// The notification identifier
|
||||
identifier: String,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
/// Initializes a instance of a Notification.
|
||||
pub fn new(identifier: impl Into<String>) -> Self {
|
||||
Self {
|
||||
identifier: identifier.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the notification body.
|
||||
#[must_use]
|
||||
pub fn body(mut self, body: impl Into<String>) -> Self {
|
||||
self.body = Some(body.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the notification title.
|
||||
#[must_use]
|
||||
pub fn title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the notification icon.
|
||||
#[must_use]
|
||||
pub fn icon(mut self, icon: impl Into<String>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows the notification.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use tauri_plugin_notification::NotificationExt;
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// app.notification()
|
||||
/// .builder()
|
||||
/// .title("Tauri")
|
||||
/// .body("Tauri is awesome!")
|
||||
/// .show()
|
||||
/// .unwrap();
|
||||
/// Ok(())
|
||||
/// })
|
||||
/// .run(tauri::generate_context!("test/tauri.conf.json"))
|
||||
/// .expect("error while running tauri application");
|
||||
/// ```
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows**: Not supported on Windows 7. If your app targets it, enable the `windows7-compat` feature and use [`Self::notify`].
|
||||
#[cfg_attr(
|
||||
all(not(doc_cfg), feature = "windows7-compat"),
|
||||
deprecated = "This function does not work on Windows 7. Use `Self::notify` instead."
|
||||
)]
|
||||
pub fn show(self) -> crate::Result<()> {
|
||||
let mut notification = notify_rust::Notification::new();
|
||||
if let Some(body) = self.body {
|
||||
notification.body(&body);
|
||||
}
|
||||
if let Some(title) = self.title {
|
||||
notification.summary(&title);
|
||||
}
|
||||
if let Some(icon) = self.icon {
|
||||
notification.icon(&icon);
|
||||
} else {
|
||||
notification.auto_icon();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let exe = tauri::utils::platform::current_exe()?;
|
||||
let exe_dir = exe.parent().expect("failed to get exe directory");
|
||||
let curr_dir = exe_dir.display().to_string();
|
||||
// set the notification's System.AppUserModel.ID only when running the installed app
|
||||
if !(curr_dir.ends_with(format!("{SEP}target{SEP}debug").as_str())
|
||||
|| curr_dir.ends_with(format!("{SEP}target{SEP}release").as_str()))
|
||||
{
|
||||
notification.app_id(&self.identifier);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = notify_rust::set_application(if cfg!(feature = "custom-protocol") {
|
||||
&self.identifier
|
||||
} else {
|
||||
"com.apple.Terminal"
|
||||
});
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = notification.show();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shows the notification. This API is similar to [`Self::show`], but it also works on Windows 7.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use tauri_plugin_notification::NotificationExt;
|
||||
///
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(move |app| {
|
||||
/// app.notification().builder()
|
||||
/// .title("Tauri")
|
||||
/// .body("Tauri is awesome!")
|
||||
/// .show()
|
||||
/// .unwrap();
|
||||
/// Ok(())
|
||||
/// })
|
||||
/// .run(tauri::generate_context!("test/tauri.conf.json"))
|
||||
/// .expect("error while running tauri application");
|
||||
/// ```
|
||||
#[cfg(feature = "windows7-compat")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "windows7-compat")))]
|
||||
#[allow(unused_variables)]
|
||||
pub fn notify<R: tauri::Runtime>(self, app: &tauri::AppHandle<R>) -> crate::Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if tauri::utils::platform::is_windows_7() {
|
||||
self.notify_win7(app)
|
||||
} else {
|
||||
#[allow(deprecated)]
|
||||
self.show()
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
self.show()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(windows, feature = "windows7-compat"))]
|
||||
fn notify_win7<R: tauri::Runtime>(self, app: &tauri::AppHandle<R>) -> crate::Result<()> {
|
||||
let app = app.clone();
|
||||
let default_window_icon = app.default_window_icon().cloned();
|
||||
let _ = app.run_on_main_thread(move || {
|
||||
let mut notification = win7_notifications::Notification::new();
|
||||
if let Some(body) = self.body {
|
||||
notification.body(&body);
|
||||
}
|
||||
if let Some(title) = self.title {
|
||||
notification.summary(&title);
|
||||
}
|
||||
if let Some(tauri::Icon::Rgba {
|
||||
rgba,
|
||||
width,
|
||||
height,
|
||||
}) = default_window_icon
|
||||
{
|
||||
notification.icon(rgba, width, height);
|
||||
}
|
||||
let _ = notification.show();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/error.rs
Normal file
25
src/error.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[cfg(mobile)]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
75
src/init.js
Normal file
75
src/init.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
(function () {
|
||||
let permissionSettable = false;
|
||||
let permissionValue = "default";
|
||||
|
||||
function isPermissionGranted() {
|
||||
if (window.Notification.permission !== "default") {
|
||||
return Promise.resolve(window.Notification.permission === "granted");
|
||||
}
|
||||
return window.__TAURI__.invoke("plugin:notification|is_permission_granted");
|
||||
}
|
||||
|
||||
function setNotificationPermission(value) {
|
||||
permissionSettable = true;
|
||||
// @ts-expect-error we can actually set this value on the webview
|
||||
window.Notification.permission = value;
|
||||
permissionSettable = false;
|
||||
}
|
||||
|
||||
function requestPermission() {
|
||||
return window.__TAURI__
|
||||
.invoke("plugin:notification|request_permission")
|
||||
.then(function (permission) {
|
||||
setNotificationPermission(permission);
|
||||
return permission;
|
||||
});
|
||||
}
|
||||
|
||||
function sendNotification(options) {
|
||||
if (typeof options === "object") {
|
||||
Object.freeze(options);
|
||||
}
|
||||
|
||||
return window.__TAURI__.invoke("plugin:notification|notify", {
|
||||
options:
|
||||
typeof options === "string"
|
||||
? {
|
||||
title: options,
|
||||
}
|
||||
: options,
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error unfortunately we can't implement the whole type, so we overwrite it with our own version
|
||||
window.Notification = function (title, options) {
|
||||
const opts = options || {};
|
||||
sendNotification(Object.assign(opts, { title }));
|
||||
};
|
||||
|
||||
window.Notification.requestPermission = requestPermission;
|
||||
|
||||
Object.defineProperty(window.Notification, "permission", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return permissionValue;
|
||||
},
|
||||
set: function (v) {
|
||||
if (!permissionSettable) {
|
||||
throw new Error("Readonly property");
|
||||
}
|
||||
permissionValue = v;
|
||||
},
|
||||
});
|
||||
|
||||
isPermissionGranted().then(function (response) {
|
||||
if (response === null) {
|
||||
setNotificationPermission("default");
|
||||
} else {
|
||||
setNotificationPermission(response ? "granted" : "denied");
|
||||
}
|
||||
});
|
||||
})();
|
||||
233
src/lib.rs
Normal file
233
src/lib.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::Serialize;
|
||||
#[cfg(mobile)]
|
||||
use tauri::plugin::PluginHandle;
|
||||
#[cfg(desktop)]
|
||||
use tauri::AppHandle;
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime,
|
||||
};
|
||||
|
||||
pub use models::*;
|
||||
|
||||
#[cfg(desktop)]
|
||||
mod desktop;
|
||||
#[cfg(mobile)]
|
||||
mod mobile;
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
mod models;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
#[cfg(desktop)]
|
||||
use desktop::Notification;
|
||||
#[cfg(mobile)]
|
||||
use mobile::Notification;
|
||||
|
||||
/// The notification builder.
|
||||
#[derive(Debug)]
|
||||
pub struct NotificationBuilder<R: Runtime> {
|
||||
#[cfg(desktop)]
|
||||
app: AppHandle<R>,
|
||||
#[cfg(mobile)]
|
||||
handle: PluginHandle<R>,
|
||||
pub(crate) data: NotificationData,
|
||||
}
|
||||
|
||||
impl<R: Runtime> NotificationBuilder<R> {
|
||||
#[cfg(desktop)]
|
||||
fn new(app: AppHandle<R>) -> Self {
|
||||
Self {
|
||||
app,
|
||||
data: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
fn new(handle: PluginHandle<R>) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
data: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the notification identifier.
|
||||
pub fn id(mut self, id: i32) -> Self {
|
||||
self.data.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Identifier of the {@link Channel} that deliveres this notification.
|
||||
///
|
||||
/// If the channel does not exist, the notification won't fire.
|
||||
/// Make sure the channel exists with {@link listChannels} and {@link createChannel}.
|
||||
pub fn channel_id(mut self, id: impl Into<String>) -> Self {
|
||||
self.data.channel_id.replace(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the notification title.
|
||||
pub fn title(mut self, title: impl Into<String>) -> Self {
|
||||
self.data.title.replace(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the notification body.
|
||||
pub fn body(mut self, body: impl Into<String>) -> Self {
|
||||
self.data.body.replace(body.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Schedule this notification to fire on a later time or a fixed interval.
|
||||
pub fn schedule(mut self, schedule: Schedule) -> Self {
|
||||
self.data.schedule.replace(schedule);
|
||||
self
|
||||
}
|
||||
|
||||
/// Multiline text.
|
||||
/// Changes the notification style to big text.
|
||||
/// Cannot be used with `inboxLines`.
|
||||
pub fn large_body(mut self, large_body: impl Into<String>) -> Self {
|
||||
self.data.large_body.replace(large_body.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`.
|
||||
pub fn summary(mut self, summary: impl Into<String>) -> Self {
|
||||
self.data.summary.replace(summary.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines an action type for this notification.
|
||||
pub fn action_type_id(mut self, action_type_id: impl Into<String>) -> Self {
|
||||
self.data.action_type_id.replace(action_type_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Identifier used to group multiple notifications.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier
|
||||
pub fn group(mut self, group: impl Into<String>) -> Self {
|
||||
self.data.group.replace(group.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Instructs the system that this notification is the summary of a group on Android.
|
||||
pub fn group_summary(mut self) -> Self {
|
||||
self.data.group_summary = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// The sound resource name. Only available on mobile.
|
||||
pub fn sound(mut self, sound: impl Into<String>) -> Self {
|
||||
self.data.sound.replace(sound.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Append an inbox line to the notification.
|
||||
/// Changes the notification style to inbox.
|
||||
/// Cannot be used with `largeBody`.
|
||||
///
|
||||
/// Only supports up to 5 lines.
|
||||
pub fn inbox_line(mut self, line: impl Into<String>) -> Self {
|
||||
self.data.inbox_lines.push(line.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Notification icon.
|
||||
///
|
||||
/// On Android the icon must be placed in the app's `res/drawable` folder.
|
||||
pub fn icon(mut self, icon: impl Into<String>) -> Self {
|
||||
self.data.icon.replace(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Notification large icon (Android).
|
||||
///
|
||||
/// The icon must be placed in the app's `res/drawable` folder.
|
||||
pub fn large_icon(mut self, large_icon: impl Into<String>) -> Self {
|
||||
self.data.large_icon.replace(large_icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Icon color on Android.
|
||||
pub fn icon_color(mut self, icon_color: impl Into<String>) -> Self {
|
||||
self.data.icon_color.replace(icon_color.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Append an attachment to the notification.
|
||||
pub fn attachment(mut self, attachment: Attachment) -> Self {
|
||||
self.data.attachments.push(attachment);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an extra payload to store in the notification.
|
||||
pub fn extra(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
|
||||
self.data
|
||||
.extra
|
||||
.insert(key.into(), serde_json::to_value(value).unwrap());
|
||||
self
|
||||
}
|
||||
|
||||
/// If true, the notification cannot be dismissed by the user on Android.
|
||||
///
|
||||
/// An application service must manage the dismissal of the notification.
|
||||
/// It is typically used to indicate a background task that is pending (e.g. a file download)
|
||||
/// or the user is engaged with (e.g. playing music).
|
||||
pub fn ongoing(mut self) -> Self {
|
||||
self.data.ongoing = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Automatically cancel the notification when the user clicks on it.
|
||||
pub fn auto_cancel(mut self) -> Self {
|
||||
self.data.auto_cancel = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the notification presentation to be silent on iOS (no badge, no sound, not listed).
|
||||
pub fn silent(mut self) -> Self {
|
||||
self.data.silent = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the notification APIs.
|
||||
pub trait NotificationExt<R: Runtime> {
|
||||
fn notification(&self) -> &Notification<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> crate::NotificationExt<R> for T {
|
||||
fn notification(&self) -> &Notification<R> {
|
||||
self.state::<Notification<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
let mut init_script = include_str!("init.js").to_string();
|
||||
init_script.push_str(include_str!("api-iife.js"));
|
||||
Builder::new("notification")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::notify,
|
||||
commands::request_permission,
|
||||
commands::is_permission_granted
|
||||
])
|
||||
.js_init_script(init_script)
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let notification = mobile::init(app, api)?;
|
||||
#[cfg(desktop)]
|
||||
let notification = desktop::init(app, api)?;
|
||||
app.manage(notification);
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
173
src/mobile.rs
Normal file
173
src/mobile.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use tauri::{
|
||||
plugin::{PluginApi, PluginHandle},
|
||||
AppHandle, Runtime,
|
||||
};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
const PLUGIN_IDENTIFIER: &str = "app.tauri.notification";
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
tauri::ios_plugin_binding!(init_plugin_notification);
|
||||
|
||||
// initializes the Kotlin or Swift plugin classes
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
_app: &AppHandle<R>,
|
||||
api: PluginApi<R, C>,
|
||||
) -> crate::Result<Notification<R>> {
|
||||
#[cfg(target_os = "android")]
|
||||
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "NotificationPlugin")?;
|
||||
#[cfg(target_os = "ios")]
|
||||
let handle = api.register_ios_plugin(init_plugin_notification)?;
|
||||
Ok(Notification(handle))
|
||||
}
|
||||
|
||||
impl<R: Runtime> crate::NotificationBuilder<R> {
|
||||
pub fn show(self) -> crate::Result<()> {
|
||||
self.handle
|
||||
.run_mobile_plugin::<ShowResponse>("show", self.data)
|
||||
.map(|_| ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Access to the notification APIs.
|
||||
pub struct Notification<R: Runtime>(PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> Notification<R> {
|
||||
pub fn builder(&self) -> crate::NotificationBuilder<R> {
|
||||
crate::NotificationBuilder::new(self.0.clone())
|
||||
}
|
||||
|
||||
pub fn request_permission(&self) -> crate::Result<PermissionState> {
|
||||
self.0
|
||||
.run_mobile_plugin::<PermissionResponse>("requestPermissions", ())
|
||||
.map(|r| r.permission_state)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn permission_state(&self) -> crate::Result<PermissionState> {
|
||||
self.0
|
||||
.run_mobile_plugin::<PermissionResponse>("checkPermissions", ())
|
||||
.map(|r| r.permission_state)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn register_action_types(&self, types: Vec<ActionType>) -> crate::Result<()> {
|
||||
let mut args = HashMap::new();
|
||||
args.insert("types", types);
|
||||
self.0
|
||||
.run_mobile_plugin("registerActionTypes", args)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn remove_active(&self, notifications: Vec<i32>) -> crate::Result<()> {
|
||||
let mut args = HashMap::new();
|
||||
args.insert(
|
||||
"notifications",
|
||||
notifications
|
||||
.into_iter()
|
||||
.map(|id| {
|
||||
let mut notification = HashMap::new();
|
||||
notification.insert("id", id);
|
||||
notification
|
||||
})
|
||||
.collect::<Vec<HashMap<&str, i32>>>(),
|
||||
);
|
||||
self.0
|
||||
.run_mobile_plugin("removeActive", args)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn active(&self) -> crate::Result<Vec<ActiveNotification>> {
|
||||
self.0
|
||||
.run_mobile_plugin::<ActiveResponse>("getActive", ())
|
||||
.map(|r| r.notifications)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn remove_all_active(&self) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("removeActive", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn pending(&self) -> crate::Result<Vec<PendingNotification>> {
|
||||
self.0
|
||||
.run_mobile_plugin::<PendingResponse>("getPending", ())
|
||||
.map(|r| r.notifications)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Cancel pending notifications.
|
||||
pub fn cancel(&self, notifications: Vec<i32>) -> crate::Result<()> {
|
||||
let mut args = HashMap::new();
|
||||
args.insert("notifications", notifications);
|
||||
self.0.run_mobile_plugin("cancel", args).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Cancel all pending notifications.
|
||||
pub fn cancel_all(&self) -> crate::Result<()> {
|
||||
self.0.run_mobile_plugin("cancel", ()).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn create_channel(&self, channel: Channel) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("createChannel", channel)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn delete_channel(&self, id: impl Into<String>) -> crate::Result<()> {
|
||||
let mut args = HashMap::new();
|
||||
args.insert("id", id.into());
|
||||
self.0
|
||||
.run_mobile_plugin("deleteChannel", args)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn list_channels(&self) -> crate::Result<Vec<Channel>> {
|
||||
self.0
|
||||
.run_mobile_plugin::<ListChannelsResult>("listChannels", ())
|
||||
.map(|r| r.channels)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[derive(Deserialize)]
|
||||
struct ListChannelsResult {
|
||||
channels: Vec<Channel>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PendingResponse {
|
||||
notifications: Vec<PendingNotification>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ActiveResponse {
|
||||
notifications: Vec<ActiveNotification>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ShowResponse {
|
||||
#[allow(dead_code)]
|
||||
id: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PermissionResponse {
|
||||
permission_state: PermissionState,
|
||||
}
|
||||
509
src/models.rs
Normal file
509
src/models.rs
Normal file
@@ -0,0 +1,509 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{collections::HashMap, fmt::Display};
|
||||
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Attachment {
|
||||
id: String,
|
||||
url: Url,
|
||||
}
|
||||
|
||||
impl Attachment {
|
||||
pub fn new(id: impl Into<String>, url: Url) -> Self {
|
||||
Self { id: id.into(), url }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScheduleInterval {
|
||||
pub year: Option<u8>,
|
||||
pub month: Option<u8>,
|
||||
pub day: Option<u8>,
|
||||
pub weekday: Option<u8>,
|
||||
pub hour: Option<u8>,
|
||||
pub minute: Option<u8>,
|
||||
pub second: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScheduleEvery {
|
||||
Year,
|
||||
Month,
|
||||
TwoWeeks,
|
||||
Week,
|
||||
Day,
|
||||
Hour,
|
||||
Minute,
|
||||
Second,
|
||||
}
|
||||
|
||||
impl Display for ScheduleEvery {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Year => "Year",
|
||||
Self::Month => "Month",
|
||||
Self::TwoWeeks => "TwoWeeks",
|
||||
Self::Week => "Week",
|
||||
Self::Day => "Day",
|
||||
Self::Hour => "Hour",
|
||||
Self::Minute => "Minute",
|
||||
Self::Second => "Second",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ScheduleEvery {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ScheduleEvery {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.to_lowercase().as_str() {
|
||||
"year" => Ok(Self::Year),
|
||||
"month" => Ok(Self::Month),
|
||||
"twoweeks" => Ok(Self::TwoWeeks),
|
||||
"week" => Ok(Self::Week),
|
||||
"day" => Ok(Self::Day),
|
||||
"hour" => Ok(Self::Hour),
|
||||
"minute" => Ok(Self::Minute),
|
||||
"second" => Ok(Self::Second),
|
||||
_ => Err(DeError::custom(format!("unknown every kind '{s}'"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", content = "data")]
|
||||
pub enum Schedule {
|
||||
At {
|
||||
#[serde(
|
||||
serialize_with = "iso8601::serialize",
|
||||
deserialize_with = "time::serde::iso8601::deserialize"
|
||||
)]
|
||||
date: time::OffsetDateTime,
|
||||
#[serde(default)]
|
||||
repeating: bool,
|
||||
},
|
||||
Interval(ScheduleInterval),
|
||||
Every {
|
||||
interval: ScheduleEvery,
|
||||
},
|
||||
}
|
||||
|
||||
// custom ISO-8601 serialization that does not use 6 digits for years.
|
||||
mod iso8601 {
|
||||
use serde::{ser::Error as _, Serialize, Serializer};
|
||||
use time::{
|
||||
format_description::well_known::iso8601::{Config, EncodedConfig},
|
||||
format_description::well_known::Iso8601,
|
||||
OffsetDateTime,
|
||||
};
|
||||
|
||||
const SERDE_CONFIG: EncodedConfig = Config::DEFAULT.encode();
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
datetime: &OffsetDateTime,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
datetime
|
||||
.format(&Iso8601::<SERDE_CONFIG>)
|
||||
.map_err(S::Error::custom)?
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NotificationData {
|
||||
#[serde(default = "default_id")]
|
||||
pub(crate) id: i32,
|
||||
pub(crate) channel_id: Option<String>,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) body: Option<String>,
|
||||
pub(crate) schedule: Option<Schedule>,
|
||||
pub(crate) large_body: Option<String>,
|
||||
pub(crate) summary: Option<String>,
|
||||
pub(crate) action_type_id: Option<String>,
|
||||
pub(crate) group: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) group_summary: bool,
|
||||
pub(crate) sound: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) inbox_lines: Vec<String>,
|
||||
pub(crate) icon: Option<String>,
|
||||
pub(crate) large_icon: Option<String>,
|
||||
pub(crate) icon_color: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) attachments: Vec<Attachment>,
|
||||
#[serde(default)]
|
||||
pub(crate) extra: HashMap<String, serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub(crate) ongoing: bool,
|
||||
#[serde(default)]
|
||||
pub(crate) auto_cancel: bool,
|
||||
#[serde(default)]
|
||||
pub(crate) silent: bool,
|
||||
}
|
||||
|
||||
fn default_id() -> i32 {
|
||||
rand::random()
|
||||
}
|
||||
|
||||
impl Default for NotificationData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: default_id(),
|
||||
channel_id: None,
|
||||
title: None,
|
||||
body: None,
|
||||
schedule: None,
|
||||
large_body: None,
|
||||
summary: None,
|
||||
action_type_id: None,
|
||||
group: None,
|
||||
group_summary: false,
|
||||
sound: None,
|
||||
inbox_lines: Vec::new(),
|
||||
icon: None,
|
||||
large_icon: None,
|
||||
icon_color: None,
|
||||
attachments: Vec::new(),
|
||||
extra: Default::default(),
|
||||
ongoing: false,
|
||||
auto_cancel: false,
|
||||
silent: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Permission state.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PermissionState {
|
||||
/// Permission access has been granted.
|
||||
Granted,
|
||||
/// Permission access has been denied.
|
||||
Denied,
|
||||
/// Unknown state. Must request permission.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Display for PermissionState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Granted => write!(f, "granted"),
|
||||
Self::Denied => write!(f, "denied"),
|
||||
Self::Unknown => write!(f, "Unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PermissionState {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PermissionState {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.to_lowercase().as_str() {
|
||||
"granted" => Ok(Self::Granted),
|
||||
"denied" => Ok(Self::Denied),
|
||||
"default" => Ok(Self::Unknown),
|
||||
_ => Err(DeError::custom(format!("unknown permission state '{s}'"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PendingNotification {
|
||||
id: i32,
|
||||
title: Option<String>,
|
||||
body: Option<String>,
|
||||
schedule: Schedule,
|
||||
}
|
||||
|
||||
impl PendingNotification {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn title(&self) -> Option<&str> {
|
||||
self.title.as_deref()
|
||||
}
|
||||
|
||||
pub fn body(&self) -> Option<&str> {
|
||||
self.body.as_deref()
|
||||
}
|
||||
|
||||
pub fn schedule(&self) -> &Schedule {
|
||||
&self.schedule
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ActiveNotification {
|
||||
id: i32,
|
||||
tag: Option<String>,
|
||||
title: Option<String>,
|
||||
body: Option<String>,
|
||||
group: Option<String>,
|
||||
#[serde(default)]
|
||||
group_summary: bool,
|
||||
#[serde(default)]
|
||||
data: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
extra: HashMap<String, serde_json::Value>,
|
||||
#[serde(default)]
|
||||
attachments: Vec<Attachment>,
|
||||
action_type_id: Option<String>,
|
||||
schedule: Option<Schedule>,
|
||||
sound: Option<String>,
|
||||
}
|
||||
|
||||
impl ActiveNotification {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn tag(&self) -> Option<&str> {
|
||||
self.tag.as_deref()
|
||||
}
|
||||
|
||||
pub fn title(&self) -> Option<&str> {
|
||||
self.title.as_deref()
|
||||
}
|
||||
|
||||
pub fn body(&self) -> Option<&str> {
|
||||
self.body.as_deref()
|
||||
}
|
||||
|
||||
pub fn group(&self) -> Option<&str> {
|
||||
self.group.as_deref()
|
||||
}
|
||||
|
||||
pub fn group_summary(&self) -> bool {
|
||||
self.group_summary
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &HashMap<String, String> {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn extra(&self) -> &HashMap<String, serde_json::Value> {
|
||||
&self.extra
|
||||
}
|
||||
|
||||
pub fn attachments(&self) -> &[Attachment] {
|
||||
&self.attachments
|
||||
}
|
||||
|
||||
pub fn action_type_id(&self) -> Option<&str> {
|
||||
self.action_type_id.as_deref()
|
||||
}
|
||||
|
||||
pub fn schedule(&self) -> Option<&Schedule> {
|
||||
self.schedule.as_ref()
|
||||
}
|
||||
|
||||
pub fn sound(&self) -> Option<&str> {
|
||||
self.sound.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ActionType {
|
||||
id: String,
|
||||
actions: Vec<Action>,
|
||||
hidden_previews_body_placeholder: Option<String>,
|
||||
custom_dismiss_action: bool,
|
||||
allow_in_car_play: bool,
|
||||
hidden_previews_show_title: bool,
|
||||
hidden_previews_show_subtitle: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Action {
|
||||
id: String,
|
||||
title: String,
|
||||
requires_authentication: bool,
|
||||
foreground: bool,
|
||||
destructive: bool,
|
||||
input: bool,
|
||||
input_button_title: Option<String>,
|
||||
input_placeholder: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub use android::*;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum Importance {
|
||||
None = 0,
|
||||
Min = 1,
|
||||
Low = 2,
|
||||
Default = 3,
|
||||
High = 4,
|
||||
}
|
||||
|
||||
impl Default for Importance {
|
||||
fn default() -> Self {
|
||||
Self::Default
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(i8)]
|
||||
pub enum Visibility {
|
||||
Secret = -1,
|
||||
Private = 0,
|
||||
Public = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Channel {
|
||||
id: String,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
sound: Option<String>,
|
||||
lights: bool,
|
||||
light_color: Option<String>,
|
||||
vibration: bool,
|
||||
importance: Importance,
|
||||
visibility: Option<Visibility>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChannelBuilder(Channel);
|
||||
|
||||
impl Channel {
|
||||
pub fn builder(id: impl Into<String>, name: impl Into<String>) -> ChannelBuilder {
|
||||
ChannelBuilder(Self {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
description: None,
|
||||
sound: None,
|
||||
lights: false,
|
||||
light_color: None,
|
||||
vibration: false,
|
||||
importance: Default::default(),
|
||||
visibility: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
self.description.as_deref()
|
||||
}
|
||||
|
||||
pub fn sound(&self) -> Option<&str> {
|
||||
self.sound.as_deref()
|
||||
}
|
||||
|
||||
pub fn lights(&self) -> bool {
|
||||
self.lights
|
||||
}
|
||||
|
||||
pub fn light_color(&self) -> Option<&str> {
|
||||
self.light_color.as_deref()
|
||||
}
|
||||
|
||||
pub fn vibration(&self) -> bool {
|
||||
self.vibration
|
||||
}
|
||||
|
||||
pub fn importance(&self) -> Importance {
|
||||
self.importance
|
||||
}
|
||||
|
||||
pub fn visibility(&self) -> Option<Visibility> {
|
||||
self.visibility
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelBuilder {
|
||||
pub fn description(mut self, description: impl Into<String>) -> Self {
|
||||
self.0.description.replace(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn sound(mut self, sound: impl Into<String>) -> Self {
|
||||
self.0.sound.replace(sound.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn lights(mut self, lights: bool) -> Self {
|
||||
self.0.lights = lights;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn light_color(mut self, color: impl Into<String>) -> Self {
|
||||
self.0.light_color.replace(color.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn vibration(mut self, vibration: bool) -> Self {
|
||||
self.0.vibration = vibration;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn importance(mut self, importance: Importance) -> Self {
|
||||
self.0.importance = importance;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn visibility(mut self, visibility: Visibility) -> Self {
|
||||
self.0.visibility.replace(visibility);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Channel {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
22
test/tauri.conf.json
Normal file
22
test/tauri.conf.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "../../../node_modules/.pnpm/@tauri-apps+cli@2.0.0-alpha.8/node_modules/@tauri-apps/cli/schema.json",
|
||||
"build": {
|
||||
"distDir": ".",
|
||||
"devPath": "http://localhost:4000"
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
"identifier": "studio.tauri.example",
|
||||
"active": true,
|
||||
"icon": ["../../../examples/api/src-tauri/icons/icon.png"]
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Tauri App"
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["guest-js/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user