feat: update to alpha.17, typed mobile plugin IPC arguments (#676)

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>
pull/688/head
Lucas Fernandes Nogueira 2 years ago committed by GitHub
parent 76cfdc32b4
commit e438e0a62d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,30 @@
---
"authenticator": patch
"autostart": patch
"barcode-scanner": patch
"cli": patch
"clipboard-manager": patch
"deep-link": patch
"dialog": patch
"fs": patch
"global-shortcut": patch
"http": patch
"localhost": patch
"log-plugin": patch
"notification": patch
"os": patch
"persisted-scope": patch
"positioner": patch
"process": patch
"shell": patch
"single-instance": patch
"sql": patch
"store": patch
"stronghold": patch
"updater": patch
"upload": patch
"websocket": patch
"window-state": patch
---
Update to tauri@alpha.17.

@ -0,0 +1,27 @@
---
"authenticator-js": patch
"autostart-js": patch
"barcode-scanner-js": patch
"cli-js": patch
"clipboard-manager-js": patch
"deep-link-js": patch
"dialog-js": patch
"fs-js": patch
"global-shortcut-js": patch
"http-js": patch
"log-js": patch
"notification-js": patch
"os-js": patch
"positioner-js": patch
"process-js": patch
"shell-js": patch
"sql-js": patch
"store-js": patch
"stronghold-js": patch
"updater-js": patch
"upload-js": patch
"websocket-js": patch
"window-state-js": patch
---
Update to @tauri-apps/api v2.0.0-alpha.11.

@ -24,4 +24,4 @@
"window-state-js": patch "window-state-js": patch
--- ---
Update to @tauri-apps/api v2.0.0-alpha.16. Update to @tauri-apps/api v2.0.0-alpha.9.

@ -0,0 +1,7 @@
---
"log-plugin": patch
"deep-link": patch
"store": patch
---
Update to breaking changes from tauri@alpha.17.

547
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -5,8 +5,8 @@ resolver = "2"
[workspace.dependencies] [workspace.dependencies]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
log = "0.4" log = "0.4"
tauri = "2.0.0-alpha.16" tauri = "2.0.0-alpha.17"
tauri-build = "2.0.0-alpha.10" tauri-build = "2.0.0-alpha.11"
serde_json = "1" serde_json = "1"
thiserror = "1" thiserror = "1"

@ -9,7 +9,7 @@
"serve": "vite preview" "serve": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9", "@tauri-apps/api": "2.0.0-alpha.11",
"@tauri-apps/plugin-barcode-scanner": "2.0.0-alpha.1", "@tauri-apps/plugin-barcode-scanner": "2.0.0-alpha.1",
"@tauri-apps/plugin-cli": "2.0.0-alpha.2", "@tauri-apps/plugin-cli": "2.0.0-alpha.2",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.2", "@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.2",
@ -28,7 +28,7 @@
"@iconify-json/codicon": "^1.1.31", "@iconify-json/codicon": "^1.1.31",
"@iconify-json/ph": "^1.1.6", "@iconify-json/ph": "^1.1.6",
"@sveltejs/vite-plugin-svelte": "^2.4.6", "@sveltejs/vite-plugin-svelte": "^2.4.6",
"@tauri-apps/cli": "2.0.0-alpha.16", "@tauri-apps/cli": "2.0.0-alpha.17",
"@unocss/extractor-svelte": "^0.56.5", "@unocss/extractor-svelte": "^0.56.5",
"internal-ip": "^8.0.0", "internal-ip": "^8.0.0",
"svelte": "^4.2.2", "svelte": "^4.2.2",

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSCameraUsageDescription</key>
<string>Request camera access for barcode scanner</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>

@ -12,7 +12,7 @@ mod cmd;
mod tray; mod tray;
use serde::Serialize; use serde::Serialize;
use tauri::{window::WindowBuilder, App, AppHandle, RunEvent, WindowUrl}; use tauri::{window::WindowBuilder, App, AppHandle, Manager, RunEvent, WindowUrl};
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
struct Reply { struct Reply {

@ -72,7 +72,7 @@
component: Http, component: Http,
icon: "i-ph-globe-hemisphere-west", icon: "i-ph-globe-hemisphere-west",
}, },
!isMobile && { {
label: "Notifications", label: "Notifications",
component: Notifications, component: Notifications,
icon: "i-codicon-bell-dot", icon: "i-codicon-bell-dot",
@ -92,7 +92,7 @@
component: Updater, component: Updater,
icon: "i-codicon-cloud-download", icon: "i-codicon-cloud-download",
}, },
!isMobile && { {
label: "Clipboard", label: "Clipboard",
component: Clipboard, component: Clipboard,
icon: "i-codicon-clippy", icon: "i-codicon-clippy",

@ -29,6 +29,6 @@
} }
</script> </script>
<button class="btn" id="notification" on:click={_sendNotification}> <button class="btn" id="notification" on:click={sendNotification}>
Send test notification Send test notification
</button> </button>

@ -2,7 +2,7 @@
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]

@ -8,7 +8,8 @@ edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "tauri/dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }

@ -28,6 +28,6 @@
"tslib": "2.6.0" "tslib": "2.6.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_AUTHENTICATOR__=function(e){"use strict";var t=Object.defineProperty,n=(e,t,n)=>{if(!t.has(e))throw TypeError("Cannot "+n)},i=(e,t,i)=>(n(e,t,"read from private field"),i?i.call(e):t.get(e));function a(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}((e,n)=>{for(var i in n)t(e,i,{get:n[i],enumerable:!0})})({},{Channel:()=>s,PluginListener:()=>c,addPluginListener:()=>l,convertFileSrc:()=>u,invoke:()=>o,transformCallback:()=>a});var r,s=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,t,n)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,n)})(this,r,(()=>{})),this.id=a((e=>{i(this,r).call(this,e)}))}set onmessage(e){var t,i,a,s;a=e,n(t=this,i=r,"write to private field"),s?s.call(t,a):i.set(t,a)}get onmessage(){return i(this,r)}toJSON(){return`__CHANNEL__:${this.id}`}};r=new WeakMap;var c=class{constructor(e,t,n){this.plugin=e,this.event=t,this.channelId=n}async unregister(){return o(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function l(e,t,n){let i=new s;return i.onmessage=n,o(`plugin:${e}|register_listener`,{event:t,handler:i}).then((()=>new c(e,t,i.id)))}async function o(e,t={},n){return window.__TAURI_INTERNALS__.invoke(e,t,n)}function u(e,t="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,t)}return e.Authenticator=class{async init(){return await o("plugin:authenticator|init_auth")}async register(e,t){return await o("plugin:authenticator|register",{timeout:1e4,challenge:e,application:t})}async verifyRegistration(e,t,n,i){return await o("plugin:authenticator|verify_registration",{challenge:e,application:t,registerData:n,clientData:i})}async sign(e,t,n){return await o("plugin:authenticator|sign",{timeout:1e4,challenge:e,application:t,keyHandle:n})}async verifySignature(e,t,n,i,a,r){return await o("plugin:authenticator|verify_signature",{challenge:e,application:t,signData:n,clientData:i,keyHandle:a,pubkey:r})}},e}({});Object.defineProperty(window.__TAURI__,"authenticator",{value:__TAURI_AUTHENTICATOR__})} if("__TAURI__"in window){var __TAURI_AUTHENTICATOR__=function(t){"use strict";async function i(t,i={},a){return window.__TAURI_INTERNALS__.invoke(t,i,a)}"function"==typeof SuppressedError&&SuppressedError;return t.Authenticator=class{async init(){return await i("plugin:authenticator|init_auth")}async register(t,a){return await i("plugin:authenticator|register",{timeout:1e4,challenge:t,application:a})}async verifyRegistration(t,a,e,n){return await i("plugin:authenticator|verify_registration",{challenge:t,application:a,registerData:e,clientData:n})}async sign(t,a,e){return await i("plugin:authenticator|sign",{timeout:1e4,challenge:t,application:a,keyHandle:e})}async verifySignature(t,a,e,n,r,u){return await i("plugin:authenticator|verify_signature",{challenge:t,application:a,signData:e,clientData:n,keyHandle:r,pubkey:u})}},t}({});Object.defineProperty(window.__TAURI__,"authenticator",{value:__TAURI_AUTHENTICATOR__})}

@ -2,7 +2,7 @@
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]

@ -8,7 +8,8 @@ edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "tauri/dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }

@ -27,6 +27,6 @@
"tslib": "2.6.0" "tslib": "2.6.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_AUTOSTART__=function(e){"use strict";var n=Object.defineProperty,t=(e,n,t)=>{if(!n.has(e))throw TypeError("Cannot "+t)},r=(e,n,r)=>(t(e,n,"read from private field"),r?r.call(e):n.get(e));function a(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}((e,t)=>{for(var r in t)n(e,r,{get:t[r],enumerable:!0})})({},{Channel:()=>s,PluginListener:()=>o,addPluginListener:()=>_,convertFileSrc:()=>c,invoke:()=>l,transformCallback:()=>a});var i,s=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,n,t)=>{if(n.has(e))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(e):n.set(e,t)})(this,i,(()=>{})),this.id=a((e=>{r(this,i).call(this,e)}))}set onmessage(e){var n,r,a,s;a=e,t(n=this,r=i,"write to private field"),s?s.call(n,a):r.set(n,a)}get onmessage(){return r(this,i)}toJSON(){return`__CHANNEL__:${this.id}`}};i=new WeakMap;var o=class{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return l(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function _(e,n,t){let r=new s;return r.onmessage=t,l(`plugin:${e}|register_listener`,{event:n,handler:r}).then((()=>new o(e,n,r.id)))}async function l(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}function c(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)}return e.disable=async function(){await l("plugin:autostart|disable")},e.enable=async function(){await l("plugin:autostart|enable")},e.isEnabled=async function(){return await l("plugin:autostart|is_enabled")},e}({});Object.defineProperty(window.__TAURI__,"autostart",{value:__TAURI_AUTOSTART__})} if("__TAURI__"in window){var __TAURI_AUTOSTART__=function(n){"use strict";async function t(n,t={},a){return window.__TAURI_INTERNALS__.invoke(n,t,a)}return"function"==typeof SuppressedError&&SuppressedError,n.disable=async function(){await t("plugin:autostart|disable")},n.enable=async function(){await t("plugin:autostart|enable")},n.isEnabled=async function(){return await t("plugin:autostart|is_enabled")},n}({});Object.defineProperty(window.__TAURI__,"autostart",{value:__TAURI_AUTOSTART__})}

@ -2,7 +2,7 @@
## \[2.0.0-alpha.1] ## \[2.0.0-alpha.1]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.0] ## \[2.0.0-alpha.0]

@ -9,7 +9,8 @@ rust-version = { workspace = true }
links = "tauri-plugin-barcode-scanner" links = "tauri-plugin-barcode-scanner"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
targets = [ "x86_64-linux-android" ] targets = [ "x86_64-linux-android" ]
[build-dependencies] [build-dependencies]
@ -21,6 +22,3 @@ serde_json = { workspace = true }
tauri = { workspace = true } tauri = { workspace = true }
log = { workspace = true } log = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
[features]
dox = [ "tauri/dox" ]

@ -37,6 +37,7 @@ import app.tauri.Logger
import app.tauri.PermissionState import app.tauri.PermissionState
import app.tauri.annotation.ActivityCallback import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.Command import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Permission import app.tauri.annotation.Permission
import app.tauri.annotation.PermissionCallback import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.TauriPlugin import app.tauri.annotation.TauriPlugin
@ -57,6 +58,13 @@ private const val PERMISSION_ALIAS_CAMERA = "camera"
private const val PERMISSION_NAME = Manifest.permission.CAMERA private const val PERMISSION_NAME = Manifest.permission.CAMERA
private const val PREFS_PERMISSION_FIRST_TIME_ASKING = "PREFS_PERMISSION_FIRST_TIME_ASKING" private const val PREFS_PERMISSION_FIRST_TIME_ASKING = "PREFS_PERMISSION_FIRST_TIME_ASKING"
@InvokeArg
class ScanOptions {
var formats: Array<String>? = null
var windowed: Boolean = false
var cameraDirection: String? = null
}
@TauriPlugin( @TauriPlugin(
permissions = [ permissions = [
Permission(strings = [Manifest.permission.CAMERA], alias = "camera") Permission(strings = [Manifest.permission.CAMERA], alias = "camera")
@ -206,19 +214,12 @@ class BarcodeScannerPlugin(private val activity: Activity) : Plugin(activity),
} }
} }
private fun getFormats(invoke: Invoke): List<Int> { private fun getFormats(args: ScanOptions): List<Int> {
val jsFormats = invoke.getArray("formats", JSArray())
val formats = ArrayList<Int>() val formats = ArrayList<Int>()
for (i in 0 until jsFormats.length()) { for (format in args.formats ?: arrayOf()) {
try { val targetedBarcodeFormat = supportedFormats[format]
val targetedFormat: String = jsFormats.getString(i) if (targetedBarcodeFormat != null) {
val targetedBarcodeFormat = formats.add(targetedBarcodeFormat)
supportedFormats[targetedFormat]
if (targetedBarcodeFormat != null) {
formats.add(targetedBarcodeFormat)
}
} catch (e: JSONException) {
e.printStackTrace()
} }
} }
return formats return formats
@ -341,14 +342,16 @@ class BarcodeScannerPlugin(private val activity: Activity) : Plugin(activity),
@Command @Command
fun scan(invoke: Invoke) { fun scan(invoke: Invoke) {
val args = invoke.parseArgs(ScanOptions::class.java)
savedInvoke = invoke savedInvoke = invoke
if (hasCamera()) { if (hasCamera()) {
if (getPermissionState("camera") != PermissionState.GRANTED) { if (getPermissionState("camera") != PermissionState.GRANTED) {
throw Exception("No permission to use camera. Did you request it yet?") throw Exception("No permission to use camera. Did you request it yet?")
} else { } else {
webViewBackground = null webViewBackground = null
prepare(invoke.getString("cameraDirection", "back"), invoke.getBoolean("windowed", false)) prepare(args.cameraDirection ?: "back", args.windowed)
configureCamera(getFormats(invoke)) configureCamera(getFormats(args))
} }
} }
} }

@ -10,7 +10,7 @@ fn main() {
{ {
println!("{error:#}"); println!("{error:#}");
// when building documentation for Android the plugin build result is irrelevant to the crate itself // when building documentation for Android the plugin build result is irrelevant to the crate itself
if !(cfg!(feature = "dox") && std::env::var("TARGET").unwrap().contains("android")) { if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) {
std::process::exit(1); std::process::exit(1);
} }
} }

@ -7,7 +7,13 @@ import Tauri
import UIKit import UIKit
import WebKit import WebKit
enum SupportedFormat: String, CaseIterable { struct ScanOptions: Decodable {
var formats: [SupportedFormat] = []
let windowed: Bool?
let cameraDirection: String?
}
enum SupportedFormat: String, CaseIterable, Decodable {
// UPC_A not supported // UPC_A not supported
case UPC_E case UPC_E
case EAN_8 case EAN_8
@ -232,19 +238,11 @@ class BarcodeScannerPlugin: Plugin, AVCaptureMetadataOutputObjectsDelegate {
} }
} }
private func runScanner(_ invoke: Invoke) { private func runScanner(_ invoke: Invoke, args: ScanOptions) {
scanFormats = [AVMetadataObject.ObjectType]() scanFormats = [AVMetadataObject.ObjectType]()
if (invoke.data["formats"]) != nil { args.formats.forEach { format in
let _scanFormats = invoke.getArray("formats", String.self) scanFormats.append(format.value)
if _scanFormats != nil && _scanFormats?.count ?? 0 > 0 {
_scanFormats?.forEach { targetedFormat in
if let value = SupportedFormat(rawValue: targetedFormat)?.value {
scanFormats.append(value)
}
}
}
} }
if scanFormats.count == 0 { if scanFormats.count == 0 {
@ -259,7 +257,9 @@ class BarcodeScannerPlugin: Plugin, AVCaptureMetadataOutputObjectsDelegate {
self.isScanning = true self.isScanning = true
} }
@objc private func scan(_ invoke: Invoke) { @objc private func scan(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(ScanOptions.self)
self.invoke = invoke self.invoke = invoke
var iOS14min: Bool = false var iOS14min: Bool = false
@ -279,10 +279,10 @@ class BarcodeScannerPlugin: Plugin, AVCaptureMetadataOutputObjectsDelegate {
self.loadCamera() self.loadCamera()
self.dismantleCamera() self.dismantleCamera()
self.setupCamera( self.setupCamera(
direction: invoke.getString("cameraDirection") ?? "back", direction: args.cameraDirection ?? "back",
windowed: invoke.getBool("windowed") ?? false windowed: args.windowed ?? false
) )
self.runScanner(invoke) self.runScanner(invoke, args: args)
} }
} }

@ -28,6 +28,6 @@
"tslib": "^2.5.0" "tslib": "^2.5.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_BARCODESCANNER__=function(e){"use strict";var n=Object.defineProperty,t=(e,n,t)=>{if(!n.has(e))throw TypeError("Cannot "+t)},r=(e,n,r)=>(t(e,n,"read from private field"),r?r.call(e):n.get(e));function a(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}((e,t)=>{for(var r in t)n(e,r,{get:t[r],enumerable:!0})})({},{Channel:()=>s,PluginListener:()=>o,addPluginListener:()=>_,convertFileSrc:()=>l,invoke:()=>u,transformCallback:()=>a});var i,s=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,n,t)=>{if(n.has(e))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(e):n.set(e,t)})(this,i,(()=>{})),this.id=a((e=>{r(this,i).call(this,e)}))}set onmessage(e){var n,r,a,s;a=e,t(n=this,r=i,"write to private field"),s?s.call(n,a):r.set(n,a)}get onmessage(){return r(this,i)}toJSON(){return`__CHANNEL__:${this.id}`}};i=new WeakMap;var c,o=class{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return u(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function _(e,n,t){let r=new s;return r.onmessage=t,u(`plugin:${e}|register_listener`,{event:n,handler:r}).then((()=>new o(e,n,r.id)))}async function u(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}function l(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)}return e.Format=void 0,(c=e.Format||(e.Format={})).QRCode="QR_CODE",c.UPC_A="UPC_A",c.UPC_E="UPC_E",c.EAN8="EAN_8",c.EAN13="EAN_13",c.Code39="CODE_39",c.Code93="CODE_93",c.Code128="CODE_128",c.Codabar="CODABAR",c.ITF="ITF",c.Aztec="AZTEC",c.DataMatrix="DATA_MATRIX",c.PDF417="PDF_417",e.cancel=async function(){return await u("plugin:barcodeScanner|cancel")},e.checkPermissions=async function(){return await u("plugin:barcodeScanner|checkPermissions").then((e=>e.camera))},e.openAppSettings=async function(){return await u("plugin:barcodeScanner|openAppSettings")},e.requestPermissions=async function(){return await u("plugin:barcodeScanner|requestPermissions").then((e=>e.camera))},e.scan=async function(e){return await u("plugin:barcodeScanner|scan",{...e})},e}({});Object.defineProperty(window.__TAURI__,"barcodeScanner",{value:__TAURI_BARCODESCANNER__})} if("__TAURI__"in window){var __TAURI_BARCODESCANNER__=function(n){"use strict";async function e(n,e={},r){return window.__TAURI_INTERNALS__.invoke(n,e,r)}var r;return"function"==typeof SuppressedError&&SuppressedError,n.Format=void 0,(r=n.Format||(n.Format={})).QRCode="QR_CODE",r.UPC_A="UPC_A",r.UPC_E="UPC_E",r.EAN8="EAN_8",r.EAN13="EAN_13",r.Code39="CODE_39",r.Code93="CODE_93",r.Code128="CODE_128",r.Codabar="CODABAR",r.ITF="ITF",r.Aztec="AZTEC",r.DataMatrix="DATA_MATRIX",r.PDF417="PDF_417",n.cancel=async function(){return await e("plugin:barcodeScanner|cancel")},n.checkPermissions=async function(){return await e("plugin:barcodeScanner|checkPermissions").then((n=>n.camera))},n.openAppSettings=async function(){return await e("plugin:barcodeScanner|openAppSettings")},n.requestPermissions=async function(){return await e("plugin:barcodeScanner|requestPermissions").then((n=>n.camera))},n.scan=async function(n){return await e("plugin:barcodeScanner|scan",{...n})},n}({});Object.defineProperty(window.__TAURI__,"barcodeScanner",{value:__TAURI_BARCODESCANNER__})}

@ -2,7 +2,7 @@
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]

@ -8,7 +8,8 @@ license = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "tauri/dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }

@ -27,6 +27,6 @@
"tslib": "^2.4.1" "tslib": "^2.4.1"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_CLI__=function(e){"use strict";var n=Object.defineProperty,t=(e,n,t)=>{if(!n.has(e))throw TypeError("Cannot "+t)},r=(e,n,r)=>(t(e,n,"read from private field"),r?r.call(e):n.get(e));function i(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}((e,t)=>{for(var r in t)n(e,r,{get:t[r],enumerable:!0})})({},{Channel:()=>s,PluginListener:()=>_,addPluginListener:()=>o,convertFileSrc:()=>l,invoke:()=>c,transformCallback:()=>i});var a,s=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,n,t)=>{if(n.has(e))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(e):n.set(e,t)})(this,a,(()=>{})),this.id=i((e=>{r(this,a).call(this,e)}))}set onmessage(e){var n,r,i,s;i=e,t(n=this,r=a,"write to private field"),s?s.call(n,i):r.set(n,i)}get onmessage(){return r(this,a)}toJSON(){return`__CHANNEL__:${this.id}`}};a=new WeakMap;var _=class{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return c(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function o(e,n,t){let r=new s;return r.onmessage=t,c(`plugin:${e}|register_listener`,{event:n,handler:r}).then((()=>new _(e,n,r.id)))}async function c(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}function l(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)}return e.getMatches=async function(){return await c("plugin:cli|cli_matches")},e}({});Object.defineProperty(window.__TAURI__,"cli",{value:__TAURI_CLI__})} if("__TAURI__"in window){var __TAURI_CLI__=function(_){"use strict";return"function"==typeof SuppressedError&&SuppressedError,_.getMatches=async function(){return await async function(_,n={},e){return window.__TAURI_INTERNALS__.invoke(_,n,e)}("plugin:cli|cli_matches")},_}({});Object.defineProperty(window.__TAURI__,"cli",{value:__TAURI_CLI__})}

@ -2,7 +2,7 @@
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]

@ -9,7 +9,8 @@ rust-version = { workspace = true }
links = "tauri-plugin-clipboard-manager" links = "tauri-plugin-clipboard-manager"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
targets = [ "x86_64-unknown-linux-gnu", "x86_64-linux-android" ] targets = [ "x86_64-unknown-linux-gnu", "x86_64-linux-android" ]
[build-dependencies] [build-dependencies]
@ -24,6 +25,3 @@ thiserror = { workspace = true }
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
arboard = "3" arboard = "3"
[features]
dox = [ "tauri/dox" ]

@ -38,6 +38,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0") implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0") implementation("com.google.android.material:material:1.7.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

@ -11,11 +11,74 @@ import android.content.ClipDescription
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import app.tauri.annotation.Command import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Invoke import app.tauri.plugin.Invoke
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin import app.tauri.plugin.Plugin
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import java.io.IOException
@InvokeArg
@JsonDeserialize(using = WriteOptionsDeserializer::class)
sealed class WriteOptions {
@JsonDeserialize
class PlainText: WriteOptions() {
lateinit var text: String
var label: String? = null
}
}
@JsonSerialize(using = ReadClipDataSerializer::class)
sealed class ReadClipData {
class PlainText: ReadClipData() {
lateinit var text: String
}
}
internal class ReadClipDataSerializer @JvmOverloads constructor(t: Class<ReadClipData>? = null) :
StdSerializer<ReadClipData>(t) {
@Throws(IOException::class, JsonProcessingException::class)
override fun serialize(
value: ReadClipData, jgen: JsonGenerator, provider: SerializerProvider
) {
jgen.writeStartObject()
when (value) {
is ReadClipData.PlainText -> {
jgen.writeObjectFieldStart("plainText")
jgen.writeStringField("text", value.text)
jgen.writeEndObject()
}
else -> {}
}
jgen.writeEndObject()
}
}
internal class WriteOptionsDeserializer: JsonDeserializer<WriteOptions>() {
override fun deserialize(
jsonParser: JsonParser,
deserializationContext: DeserializationContext
): WriteOptions {
val node: JsonNode = jsonParser.codec.readTree(jsonParser)
node.get("plainText")?.let {
return jsonParser.codec.treeToValue(it, WriteOptions.PlainText::class.java)
} ?: run {
throw Error("unknown write options $node")
}
}
}
@TauriPlugin @TauriPlugin
class ClipboardPlugin(private val activity: Activity) : Plugin(activity) { class ClipboardPlugin(private val activity: Activity) : Plugin(activity) {
@ -25,22 +88,14 @@ class ClipboardPlugin(private val activity: Activity) : Plugin(activity) {
@Command @Command
@Suppress("MoveVariableDeclarationIntoWhen") @Suppress("MoveVariableDeclarationIntoWhen")
fun write(invoke: Invoke) { fun write(invoke: Invoke) {
val options = invoke.getObject("options") val args = invoke.parseArgs(WriteOptions::class.java)
if (options == null) {
invoke.reject("Missing `options` input")
return
}
val kind = invoke.getString("kind", "")
val clipData = when (kind) { val clipData = when (args) {
"PlainText" -> { is WriteOptions.PlainText -> {
val label = options.getString("label", "") ClipData.newPlainText(args.label, args.text)
val text = options.getString("text", "")
ClipData.newPlainText(label, text)
} }
else -> { else -> {
invoke.reject("Unknown kind $kind") invoke.reject("unimplemented clip data")
return return
} }
} }
@ -52,10 +107,12 @@ class ClipboardPlugin(private val activity: Activity) : Plugin(activity) {
@Command @Command
fun read(invoke: Invoke) { fun read(invoke: Invoke) {
val (kind, options) = if (manager.hasPrimaryClip()) { val data = if (manager.hasPrimaryClip()) {
if (manager.primaryClipDescription?.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) == true) { if (manager.primaryClipDescription?.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) == true) {
val item: ClipData.Item = manager.primaryClip!!.getItemAt(0) val item: ClipData.Item = manager.primaryClip!!.getItemAt(0)
Pair("PlainText", item.text) val data = ReadClipData.PlainText()
data.text = item.text.toString()
data
} else { } else {
// TODO // TODO
invoke.reject("Clipboard content reader not implemented") invoke.reject("Clipboard content reader not implemented")
@ -66,9 +123,6 @@ class ClipboardPlugin(private val activity: Activity) : Plugin(activity) {
return return
} }
val response = JSObject() invoke.resolveObject(data)
response.put("kind", kind)
response.put("options", options)
invoke.resolve(response)
} }
} }

@ -10,7 +10,7 @@ fn main() {
{ {
println!("{error:#}"); println!("{error:#}");
// when building documentation for Android the plugin build result is irrelevant to the crate itself // when building documentation for Android the plugin build result is irrelevant to the crate itself
if !(cfg!(feature = "dox") && std::env::var("TARGET").unwrap().contains("android")) { if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) {
std::process::exit(1); std::process::exit(1);
} }
} }

@ -10,12 +10,7 @@
import { invoke } from "@tauri-apps/api/primitives"; import { invoke } from "@tauri-apps/api/primitives";
interface Clip<K, T> { type ClipResponse = Record<"plainText", { text: string }>;
kind: K;
options: T;
}
type ClipResponse = Clip<"PlainText", string>;
/** /**
* Writes plain text to the clipboard. * Writes plain text to the clipboard.
@ -36,8 +31,7 @@ async function writeText(
): Promise<void> { ): Promise<void> {
return invoke("plugin:clipboard|write", { return invoke("plugin:clipboard|write", {
data: { data: {
kind: "PlainText", plainText: {
options: {
label: opts?.label, label: opts?.label,
text, text,
}, },
@ -56,7 +50,7 @@ async function writeText(
*/ */
async function readText(): Promise<string> { async function readText(): Promise<string> {
const kind: ClipResponse = await invoke("plugin:clipboard|read"); const kind: ClipResponse = await invoke("plugin:clipboard|read");
return kind.options; return kind.plainText.text;
} }
export { writeText, readText }; export { writeText, readText };

@ -2,42 +2,42 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import SwiftRs
import Tauri
import UIKit import UIKit
import WebKit import WebKit
import Tauri
import SwiftRs enum WriteOptions: Codable {
case plainText(text: String)
}
enum ReadClipData: Codable {
case plainText(text: String)
}
class ClipboardPlugin: Plugin { class ClipboardPlugin: Plugin {
@objc public func write(_ invoke: Invoke) throws { @objc public func write(_ invoke: Invoke) throws {
let options = invoke.getObject("options") let options = try invoke.parseArgs(WriteOptions.self)
if let options = options { let clipboard = UIPasteboard.general
let clipboard = UIPasteboard.general switch options {
let kind = invoke.getString("kind", "") case .plainText(let text):
switch kind { clipboard.string = text
case "PlainText": default:
let text = options["text"] as? String invoke.unimplemented()
clipboard.string = text return
default: }
invoke.reject("Unknown kind \(kind)") invoke.resolve()
return
} }
invoke.resolve()
} else {
invoke.reject("Missing `options` input")
}
}
@objc public func read(_ invoke: Invoke) throws { @objc public func read(_ invoke: Invoke) throws {
let clipboard = UIPasteboard.general let clipboard = UIPasteboard.general
if let text = clipboard.string { if let text = clipboard.string {
invoke.resolve([ invoke.resolve(ReadClipData.plainText(text: text))
"kind": "PlainText", } else {
"options": text invoke.reject("Clipboard is empty")
]) }
} else { }
invoke.reject("Clipboard is empty")
}
}
} }
@_cdecl("init_plugin_clipboard") @_cdecl("init_plugin_clipboard")

@ -27,6 +27,6 @@
"tslib": "^2.4.1" "tslib": "^2.4.1"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_CLIPBOARDMANAGER__=function(e){"use strict";var n=Object.defineProperty,t=(e,n,t)=>{if(!n.has(e))throw TypeError("Cannot "+t)},r=(e,n,r)=>(t(e,n,"read from private field"),r?r.call(e):n.get(e));function i(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}((e,t)=>{for(var r in t)n(e,r,{get:t[r],enumerable:!0})})({},{Channel:()=>s,PluginListener:()=>o,addPluginListener:()=>_,convertFileSrc:()=>c,invoke:()=>l,transformCallback:()=>i});var a,s=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,n,t)=>{if(n.has(e))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(e):n.set(e,t)})(this,a,(()=>{})),this.id=i((e=>{r(this,a).call(this,e)}))}set onmessage(e){var n,r,i,s;i=e,t(n=this,r=a,"write to private field"),s?s.call(n,i):r.set(n,i)}get onmessage(){return r(this,a)}toJSON(){return`__CHANNEL__:${this.id}`}};a=new WeakMap;var o=class{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return l(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function _(e,n,t){let r=new s;return r.onmessage=t,l(`plugin:${e}|register_listener`,{event:n,handler:r}).then((()=>new o(e,n,r.id)))}async function l(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}function c(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)}return e.readText=async function(){return(await l("plugin:clipboard|read")).options},e.writeText=async function(e,n){return l("plugin:clipboard|write",{data:{kind:"PlainText",options:{label:null==n?void 0:n.label,text:e}}})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_CLIPBOARDMANAGER__})} if("__TAURI__"in window){var __TAURI_CLIPBOARDMANAGER__=function(e){"use strict";async function n(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}return"function"==typeof SuppressedError&&SuppressedError,e.readText=async function(){return(await n("plugin:clipboard|read")).plainText.text},e.writeText=async function(e,r){return n("plugin:clipboard|write",{data:{plainText:{label:null==r?void 0:r.label,text:e}}})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_CLIPBOARDMANAGER__})}

@ -39,7 +39,7 @@ impl<R: Runtime> Clipboard<R> {
match &self.clipboard { match &self.clipboard {
Ok(clipboard) => { Ok(clipboard) => {
let text = clipboard.lock().unwrap().get_text()?; let text = clipboard.lock().unwrap().get_text()?;
Ok(ClipboardContents::PlainText(text)) Ok(ClipboardContents::PlainText { text })
} }
Err(e) => Err(crate::Error::Clipboard(e.to_string())), Err(e) => Err(crate::Error::Clipboard(e.to_string())),
} }

@ -5,13 +5,13 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind", content = "options")] #[serde(rename_all = "camelCase")]
pub enum ClipKind { pub enum ClipKind {
PlainText { label: Option<String>, text: String }, PlainText { label: Option<String>, text: String },
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind", content = "options")] #[serde(rename_all = "camelCase")]
pub enum ClipboardContents { pub enum ClipboardContents {
PlainText(String), PlainText { text: String },
} }

@ -2,7 +2,7 @@
## \[2.0.0-alpha.1] ## \[2.0.0-alpha.1]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.0] ## \[2.0.0-alpha.0]

@ -9,7 +9,8 @@ rust-version = { workspace = true }
links = "tauri-plugin-deep-link" links = "tauri-plugin-deep-link"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
targets = [ "x86_64-linux-android" ] targets = [ "x86_64-linux-android" ]
[build-dependencies] [build-dependencies]
@ -24,6 +25,3 @@ tauri = { workspace = true }
log = { workspace = true } log = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
url = "2" url = "2"
[features]
dox = [ "tauri/dox" ]

@ -6,8 +6,10 @@ package app.tauri.deep_link
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle
import android.webkit.WebView import android.webkit.WebView
import app.tauri.Logger import app.tauri.Logger
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Command import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Channel import app.tauri.plugin.Channel
@ -15,6 +17,11 @@ import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke import app.tauri.plugin.Invoke
@InvokeArg
class SetEventHandlerArgs {
lateinit var handler: Channel
}
@TauriPlugin @TauriPlugin
class DeepLinkPlugin(private val activity: Activity): Plugin(activity) { class DeepLinkPlugin(private val activity: Activity): Plugin(activity) {
//private val implementation = Example() //private val implementation = Example()
@ -33,29 +40,17 @@ class DeepLinkPlugin(private val activity: Activity): Plugin(activity) {
invoke.resolve(ret) invoke.resolve(ret)
} }
/* @Command
fun registerListenerRust(invoke: Invoke) {
val value = invoke.getString("value") ?: ""
val ret = JSObject()
ret.put("value", this.currentUrl ?: "none")
invoke.resolve(ret)
} */
@Command @Command
fun setEventHandler(invoke: Invoke) { fun setEventHandler(invoke: Invoke) {
val channel = invoke.getChannel("handler") val args = invoke.parseArgs(SetEventHandlerArgs::class.java)
this.channel = args.handler
if (channel == null) {
invoke.reject("`handler` not provided")
}
this.channel = channel
invoke.resolve() invoke.resolve()
} }
override fun load(webView: WebView) { override fun load(webView: WebView) {
instance = this instance = this
var intent = activity.intent val intent = activity.intent
if (intent.action == Intent.ACTION_VIEW) { if (intent.action == Intent.ACTION_VIEW) {
// TODO: check if it makes sense to split up init url and last url // TODO: check if it makes sense to split up init url and last url

@ -35,7 +35,7 @@ fn main() {
.run() .run()
{ {
println!("{error:#}"); println!("{error:#}");
if !(cfg!(feature = "dox") && std::env::var("TARGET").unwrap().contains("android")) { if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) {
std::process::exit(1); std::process::exit(1);
} }
} }

@ -10,11 +10,11 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.6", "@tauri-apps/api": "2.0.0-alpha.11",
"@tauri-apps/plugin-deep-link": "2.0.0-alpha.1" "@tauri-apps/plugin-deep-link": "2.0.0-alpha.1"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "2.0.0-alpha.16", "@tauri-apps/cli": "2.0.0-alpha.17",
"internal-ip": "^8.0.0", "internal-ip": "^8.0.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.5.0" "vite": "^4.5.0"

@ -28,6 +28,6 @@
"tslib": "^2.5.0" "tslib": "^2.5.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_DEEPLINK__=function(e){"use strict";var n=Object.defineProperty,t=(e,t)=>{for(var r in t)n(e,r,{get:t[r],enumerable:!0})},r=(e,n,t)=>{if(!n.has(e))throw TypeError("Cannot "+t)},i=(e,n,t)=>(r(e,n,"read from private field"),t?t.call(e):n.get(e));function a(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}t({},{Channel:()=>s,PluginListener:()=>u,addPluginListener:()=>o,convertFileSrc:()=>c,invoke:()=>l,transformCallback:()=>a});var _,s=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,n,t)=>{if(n.has(e))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(e):n.set(e,t)})(this,_,(()=>{})),this.id=a((e=>{i(this,_).call(this,e)}))}set onmessage(e){((e,n,t,i)=>{r(e,n,"write to private field"),i?i.call(e,t):n.set(e,t)})(this,_,e)}get onmessage(){return i(this,_)}toJSON(){return`__CHANNEL__:${this.id}`}};_=new WeakMap;var u=class{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return l(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function o(e,n,t){let r=new s;return r.onmessage=t,l(`plugin:${e}|register_listener`,{event:n,handler:r}).then((()=>new u(e,n,r.id)))}async function l(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}function c(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)}t({},{TauriEvent:()=>d,emit:()=>v,listen:()=>h,once:()=>I});var d=(e=>(e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_FILE_DROP="tauri://file-drop",e.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",e.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",e.MENU="tauri://menu",e))(d||{});async function E(e,n){await l("plugin:event|unlisten",{event:e,eventId:n})}async function h(e,n,t){return l("plugin:event|listen",{event:e,windowLabel:t?.target,handler:a(n)}).then((n=>async()=>E(e,n)))}async function I(e,n,t){return h(e,(t=>{n(t),E(e,t.id).catch((()=>{}))}),t)}async function v(e,n,t){await l("plugin:event|emit",{event:e,windowLabel:t?.target,payload:n})}async function N(){return await l("plugin:deep-link|get_current")}return e.getCurrent=N,e.onOpenUrl=async function(e){const n=await N();return null!=n&&e(n),await h("deep-link://new-url",(n=>e(n.payload)))},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_DEEPLINK__})} if("__TAURI__"in window){var __TAURI_DEEPLINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function _(e,t,_){return r("plugin:event|listen",{event:e,windowLabel:_?.target,handler:n(t)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function i(){return await r("plugin:deep-link|get_current")}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_FILE_DROP="tauri://file-drop",e.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",e.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",e.MENU="tauri://menu"}(t||(t={})),e.getCurrent=i,e.onOpenUrl=async function(e){const n=await i();return null!=n&&e(n),await _("deep-link://new-url",(n=>e(n.payload)))},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_DEEPLINK__})}

@ -141,7 +141,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
.on_event(|_app, _event| { .on_event(|_app, _event| {
#[cfg(any(target_os = "macos", target_os = "ios"))] #[cfg(any(target_os = "macos", target_os = "ios"))]
if let tauri::RunEvent::Opened { urls } = _event { if let tauri::RunEvent::Opened { urls } = _event {
let _ = _app.emit_all("deep-link://new-url", urls); let _ = _app.emit("deep-link://new-url", urls);
_app.state::<DeepLink<R>>() _app.state::<DeepLink<R>>()
.current .current
.lock() .lock()

@ -2,7 +2,7 @@
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]

@ -9,7 +9,8 @@ rust-version = { workspace = true }
links = "tauri-plugin-dialog" links = "tauri-plugin-dialog"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
targets = [ "x86_64-unknown-linux-gnu", "x86_64-linux-android" ] targets = [ "x86_64-unknown-linux-gnu", "x86_64-linux-android" ]
[dependencies] [dependencies]
@ -24,11 +25,8 @@ tauri-plugin-fs = { path = "../fs", version = "2.0.0-alpha.3" }
glib = "0.16" glib = "0.16"
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
rfd = { version = "0.11", features = [ "gtk3", "common-controls-v6" ] } rfd = { version = "0.12", features = [ "gtk3", "common-controls-v6" ] }
raw-window-handle = "0.5" raw-window-handle = "0.5"
[build-dependencies] [build-dependencies]
tauri-build = { workspace = true } tauri-build = { workspace = true }
[features]
dox = [ "tauri/dox" ]

@ -14,24 +14,44 @@ import androidx.activity.result.ActivityResult
import app.tauri.Logger import app.tauri.Logger
import app.tauri.annotation.ActivityCallback import app.tauri.annotation.ActivityCallback
import app.tauri.annotation.Command import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Invoke import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin import app.tauri.plugin.Plugin
import org.json.JSONException
@InvokeArg
class Filter {
lateinit var extensions: Array<String>
}
@InvokeArg
class FilePickerOptions {
lateinit var filters: Array<Filter>
var multiple: Boolean? = null
var readData: Boolean? = null
}
@InvokeArg
class MessageOptions {
var title: String?
lateinit var message: String
var okButtonLabel: String?
var cancelButtonLabel: String?
}
@TauriPlugin @TauriPlugin
class DialogPlugin(private val activity: Activity): Plugin(activity) { class DialogPlugin(private val activity: Activity): Plugin(activity) {
var filePickerOptions: FilePickerOptions? = null
@Command @Command
fun showFilePicker(invoke: Invoke) { fun showFilePicker(invoke: Invoke) {
try { try {
val filters = invoke.getArray("filters", JSArray()) val args = invoke.parseArgs(FilePickerOptions::class.java)
val multiple = invoke.getBoolean("multiple", false) val parsedTypes = parseFiltersOption(args.filters)
val parsedTypes = parseFiltersOption(filters)
val intent = if (parsedTypes != null && parsedTypes.isNotEmpty()) { val intent = if (parsedTypes.isNotEmpty()) {
val intent = Intent(Intent.ACTION_PICK) val intent = Intent(Intent.ACTION_PICK)
intent.putExtra(Intent.EXTRA_MIME_TYPES, parsedTypes) intent.putExtra(Intent.EXTRA_MIME_TYPES, parsedTypes)
@ -55,7 +75,7 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
intent intent
} }
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple) intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, args.multiple ?: false)
startActivityForResult(invoke, intent, "filePickerResult") startActivityForResult(invoke, intent, "filePickerResult")
} catch (ex: Exception) { } catch (ex: Exception) {
@ -68,10 +88,9 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
@ActivityCallback @ActivityCallback
fun filePickerResult(invoke: Invoke, result: ActivityResult) { fun filePickerResult(invoke: Invoke, result: ActivityResult) {
try { try {
val readData = invoke.getBoolean("readData", false)
when (result.resultCode) { when (result.resultCode) {
Activity.RESULT_OK -> { Activity.RESULT_OK -> {
val callResult = createPickFilesResult(result.data, readData) val callResult = createPickFilesResult(result.data, filePickerOptions?.readData ?: false)
invoke.resolve(callResult) invoke.resolve(callResult)
} }
Activity.RESULT_CANCELED -> invoke.reject("File picker cancelled") Activity.RESULT_CANCELED -> invoke.reject("File picker cancelled")
@ -130,36 +149,19 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
return callResult return callResult
} }
private fun parseFiltersOption(filters: JSArray): Array<String>? { private fun parseFiltersOption(filters: Array<Filter>): Array<String> {
return try { val mimeTypes = mutableListOf<String>()
val filtersList: List<JSObject> = filters.toList() for (filter in filters) {
val mimeTypes = mutableListOf<String>() for (mime in filter.extensions) {
for (filter in filtersList) { mimeTypes.add(if (mime == "text/csv") "text/comma-separated-values" else mime)
val extensionsList = filter.getJSONArray("extensions")
for (i in 0 until extensionsList.length()) {
val mime = extensionsList.getString(i)
mimeTypes.add(if (mime == "text/csv") "text/comma-separated-values" else mime)
}
} }
mimeTypes.toTypedArray()
} catch (exception: JSONException) {
Logger.error("parseTypesOption failed.", exception)
null
} }
return mimeTypes.toTypedArray()
} }
@Command @Command
fun showMessageDialog(invoke: Invoke) { fun showMessageDialog(invoke: Invoke) {
val title = invoke.getString("title") val args = invoke.parseArgs(MessageOptions::class.java)
val message = invoke.getString("message")
val okButtonLabel = invoke.getString("okButtonLabel", "OK")
val cancelButtonLabel = invoke.getString("cancelButtonLabel", "Cancel")
if (message == null) {
invoke.reject("The `message` argument is required")
return
}
if (activity.isFinishing) { if (activity.isFinishing) {
invoke.reject("App is finishing") invoke.reject("App is finishing")
@ -177,19 +179,19 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
.post { .post {
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
if (title != null) { if (args.title != null) {
builder.setTitle(title) builder.setTitle(args.title)
} }
builder builder
.setMessage(message) .setMessage(args.message)
.setPositiveButton( .setPositiveButton(
okButtonLabel args.okButtonLabel ?: "OK"
) { dialog, _ -> ) { dialog, _ ->
dialog.dismiss() dialog.dismiss()
handler(false, true) handler(false, true)
} }
.setNegativeButton( .setNegativeButton(
cancelButtonLabel args.cancelButtonLabel ?: "Cancel"
) { dialog, _ -> ) { dialog, _ ->
dialog.dismiss() dialog.dismiss()
handler(false, false) handler(false, false)

@ -10,7 +10,7 @@ fn main() {
{ {
println!("{error:#}"); println!("{error:#}");
// when building documentation for Android the plugin build result is irrelevant to the crate itself // when building documentation for Android the plugin build result is irrelevant to the crate itself
if !(cfg!(feature = "dox") && std::env::var("TARGET").unwrap().contains("android")) { if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) {
std::process::exit(1); std::process::exit(1);
} }
} }

@ -2,207 +2,223 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import UIKit
import MobileCoreServices import MobileCoreServices
import PhotosUI
import Photos import Photos
import WebKit import PhotosUI
import Tauri
import SwiftRs import SwiftRs
import Tauri
import UIKit
import WebKit
enum FilePickerEvent { enum FilePickerEvent {
case selected([URL]) case selected([URL])
case cancelled case cancelled
case error(String) case error(String)
}
struct MessageDialogOptions: Decodable {
let title: String?
let message: String
var okButtonLabel = "OK"
var cancelButtonLabel = "Cancel"
}
struct Filter: Decodable {
var extensions: [String] = []
}
struct FilePickerOptions: Decodable {
var multiple = false
var readData = false
var filters: [Filter] = []
} }
class DialogPlugin: Plugin { class DialogPlugin: Plugin {
var filePickerController: FilePickerController! var filePickerController: FilePickerController!
var pendingInvoke: Invoke? = nil var pendingInvoke: Invoke? = nil
var pendingInvokeArgs: FilePickerOptions? = nil
override init() {
super.init() override init() {
filePickerController = FilePickerController(self) super.init()
} filePickerController = FilePickerController(self)
}
@objc public func showFilePicker(_ invoke: Invoke) {
let multiple = invoke.getBool("multiple", false) @objc public func showFilePicker(_ invoke: Invoke) throws {
let filters = invoke.getArray("filters") ?? [] let args = try invoke.parseArgs(FilePickerOptions.self)
let parsedTypes = parseFiltersOption(filters)
let parsedTypes = parseFiltersOption(args.filters)
var isMedia = true
var uniqueMimeType: Bool? = nil var isMedia = true
var mimeKind: String? = nil var uniqueMimeType: Bool? = nil
if !parsedTypes.isEmpty { var mimeKind: String? = nil
uniqueMimeType = true if !parsedTypes.isEmpty {
for mime in parsedTypes { uniqueMimeType = true
let kind = mime.components(separatedBy: "/")[0] for mime in parsedTypes {
if kind != "image" && kind != "video" { let kind = mime.components(separatedBy: "/")[0]
isMedia = false if kind != "image" && kind != "video" {
} isMedia = false
if (mimeKind == nil) { }
mimeKind = kind if mimeKind == nil {
} else if (mimeKind != kind) { mimeKind = kind
uniqueMimeType = false } else if mimeKind != kind {
} uniqueMimeType = false
}
} }
} }
pendingInvoke = invoke pendingInvoke = invoke
pendingInvokeArgs = args
if uniqueMimeType == true || isMedia {
DispatchQueue.main.async { if uniqueMimeType == true || isMedia {
if #available(iOS 14, *) { DispatchQueue.main.async {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) if #available(iOS 14, *) {
configuration.selectionLimit = multiple ? 0 : 1 var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.selectionLimit = args.multiple ? 0 : 1
if uniqueMimeType == true {
if mimeKind == "image" { if uniqueMimeType == true {
configuration.filter = .images if mimeKind == "image" {
} else if mimeKind == "video" { configuration.filter = .images
configuration.filter = .videos } else if mimeKind == "video" {
} configuration.filter = .videos
} }
}
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self.filePickerController let picker = PHPickerViewController(configuration: configuration)
picker.modalPresentationStyle = .fullScreen picker.delegate = self.filePickerController
self.presentViewController(picker) picker.modalPresentationStyle = .fullScreen
} else { self.presentViewController(picker)
let picker = UIImagePickerController() } else {
picker.delegate = self.filePickerController let picker = UIImagePickerController()
picker.delegate = self.filePickerController
if uniqueMimeType == true && mimeKind == "image" {
picker.sourceType = .photoLibrary if uniqueMimeType == true && mimeKind == "image" {
} picker.sourceType = .photoLibrary
}
picker.sourceType = .photoLibrary
picker.modalPresentationStyle = .fullScreen picker.sourceType = .photoLibrary
self.presentViewController(picker) picker.modalPresentationStyle = .fullScreen
} self.presentViewController(picker)
} }
} else { }
let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes } else {
DispatchQueue.main.async { let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes
let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import) DispatchQueue.main.async {
picker.delegate = self.filePickerController let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
picker.allowsMultipleSelection = multiple picker.delegate = self.filePickerController
picker.modalPresentationStyle = .fullScreen picker.allowsMultipleSelection = args.multiple
self.presentViewController(picker) picker.modalPresentationStyle = .fullScreen
} self.presentViewController(picker)
} }
} }
}
private func presentViewController(_ viewControllerToPresent: UIViewController) {
self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil) private func presentViewController(_ viewControllerToPresent: UIViewController) {
} self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil)
private func parseFiltersOption(_ filters: JSArray) -> [String] {
var parsedTypes: [String] = []
for (_, filter) in filters.enumerated() {
let filterObj = filter as? JSObject
if let filterObj = filterObj {
let extensions = filterObj["extensions"] as? JSArray
if let extensions = extensions {
for e in extensions {
let ext = e as? String ?? ""
guard let utType: String = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, ext as CFString, nil)?.takeRetainedValue() as String? else {
continue
}
parsedTypes.append(utType)
}
}
}
}
return parsedTypes
} }
public func onFilePickerEvent(_ event: FilePickerEvent) { private func parseFiltersOption(_ filters: [Filter]) -> [String] {
switch event { var parsedTypes: [String] = []
case .selected(let urls): for filter in filters {
let readData = pendingInvoke?.getBool("readData", false) ?? false for ext in filter.extensions {
do { guard
let filesResult = try urls.map {(url: URL) -> JSObject in let utType: String = UTTypeCreatePreferredIdentifierForTag(
var file = JSObject() kUTTagClassMIMEType, ext as CFString, nil)?.takeRetainedValue() as String?
else {
let mimeType = filePickerController.getMimeTypeFromUrl(url) continue
let isVideo = mimeType.hasPrefix("video")
let isImage = mimeType.hasPrefix("image")
if readData {
file["data"] = try Data(contentsOf: url).base64EncodedString()
}
if isVideo {
file["duration"] = filePickerController.getVideoDuration(url)
let (height, width) = filePickerController.getVideoDimensions(url)
if let height = height {
file["height"] = height
}
if let width = width {
file["width"] = width
}
} else if isImage {
let (height, width) = filePickerController.getImageDimensions(url)
if let height = height {
file["height"] = height
}
if let width = width {
file["width"] = width
}
}
file["modifiedAt"] = filePickerController.getModifiedAtFromUrl(url)
file["mimeType"] = mimeType
file["name"] = url.lastPathComponent
file["path"] = url.absoluteString
file["size"] = try filePickerController.getSizeFromUrl(url)
return file
}
pendingInvoke?.resolve(["files": filesResult])
} catch let error as NSError {
pendingInvoke?.reject(error.localizedDescription, nil, error)
return
} }
parsedTypes.append(utType)
}
}
return parsedTypes
}
public func onFilePickerEvent(_ event: FilePickerEvent) {
switch event {
case .selected(let urls):
let readData = pendingInvokeArgs?.readData ?? false
do {
let filesResult = try urls.map { (url: URL) -> JSObject in
var file = JSObject()
let mimeType = filePickerController.getMimeTypeFromUrl(url)
let isVideo = mimeType.hasPrefix("video")
let isImage = mimeType.hasPrefix("image")
if readData {
file["data"] = try Data(contentsOf: url).base64EncodedString()
}
if isVideo {
file["duration"] = filePickerController.getVideoDuration(url)
let (height, width) = filePickerController.getVideoDimensions(url)
if let height = height {
file["height"] = height
}
if let width = width {
file["width"] = width
}
} else if isImage {
let (height, width) = filePickerController.getImageDimensions(url)
if let height = height {
file["height"] = height
}
if let width = width {
file["width"] = width
}
}
file["modifiedAt"] = filePickerController.getModifiedAtFromUrl(url)
file["mimeType"] = mimeType
file["name"] = url.lastPathComponent
file["path"] = url.absoluteString
file["size"] = try filePickerController.getSizeFromUrl(url)
return file
}
pendingInvoke?.resolve(["files": filesResult])
} catch let error as NSError {
pendingInvoke?.reject(error.localizedDescription, error: error)
return
}
pendingInvoke?.resolve(["files": urls])
case .cancelled:
let files: JSArray = []
pendingInvoke?.resolve(["files": files])
case .error(let error):
pendingInvoke?.reject(error)
}
}
pendingInvoke?.resolve(["files": urls]) @objc public func showMessageDialog(_ invoke: Invoke) throws {
case .cancelled: let manager = self.manager
let files: JSArray = [] let args = try invoke.parseArgs(MessageDialogOptions.self)
pendingInvoke?.resolve(["files": files])
case .error(let error): DispatchQueue.main.async { [] in
pendingInvoke?.reject(error) let alert = UIAlertController(
} title: args.title, message: args.message, preferredStyle: UIAlertController.Style.alert)
} alert.addAction(
UIAlertAction(
@objc public func showMessageDialog(_ invoke: Invoke) { title: args.cancelButtonLabel, style: UIAlertAction.Style.default,
let manager = self.manager handler: { (_) -> Void in
let title = invoke.getString("title") invoke.resolve([
guard let message = invoke.getString("message") else { "value": false,
invoke.reject("The `message` argument is required") "cancelled": false,
return ])
} }))
let okButtonLabel = invoke.getString("okButtonLabel") ?? "OK" alert.addAction(
let cancelButtonLabel = invoke.getString("cancelButtonLabel") ?? "Cancel" UIAlertAction(
title: args.okButtonLabel, style: UIAlertAction.Style.default,
DispatchQueue.main.async { [] in handler: { (_) -> Void in
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert) invoke.resolve([
alert.addAction(UIAlertAction(title: cancelButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in "value": true,
invoke.resolve([ "cancelled": false,
"value": false, ])
"cancelled": false }))
])
})) manager.viewController?.present(alert, animated: true, completion: nil)
alert.addAction(UIAlertAction(title: okButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in }
invoke.resolve([ }
"value": true,
"cancelled": false
])
}))
manager.viewController?.present(alert, animated: true, completion: nil)
}
}
} }
@_cdecl("init_plugin_dialog") @_cdecl("init_plugin_dialog")

@ -27,6 +27,6 @@
"tslib": "^2.4.1" "tslib": "^2.4.1"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_DIALOG__=function(n){"use strict";var t=Object.defineProperty,e=(n,t,e)=>{if(!t.has(n))throw TypeError("Cannot "+e)},i=(n,t,i)=>(e(n,t,"read from private field"),i?i.call(n):t.get(n));function o(n,t=!1){return window.__TAURI_INTERNALS__.transformCallback(n,t)}((n,e)=>{for(var i in e)t(n,i,{get:e[i],enumerable:!0})})({},{Channel:()=>r,PluginListener:()=>a,addPluginListener:()=>s,convertFileSrc:()=>d,invoke:()=>u,transformCallback:()=>o});var l,r=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((n,t,e)=>{if(t.has(n))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(n):t.set(n,e)})(this,l,(()=>{})),this.id=o((n=>{i(this,l).call(this,n)}))}set onmessage(n){var t,i,o,r;o=n,e(t=this,i=l,"write to private field"),r?r.call(t,o):i.set(t,o)}get onmessage(){return i(this,l)}toJSON(){return`__CHANNEL__:${this.id}`}};l=new WeakMap;var a=class{constructor(n,t,e){this.plugin=n,this.event=t,this.channelId=e}async unregister(){return u(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function s(n,t,e){let i=new r;return i.onmessage=e,u(`plugin:${n}|register_listener`,{event:t,handler:i}).then((()=>new a(n,t,i.id)))}async function u(n,t={},e){return window.__TAURI_INTERNALS__.invoke(n,t,e)}function d(n,t="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(n,t)}return n.ask=async function(n,t){var e,i,o,l,r;const a="string"==typeof t?{title:t}:t;return u("plugin:dialog|ask",{message:n.toString(),title:null===(e=null==a?void 0:a.title)||void 0===e?void 0:e.toString(),type_:null==a?void 0:a.type,okButtonLabel:null!==(o=null===(i=null==a?void 0:a.okLabel)||void 0===i?void 0:i.toString())&&void 0!==o?o:"Yes",cancelButtonLabel:null!==(r=null===(l=null==a?void 0:a.cancelLabel)||void 0===l?void 0:l.toString())&&void 0!==r?r:"No"})},n.confirm=async function(n,t){var e,i,o,l,r;const a="string"==typeof t?{title:t}:t;return u("plugin:dialog|confirm",{message:n.toString(),title:null===(e=null==a?void 0:a.title)||void 0===e?void 0:e.toString(),type_:null==a?void 0:a.type,okButtonLabel:null!==(o=null===(i=null==a?void 0:a.okLabel)||void 0===i?void 0:i.toString())&&void 0!==o?o:"Ok",cancelButtonLabel:null!==(r=null===(l=null==a?void 0:a.cancelLabel)||void 0===l?void 0:l.toString())&&void 0!==r?r:"Cancel"})},n.message=async function(n,t){var e,i;const o="string"==typeof t?{title:t}:t;return u("plugin:dialog|message",{message:n.toString(),title:null===(e=null==o?void 0:o.title)||void 0===e?void 0:e.toString(),type_:null==o?void 0:o.type,okButtonLabel:null===(i=null==o?void 0:o.okLabel)||void 0===i?void 0:i.toString()})},n.open=async function(n={}){return"object"==typeof n&&Object.freeze(n),u("plugin:dialog|open",{options:n})},n.save=async function(n={}){return"object"==typeof n&&Object.freeze(n),u("plugin:dialog|save",{options:n})},n}({});Object.defineProperty(window.__TAURI__,"dialog",{value:__TAURI_DIALOG__})} if("__TAURI__"in window){var __TAURI_DIALOG__=function(n){"use strict";async function o(n,o={},t){return window.__TAURI_INTERNALS__.invoke(n,o,t)}return"function"==typeof SuppressedError&&SuppressedError,n.ask=async function(n,t){var i,l,e,u,r;const d="string"==typeof t?{title:t}:t;return o("plugin:dialog|ask",{message:n.toString(),title:null===(i=null==d?void 0:d.title)||void 0===i?void 0:i.toString(),type_:null==d?void 0:d.type,okButtonLabel:null!==(e=null===(l=null==d?void 0:d.okLabel)||void 0===l?void 0:l.toString())&&void 0!==e?e:"Yes",cancelButtonLabel:null!==(r=null===(u=null==d?void 0:d.cancelLabel)||void 0===u?void 0:u.toString())&&void 0!==r?r:"No"})},n.confirm=async function(n,t){var i,l,e,u,r;const d="string"==typeof t?{title:t}:t;return o("plugin:dialog|confirm",{message:n.toString(),title:null===(i=null==d?void 0:d.title)||void 0===i?void 0:i.toString(),type_:null==d?void 0:d.type,okButtonLabel:null!==(e=null===(l=null==d?void 0:d.okLabel)||void 0===l?void 0:l.toString())&&void 0!==e?e:"Ok",cancelButtonLabel:null!==(r=null===(u=null==d?void 0:d.cancelLabel)||void 0===u?void 0:u.toString())&&void 0!==r?r:"Cancel"})},n.message=async function(n,t){var i,l;const e="string"==typeof t?{title:t}:t;return o("plugin:dialog|message",{message:n.toString(),title:null===(i=null==e?void 0:e.title)||void 0===i?void 0:i.toString(),type_:null==e?void 0:e.type,okButtonLabel:null===(l=null==e?void 0:e.okLabel)||void 0===l?void 0:l.toString()})},n.open=async function(n={}){return"object"==typeof n&&Object.freeze(n),o("plugin:dialog|open",{options:n})},n.save=async function(n={}){return"object"==typeof n&&Object.freeze(n),o("plugin:dialog|save",{options:n})},n}({});Object.defineProperty(window.__TAURI__,"dialog",{value:__TAURI_DIALOG__})}

@ -56,7 +56,10 @@ macro_rules! run_dialog {
($e:expr, $h: ident) => {{ ($e:expr, $h: ident) => {{
std::thread::spawn(move || { std::thread::spawn(move || {
let response = tauri::async_runtime::block_on($e); let response = tauri::async_runtime::block_on($e);
$h(response); $h(!matches!(
response,
rfd::MessageDialogResult::No | rfd::MessageDialogResult::Cancel
));
}); });
}}; }};
} }
@ -68,7 +71,10 @@ macro_rules! run_dialog {
let context = glib::MainContext::default(); let context = glib::MainContext::default();
context.invoke_with_priority(glib::PRIORITY_HIGH, move || { context.invoke_with_priority(glib::PRIORITY_HIGH, move || {
let response = $e; let response = $e;
$h(response); $h(!matches!(
response,
rfd::MessageDialogResult::No | rfd::MessageDialogResult::Cancel
));
}); });
}); });
}}; }};

@ -1 +1 @@
!function(){"use strict";var e=Object.defineProperty,n=(e,n,t)=>{if(!n.has(e))throw TypeError("Cannot "+t)},t=(e,t,r)=>(n(e,t,"read from private field"),r?r.call(e):t.get(e));function r(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}((n,t)=>{for(var r in t)e(n,r,{get:t[r],enumerable:!0})})({},{Channel:()=>s,PluginListener:()=>a,addPluginListener:()=>o,convertFileSrc:()=>c,invoke:()=>l,transformCallback:()=>r});var i,s=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,n,t)=>{if(n.has(e))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(e):n.set(e,t)})(this,i,(()=>{})),this.id=r((e=>{t(this,i).call(this,e)}))}set onmessage(e){var t,r,s,a;s=e,n(t=this,r=i,"write to private field"),a?a.call(t,s):r.set(t,s)}get onmessage(){return t(this,i)}toJSON(){return`__CHANNEL__:${this.id}`}};i=new WeakMap;var a=class{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return l(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function o(e,n,t){let r=new s;return r.onmessage=t,l(`plugin:${e}|register_listener`,{event:n,handler:r}).then((()=>new a(e,n,r.id)))}async function l(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}function c(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)}window.alert=function(e){l("plugin:dialog|message",{message:e.toString()})},window.confirm=function(e){return l("plugin:dialog|confirm",{message:e.toString()})}}(); !function(){"use strict";async function n(n,i={},o){return window.__TAURI_INTERNALS__.invoke(n,i,o)}"function"==typeof SuppressedError&&SuppressedError,window.alert=function(i){n("plugin:dialog|message",{message:i.toString()})},window.confirm=function(i){return n("plugin:dialog|confirm",{message:i.toString()})}}();

@ -2,7 +2,7 @@
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]

@ -8,7 +8,8 @@ edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "tauri/dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }

@ -28,6 +28,6 @@
"tslib": "^2.4.1" "tslib": "^2.4.1"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

File diff suppressed because one or more lines are too long

@ -2,7 +2,7 @@
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]

@ -8,7 +8,8 @@ license = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "tauri/dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }

@ -27,6 +27,6 @@
"tslib": "^2.4.1" "tslib": "^2.4.1"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_GLOBALSHORTCUT__=function(e){"use strict";var t=Object.defineProperty,n=(e,t,n)=>{if(!t.has(e))throw TypeError("Cannot "+n)},r=(e,t,r)=>(n(e,t,"read from private field"),r?r.call(e):t.get(e));function i(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}((e,n)=>{for(var r in n)t(e,r,{get:n[r],enumerable:!0})})({},{Channel:()=>a,PluginListener:()=>o,addPluginListener:()=>l,convertFileSrc:()=>c,invoke:()=>u,transformCallback:()=>i});var s,a=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,t,n)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,n)})(this,s,(()=>{})),this.id=i((e=>{r(this,s).call(this,e)}))}set onmessage(e){var t,r,i,a;i=e,n(t=this,r=s,"write to private field"),a?a.call(t,i):r.set(t,i)}get onmessage(){return r(this,s)}toJSON(){return`__CHANNEL__:${this.id}`}};s=new WeakMap;var o=class{constructor(e,t,n){this.plugin=e,this.event=t,this.channelId=n}async unregister(){return u(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function l(e,t,n){let r=new a;return r.onmessage=n,u(`plugin:${e}|register_listener`,{event:t,handler:r}).then((()=>new o(e,t,r.id)))}async function u(e,t={},n){return window.__TAURI_INTERNALS__.invoke(e,t,n)}function c(e,t="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,t)}return e.isRegistered=async function(e){return await u("plugin:globalShortcut|is_registered",{shortcut:e})},e.register=async function(e,t){const n=new a;return n.onmessage=t,await u("plugin:globalShortcut|register",{shortcut:e,handler:n})},e.registerAll=async function(e,t){const n=new a;return n.onmessage=t,await u("plugin:globalShortcut|register_all",{shortcuts:e,handler:n})},e.unregister=async function(e){return await u("plugin:globalShortcut|unregister",{shortcut:e})},e.unregisterAll=async function(){return await u("plugin:globalShortcut|unregister_all")},e}({});Object.defineProperty(window.__TAURI__,"globalShortcut",{value:__TAURI_GLOBALSHORTCUT__})} if("__TAURI__"in window){var __TAURI_GLOBALSHORTCUT__=function(t){"use strict";function e(t,e,r,n){if("a"===r&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?n:"a"===r?n.call(t):n?n.value:e.get(t)}var r;"function"==typeof SuppressedError&&SuppressedError;class n{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,r.set(this,(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((t=>{e(this,r,"f").call(this,t)}))}set onmessage(t){!function(t,e,r,n,o){if("m"===n)throw new TypeError("Private method is not writable");if("a"===n&&!o)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof e?t!==e||!o:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");"a"===n?o.call(t,r):o?o.value=r:e.set(t,r)}(this,r,t,"f")}get onmessage(){return e(this,r,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function o(t,e={},r){return window.__TAURI_INTERNALS__.invoke(t,e,r)}return r=new WeakMap,t.isRegistered=async function(t){return await o("plugin:globalShortcut|is_registered",{shortcut:t})},t.register=async function(t,e){const r=new n;return r.onmessage=e,await o("plugin:globalShortcut|register",{shortcut:t,handler:r})},t.registerAll=async function(t,e){const r=new n;return r.onmessage=e,await o("plugin:globalShortcut|register_all",{shortcuts:t,handler:r})},t.unregister=async function(t){return await o("plugin:globalShortcut|unregister",{shortcut:t})},t.unregisterAll=async function(){return await o("plugin:globalShortcut|unregister_all")},t}({});Object.defineProperty(window.__TAURI__,"globalShortcut",{value:__TAURI_GLOBALSHORTCUT__})}

@ -2,7 +2,7 @@
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.3] ## \[2.0.0-alpha.3]

@ -8,7 +8,8 @@ license = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "tauri/dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }

@ -27,6 +27,6 @@
"tslib": "^2.5.0" "tslib": "^2.5.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_HTTP__=function(e){"use strict";var t=Object.defineProperty,n=(e,t,n)=>{if(!t.has(e))throw TypeError("Cannot "+n)},r=(e,t,r)=>(n(e,t,"read from private field"),r?r.call(e):t.get(e));function i(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}((e,n)=>{for(var r in n)t(e,r,{get:n[r],enumerable:!0})})({},{Channel:()=>s,PluginListener:()=>o,addPluginListener:()=>c,convertFileSrc:()=>_,invoke:()=>l,transformCallback:()=>i});var a,s=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,t,n)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,n)})(this,a,(()=>{})),this.id=i((e=>{r(this,a).call(this,e)}))}set onmessage(e){var t,r,i,s;i=e,n(t=this,r=a,"write to private field"),s?s.call(t,i):r.set(t,i)}get onmessage(){return r(this,a)}toJSON(){return`__CHANNEL__:${this.id}`}};a=new WeakMap;var o=class{constructor(e,t,n){this.plugin=e,this.event=t,this.channelId=n}async unregister(){return l(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function c(e,t,n){let r=new s;return r.onmessage=n,l(`plugin:${e}|register_listener`,{event:t,handler:r}).then((()=>new o(e,t,r.id)))}async function l(e,t={},n){return window.__TAURI_INTERNALS__.invoke(e,t,n)}function _(e,t="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,t)}return e.fetch=async function(e,t){const n=null==t?void 0:t.maxRedirections,r=null==t?void 0:t.maxRedirections;t&&(delete t.maxRedirections,delete t.connectTimeout);const i=new Request(e,t),a=await i.arrayBuffer(),s=a.byteLength?Array.from(new Uint8Array(a)):null,o=await l("plugin:http|fetch",{method:i.method,url:i.url,headers:Array.from(i.headers.entries()),data:s,maxRedirections:n,connectTimeout:r});i.signal.addEventListener("abort",(()=>{l("plugin:http|fetch_cancel",{rid:o})}));const{status:c,statusText:_,url:d,headers:u}=await l("plugin:http|fetch_send",{rid:o}),h=await l("plugin:http|fetch_read_body",{rid:o}),f=new Response(new Uint8Array(h),{headers:u,status:c,statusText:_});return Object.defineProperty(f,"url",{value:d}),f},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})} if("__TAURI__"in window){var __TAURI_HTTP__=function(e){"use strict";async function t(e,t={},n){return window.__TAURI_INTERNALS__.invoke(e,t,n)}return"function"==typeof SuppressedError&&SuppressedError,e.fetch=async function(e,n){const r=null==n?void 0:n.maxRedirections,i=null==n?void 0:n.maxRedirections;n&&(delete n.maxRedirections,delete n.connectTimeout);const a=new Request(e,n),s=await a.arrayBuffer(),o=s.byteLength?Array.from(new Uint8Array(s)):null,u=await t("plugin:http|fetch",{method:a.method,url:a.url,headers:Array.from(a.headers.entries()),data:o,maxRedirections:r,connectTimeout:i});a.signal.addEventListener("abort",(()=>{t("plugin:http|fetch_cancel",{rid:u})}));const{status:d,statusText:c,url:_,headers:l}=await t("plugin:http|fetch_send",{rid:u}),f=await t("plugin:http|fetch_read_body",{rid:u}),h=new Response(new Uint8Array(f),{headers:l,status:d,statusText:c});return Object.defineProperty(h,"url",{value:_}),h},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})}

@ -8,7 +8,8 @@ edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "tauri/dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }

@ -2,7 +2,7 @@
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]

@ -8,7 +8,8 @@ edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "tauri/dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
[build-dependencies] [build-dependencies]
tauri-build = { workspace = true } tauri-build = { workspace = true }

@ -28,6 +28,6 @@
"tslib": "2.6.0" "tslib": "2.6.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_LOG__=function(e){"use strict";var n=Object.defineProperty,t=(e,t)=>{for(var r in t)n(e,r,{get:t[r],enumerable:!0})},r=(e,n,t)=>{if(!n.has(e))throw TypeError("Cannot "+t)},a=(e,n,t)=>(r(e,n,"read from private field"),t?t.call(e):n.get(e));function i(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}t({},{Channel:()=>l,PluginListener:()=>s,addPluginListener:()=>c,convertFileSrc:()=>_,invoke:()=>u,transformCallback:()=>i});var o,l=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,n,t)=>{if(n.has(e))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(e):n.set(e,t)})(this,o,(()=>{})),this.id=i((e=>{a(this,o).call(this,e)}))}set onmessage(e){((e,n,t,a)=>{r(e,n,"write to private field"),a?a.call(e,t):n.set(e,t)})(this,o,e)}get onmessage(){return a(this,o)}toJSON(){return`__CHANNEL__:${this.id}`}};o=new WeakMap;var s=class{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return u(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function c(e,n,t){let r=new l;return r.onmessage=t,u(`plugin:${e}|register_listener`,{event:n,handler:r}).then((()=>new s(e,n,r.id)))}async function u(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}function _(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)}t({},{TauriEvent:()=>f,emit:()=>E,listen:()=>w,once:()=>g});var d,f=(e=>(e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_FILE_DROP="tauri://file-drop",e.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",e.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",e.MENU="tauri://menu",e))(f||{});async function v(e,n){await u("plugin:event|unlisten",{event:e,eventId:n})}async function w(e,n,t){return u("plugin:event|listen",{event:e,windowLabel:t?.target,handler:i(n)}).then((n=>async()=>v(e,n)))}async function g(e,n,t){return w(e,(t=>{n(t),v(e,t.id).catch((()=>{}))}),t)}async function E(e,n,t){await u("plugin:event|emit",{event:e,windowLabel:t?.target,payload:n})}async function h(e,n,t){var r,a;const i=null===(r=(new Error).stack)||void 0===r?void 0:r.split("\n").map((e=>e.split("@"))),o=null==i?void 0:i.filter((([e,n])=>e.length>0&&"[native code]"!==n)),{file:l,line:s,keyValues:c}=null!=t?t:{};let _=null===(a=null==o?void 0:o[0])||void 0===a?void 0:a.filter((e=>e.length>0)).join("@");"Error"===_&&(_="webview::unknown"),await u("plugin:log|log",{level:e,message:n,location:_,file:l,line:s,keyValues:c})}return function(e){e[e.Trace=1]="Trace",e[e.Debug=2]="Debug",e[e.Info=3]="Info",e[e.Warn=4]="Warn",e[e.Error=5]="Error"}(d||(d={})),e.attachConsole=async function(){return await w("log://log",(e=>{const n=e.payload,t=n.message.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,"");switch(n.level){case d.Trace:console.log(t);break;case d.Debug:console.debug(t);break;case d.Info:console.info(t);break;case d.Warn:console.warn(t);break;case d.Error:console.error(t);break;default:throw new Error(`unknown log level ${n.level}`)}}))},e.debug=async function(e,n){await h(d.Debug,e,n)},e.error=async function(e,n){await h(d.Error,e,n)},e.info=async function(e,n){await h(d.Info,e,n)},e.trace=async function(e,n){await h(d.Trace,e,n)},e.warn=async function(e,n){await h(d.Warn,e,n)},e}({});Object.defineProperty(window.__TAURI__,"log",{value:__TAURI_LOG__})} if("__TAURI__"in window){var __TAURI_LOG__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var a,o;async function t(e,a,o){return r("plugin:event|listen",{event:e,windowLabel:o?.target,handler:n(a)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function i(e,n,a){var o,t;const i=null===(o=(new Error).stack)||void 0===o?void 0:o.split("\n").map((e=>e.split("@"))),l=null==i?void 0:i.filter((([e,n])=>e.length>0&&"[native code]"!==n)),{file:u,line:c,keyValues:s}=null!=a?a:{};let _=null===(t=null==l?void 0:l[0])||void 0===t?void 0:t.filter((e=>e.length>0)).join("@");"Error"===_&&(_="webview::unknown"),await r("plugin:log|log",{level:e,message:n,location:_,file:u,line:c,keyValues:s})}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_FILE_DROP="tauri://file-drop",e.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",e.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",e.MENU="tauri://menu"}(a||(a={})),function(e){e[e.Trace=1]="Trace",e[e.Debug=2]="Debug",e[e.Info=3]="Info",e[e.Warn=4]="Warn",e[e.Error=5]="Error"}(o||(o={})),e.attachConsole=async function(){return await t("log://log",(e=>{const n=e.payload,r=n.message.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,"");switch(n.level){case o.Trace:console.log(r);break;case o.Debug:console.debug(r);break;case o.Info:console.info(r);break;case o.Warn:console.warn(r);break;case o.Error:console.error(r);break;default:throw new Error(`unknown log level ${n.level}`)}}))},e.debug=async function(e,n){await i(o.Debug,e,n)},e.error=async function(e,n){await i(o.Error,e,n)},e.info=async function(e,n){await i(o.Info,e,n)},e.trace=async function(e,n){await i(o.Trace,e,n)},e.warn=async function(e,n){await i(o.Warn,e,n)},e}({});Object.defineProperty(window.__TAURI__,"log",{value:__TAURI_LOG__})}

@ -475,7 +475,7 @@ impl Builder {
}; };
let app_handle = app_handle.clone(); let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
app_handle.emit_all("log://log", payload).unwrap(); app_handle.emit("log://log", payload).unwrap();
}); });
}) })
} }

@ -2,7 +2,7 @@
## \[2.0.0-alpha.2] ## \[2.0.0-alpha.2]
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16. - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
## \[2.0.0-alpha.3] ## \[2.0.0-alpha.3]

@ -9,7 +9,8 @@ rust-version = { workspace = true }
links = "tauri-plugin-notification" links = "tauri-plugin-notification"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "dox" ] rustc-args = [ "--cfg", "docsrs" ]
rustdoc-args = [ "--cfg", "docsrs" ]
targets = [ "x86_64-unknown-linux-gnu", "x86_64-linux-android" ] targets = [ "x86_64-unknown-linux-gnu", "x86_64-linux-android" ]
[build-dependencies] [build-dependencies]
@ -34,4 +35,3 @@ win7-notifications = { version = "0.3.1", optional = true }
[features] [features]
windows7-compat = [ "win7-notifications" ] windows7-compat = [ "win7-notifications" ]
dox = [ "tauri/dox" ]

@ -38,6 +38,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0") implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0") implementation("com.google.android.material:material:1.7.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

@ -4,7 +4,7 @@
<receiver android:name="app.tauri.notification.TimedNotificationPublisher" /> <receiver android:name="app.tauri.notification.TimedNotificationPublisher" />
<receiver android:name="app.tauri.notification.NotificationDismissReceiver" /> <receiver android:name="app.tauri.notification.NotificationDismissReceiver" />
<receiver <receiver
android:name="app.tauri.notification.NotificationRestoreReceiver" android:name="app.tauri.notification.LocalNotificationRestoreReceiver"
android:directBootAware="true" android:directBootAware="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>

@ -12,21 +12,42 @@ import android.graphics.Color
import android.media.AudioAttributes import android.media.AudioAttributes
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat
import app.tauri.Logger import app.tauri.Logger
import app.tauri.annotation.InvokeArg
import app.tauri.plugin.Invoke import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray import com.fasterxml.jackson.annotation.JsonValue
import app.tauri.plugin.JSObject
enum class Importance(@JsonValue val value: Int) {
private const val CHANNEL_ID = "id" None(0),
private const val CHANNEL_NAME = "name" Min(1),
private const val CHANNEL_DESCRIPTION = "description" Low(2),
private const val CHANNEL_IMPORTANCE = "importance" Default(3),
private const val CHANNEL_VISIBILITY = "visibility" High(4);
private const val CHANNEL_SOUND = "sound" }
private const val CHANNEL_VIBRATE = "vibration"
private const val CHANNEL_USE_LIGHTS = "lights" enum class Visibility(@JsonValue val value: Int) {
private const val CHANNEL_LIGHT_COLOR = "lightColor" Secret(-1),
Private(0),
Public(1);
}
@InvokeArg
class Channel {
lateinit var id: String
lateinit var name: String
var description: String? = null
var sound: String? = null
var lights: Boolean? = null
var lightsColor: String? = null
var vibration: Boolean? = null
var importance: Importance? = null
var visibility: Visibility? = null
}
@InvokeArg
class DeleteChannelArgs {
lateinit var id: String
}
class ChannelManager(private var context: Context) { class ChannelManager(private var context: Context) {
private var notificationManager: NotificationManager? = null private var notificationManager: NotificationManager? = null
@ -38,32 +59,7 @@ class ChannelManager(private var context: Context) {
fun createChannel(invoke: Invoke) { fun createChannel(invoke: Invoke) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = JSObject() val channel = invoke.parseArgs(Channel::class.java)
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) createChannel(channel)
invoke.resolve() invoke.resolve()
} else { } else {
@ -71,18 +67,18 @@ class ChannelManager(private var context: Context) {
} }
} }
private fun createChannel(channel: JSObject) { private fun createChannel(channel: Channel) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel( val notificationChannel = NotificationChannel(
channel.getString(CHANNEL_ID), channel.id,
channel.getString(CHANNEL_NAME), channel.name,
channel.getInteger(CHANNEL_IMPORTANCE)!! (channel.importance ?: Importance.Default).value
) )
notificationChannel.description = channel.getString(CHANNEL_DESCRIPTION) notificationChannel.description = channel.description
notificationChannel.lockscreenVisibility = channel.getInteger(CHANNEL_VISIBILITY, android.app.Notification.VISIBILITY_PRIVATE) notificationChannel.lockscreenVisibility = (channel.visibility ?: Visibility.Private).value
notificationChannel.enableVibration(channel.getBoolean(CHANNEL_VIBRATE, false)) notificationChannel.enableVibration(channel.vibration ?: false)
notificationChannel.enableLights(channel.getBoolean(CHANNEL_USE_LIGHTS, false)) notificationChannel.enableLights(channel.lights ?: false)
val lightColor = channel.getString(CHANNEL_LIGHT_COLOR) val lightColor = channel.lightsColor ?: ""
if (lightColor.isNotEmpty()) { if (lightColor.isNotEmpty()) {
try { try {
notificationChannel.lightColor = Color.parseColor(lightColor) notificationChannel.lightColor = Color.parseColor(lightColor)
@ -94,7 +90,7 @@ class ChannelManager(private var context: Context) {
) )
} }
} }
var sound = channel.getString(CHANNEL_SOUND) var sound = channel.sound ?: ""
if (sound.isNotEmpty()) { if (sound.isNotEmpty()) {
if (sound.contains(".")) { if (sound.contains(".")) {
sound = sound.substring(0, sound.lastIndexOf('.')) sound = sound.substring(0, sound.lastIndexOf('.'))
@ -113,8 +109,8 @@ class ChannelManager(private var context: Context) {
fun deleteChannel(invoke: Invoke) { fun deleteChannel(invoke: Invoke) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = invoke.getString("id") val args = invoke.parseArgs(DeleteChannelArgs::class.java)
notificationManager?.deleteNotificationChannel(channelId) notificationManager?.deleteNotificationChannel(args.id)
invoke.resolve() invoke.resolve()
} else { } else {
invoke.reject("channel not available") invoke.reject("channel not available")
@ -125,28 +121,29 @@ class ChannelManager(private var context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannels: List<NotificationChannel> = val notificationChannels: List<NotificationChannel> =
notificationManager?.notificationChannels ?: listOf() notificationManager?.notificationChannels ?: listOf()
val channels = JSArray()
val channels = mutableListOf<Channel>()
for (notificationChannel in notificationChannels) { for (notificationChannel in notificationChannels) {
val channel = JSObject() val channel = Channel()
channel.put(CHANNEL_ID, notificationChannel.id) channel.id = notificationChannel.id
channel.put(CHANNEL_NAME, notificationChannel.name) channel.name = notificationChannel.name.toString()
channel.put(CHANNEL_DESCRIPTION, notificationChannel.description) channel.description = notificationChannel.description
channel.put(CHANNEL_IMPORTANCE, notificationChannel.importance) channel.sound = notificationChannel.sound.toString()
channel.put(CHANNEL_VISIBILITY, notificationChannel.lockscreenVisibility) channel.lights = notificationChannel.shouldShowLights()
channel.put(CHANNEL_SOUND, notificationChannel.sound) String.format(
channel.put(CHANNEL_VIBRATE, notificationChannel.shouldVibrate()) "#%06X",
channel.put(CHANNEL_USE_LIGHTS, notificationChannel.shouldShowLights()) 0xFFFFFF and notificationChannel.lightColor
channel.put(
CHANNEL_LIGHT_COLOR, String.format(
"#%06X",
0xFFFFFF and notificationChannel.lightColor
)
) )
channels.put(channel) channel.vibration = notificationChannel.shouldVibrate()
channel.importance = Importance.values().firstOrNull { it.value == notificationChannel.importance }
channel.visibility = Visibility.values().firstOrNull { it.value == notificationChannel.lockscreenVisibility }
channels.add(channel)
} }
val result = JSObject()
result.put("channels", channels) invoke.resolveObject(channels)
invoke.resolve(result)
} else { } else {
invoke.reject("channel not available") invoke.reject("channel not available")
} }

@ -8,20 +8,22 @@ import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import app.tauri.annotation.InvokeArg
import app.tauri.plugin.JSArray import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject import app.tauri.plugin.JSObject
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
@InvokeArg
class Notification { class Notification {
var id: Int = 0
var title: String? = null var title: String? = null
var body: String? = null var body: String? = null
var largeBody: String? = null var largeBody: String? = null
var summary: String? = null var summary: String? = null
var id: Int = 0 var sound: String? = null
private var sound: String? = null var icon: String? = null
private var smallIcon: String? = null var largeIcon: String? = null
private var largeIcon: String? = null
var iconColor: String? = null var iconColor: String? = null
var actionTypeId: String? = null var actionTypeId: String? = null
var group: String? = null var group: String? = null
@ -33,7 +35,7 @@ class Notification {
var attachments: List<NotificationAttachment>? = null var attachments: List<NotificationAttachment>? = null
var schedule: NotificationSchedule? = null var schedule: NotificationSchedule? = null
var channelId: String? = null var channelId: String? = null
var source: JSObject? = null var sourceJson: String? = null
var visibility: Int? = null var visibility: Int? = null
var number: Int? = null var number: Int? = null
@ -54,18 +56,6 @@ class Notification {
return soundPath 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 { fun getIconColor(globalColor: String): String {
// use the one defined local before trying for a globally defined color // use the one defined local before trying for a globally defined color
return iconColor ?: globalColor return iconColor ?: globalColor
@ -73,8 +63,8 @@ class Notification {
fun getSmallIcon(context: Context, defaultIcon: Int): Int { fun getSmallIcon(context: Context, defaultIcon: Int): Int {
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
if (smallIcon != null) { if (icon != null) {
resId = AssetUtils.getResourceID(context, smallIcon, "drawable") resId = AssetUtils.getResourceID(context, icon, "drawable")
} }
if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) { if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) {
resId = defaultIcon resId = defaultIcon
@ -93,77 +83,15 @@ class Notification {
val isScheduled = schedule != null val isScheduled = schedule != null
companion object { companion object {
fun fromJson(jsonNotification: JSONObject): Notification { fun buildNotificationPendingList(notifications: List<Notification>): List<PendingNotification> {
val notification: JSObject = try { val pendingNotifications = mutableListOf<PendingNotification>()
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) { for (notification in notifications) {
val jsNotification = JSObject() val pendingNotification = PendingNotification(notification.id, notification.title, notification.body, notification.schedule, notification.extra)
jsNotification.put("id", notification.id) pendingNotifications.add(pendingNotification)
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 pendingNotifications
return result
} }
} }
} }
class PendingNotification(val id: Int, val title: String?, val body: String?, val schedule: NotificationSchedule?, val extra: JSObject?)

@ -1,51 +0,0 @@
// 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
}
}
}

@ -14,6 +14,7 @@ import android.os.Build
import android.webkit.WebView import android.webkit.WebView
import app.tauri.PermissionState import app.tauri.PermissionState
import app.tauri.annotation.Command import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Permission import app.tauri.annotation.Permission
import app.tauri.annotation.PermissionCallback import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.TauriPlugin import app.tauri.annotation.TauriPlugin
@ -21,11 +22,55 @@ import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin import app.tauri.plugin.Plugin
import org.json.JSONException
import org.json.JSONObject
const val LOCAL_NOTIFICATIONS = "permissionState" const val LOCAL_NOTIFICATIONS = "permissionState"
@InvokeArg
class PluginConfig {
var icon: String? = null
var sound: String? = null
var iconColor: String? = null
}
@InvokeArg
class BatchArgs {
lateinit var notifications: List<Notification>
}
@InvokeArg
class CancelArgs {
lateinit var notifications: List<Int>
}
@InvokeArg
class NotificationAction {
lateinit var id: String
var title: String? = null
var input: Boolean? = null
}
@InvokeArg
class ActionType {
lateinit var id: String
lateinit var actions: List<NotificationAction>
}
@InvokeArg
class RegisterActionTypesArgs {
lateinit var types: List<ActionType>
}
@InvokeArg
class ActiveNotification {
var id: Int = 0
var tag: String? = null
}
@InvokeArg
class RemoveActiveArgs {
var notifications: List<ActiveNotification> = listOf()
}
@TauriPlugin( @TauriPlugin(
permissions = [ permissions = [
Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "permissionState") Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "permissionState")
@ -41,8 +86,8 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
companion object { companion object {
var instance: NotificationPlugin? = null var instance: NotificationPlugin? = null
fun triggerNotification(notification: JSObject) { fun triggerNotification(notification: Notification) {
instance?.trigger("notification", notification) instance?.triggerObject("notification", notification)
} }
} }
@ -51,23 +96,32 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
super.load(webView) super.load(webView)
this.webView = webView this.webView = webView
notificationStorage = NotificationStorage(activity) notificationStorage = NotificationStorage(activity, jsonMapper())
val manager = TauriNotificationManager( val manager = TauriNotificationManager(
notificationStorage, notificationStorage,
activity, activity,
activity, activity,
getConfig() getConfig(PluginConfig::class.java)
) )
manager.createNotificationChannel() manager.createNotificationChannel()
this.manager = manager this.manager = manager
notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val intent = activity.intent
intent?.let {
onIntent(it)
}
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
onIntent(intent)
}
fun onIntent(intent: Intent) {
if (Intent.ACTION_MAIN != intent.action) { if (Intent.ACTION_MAIN != intent.action) {
return return
} }
@ -79,80 +133,43 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
@Command @Command
fun show(invoke: Invoke) { fun show(invoke: Invoke) {
val notification = Notification.fromJSObject(invoke.data) val notification = invoke.parseArgs(Notification::class.java)
val id = manager.schedule(notification) val id = manager.schedule(notification)
val returnVal = JSObject().put("id", id) invoke.resolveObject(id)
invoke.resolve(returnVal)
} }
@Command @Command
fun batch(invoke: Invoke) { fun batch(invoke: Invoke) {
val notificationArray = invoke.getArray("notifications") val args = invoke.parseArgs(BatchArgs::class.java)
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 ids = manager.schedule(args.notifications)
val notification = Notification.fromJson(jsonNotification) notificationStorage.appendNotifications(args.notifications)
notifications.add(notification)
}
val ids = manager.schedule(notifications)
notificationStorage.appendNotifications(notifications)
val result = JSObject() invoke.resolveObject(ids)
result.put("notifications", ids)
invoke.resolve(result)
} }
@Command @Command
fun cancel(invoke: Invoke) { fun cancel(invoke: Invoke) {
val notifications: List<Int> = invoke.getArray("notifications", JSArray()).toList() val args = invoke.parseArgs(CancelArgs::class.java)
if (notifications.isEmpty()) { manager.cancel(args.notifications)
invoke.reject("Must provide notifications array as notifications option")
return
}
manager.cancel(notifications)
invoke.resolve() invoke.resolve()
} }
@Command @Command
fun removeActive(invoke: Invoke) { fun removeActive(invoke: Invoke) {
val notifications = invoke.getArray("notifications") val args = invoke.parseArgs(RemoveActiveArgs::class.java)
if (notifications == null) {
if (args.notifications.isEmpty()) {
notificationManager.cancelAll() notificationManager.cancelAll()
invoke.resolve() invoke.resolve()
} else { } else {
try { for (notification in args.notifications) {
for (o in notifications.toList<Any>()) { if (notification.tag == null) {
if (o is JSONObject) { notificationManager.cancel(notification.id)
val notification = JSObject.fromJSONObject((o)) } else {
val tag = notification.getString("tag", null) notificationManager.cancel(notification.tag, notification.id)
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() invoke.resolve()
} }
@ -162,14 +179,13 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
fun getPending(invoke: Invoke) { fun getPending(invoke: Invoke) {
val notifications= notificationStorage.getSavedNotifications() val notifications= notificationStorage.getSavedNotifications()
val result = Notification.buildNotificationPendingList(notifications) val result = Notification.buildNotificationPendingList(notifications)
invoke.resolve(result) invoke.resolveObject(result)
} }
@Command @Command
fun registerActionTypes(invoke: Invoke) { fun registerActionTypes(invoke: Invoke) {
val types = invoke.getArray("types", JSArray()) val args = invoke.parseArgs(RegisterActionTypesArgs::class.java)
val typesArray = NotificationAction.buildTypes(types) notificationStorage.writeActionGroup(args.types)
notificationStorage.writeActionGroup(typesArray)
invoke.resolve() invoke.resolve()
} }
@ -201,9 +217,8 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
notifications.put(jsNotification) notifications.put(jsNotification)
} }
} }
val result = JSObject()
result.put("notifications", notifications) invoke.resolveObject(notifications)
invoke.resolve(result)
} }
@Command @Command

@ -5,13 +5,26 @@
package app.tauri.notification package app.tauri.notification
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ClipData.Item
import android.text.format.DateUtils import android.text.format.DateUtils
import app.tauri.plugin.JSObject import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.TimeZone import java.util.TimeZone
const val JS_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" const val JS_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
enum class NotificationInterval { enum class NotificationInterval {
@ -33,70 +46,88 @@ fun getIntervalTime(interval: NotificationInterval, count: Int): Long {
} }
} }
sealed class ScheduleKind { @JsonDeserialize(using = NotificationScheduleDeserializer::class)
@JsonSerialize(using = NotificationScheduleSerializer::class)
sealed class NotificationSchedule {
// At specific moment of time (with repeating option) // At specific moment of time (with repeating option)
class At(var date: Date, val repeating: Boolean): ScheduleKind() class At(@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = JS_DATE_FORMAT) var date: Date, val repeating: Boolean = false, val allowWhileIdle: Boolean = false): NotificationSchedule()
class Interval(val interval: DateMatch): ScheduleKind() class Interval(val interval: DateMatch, val allowWhileIdle: Boolean = false): NotificationSchedule()
class Every(val interval: NotificationInterval, val count: Int): ScheduleKind() class Every(val interval: NotificationInterval, val count: Int = 0, val allowWhileIdle: Boolean = false): NotificationSchedule()
fun isRemovable(): Boolean {
return when (this) {
is At -> !repeating
else -> false
}
}
fun allowWhileIdle(): Boolean {
return when (this) {
is At -> allowWhileIdle
is Interval -> allowWhileIdle
is Every -> allowWhileIdle
else -> false
}
}
} }
@SuppressLint("SimpleDateFormat") internal class NotificationScheduleSerializer @JvmOverloads constructor(t: Class<NotificationSchedule>? = null) :
class NotificationSchedule(val scheduleObj: JSObject) { StdSerializer<NotificationSchedule>(t) {
val kind: ScheduleKind @SuppressLint("SimpleDateFormat")
// Schedule this notification to fire even if app is idled (Doze) @Throws(IOException::class, JsonProcessingException::class)
var whileIdle: Boolean = false override fun serialize(
value: NotificationSchedule, jgen: JsonGenerator, provider: SerializerProvider
) {
jgen.writeStartObject()
when (value) {
is NotificationSchedule.At -> {
jgen.writeObjectFieldStart("at")
init { val sdf = SimpleDateFormat(JS_DATE_FORMAT)
val payload = scheduleObj.getJSObject("data", JSObject()) sdf.timeZone = TimeZone.getTimeZone("UTC")
jgen.writeStringField("date", sdf.format(value.date))
jgen.writeBooleanField("repeating", value.repeating)
when (val scheduleKind = scheduleObj.getString("kind", "")) { jgen.writeEndObject()
"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" -> { is NotificationSchedule.Interval -> {
val interval = NotificationInterval.valueOf(payload.getString("interval")) jgen.writeObjectFieldStart("interval")
kind = ScheduleKind.Every(interval, payload.getInteger("count", 1))
jgen.writeObjectField("interval", value.interval)
jgen.writeEndObject()
} }
else -> { is NotificationSchedule.Every -> {
throw Exception("Unknown schedule kind $scheduleKind") jgen.writeObjectFieldStart("every")
jgen.writeObjectField("interval", value.interval)
jgen.writeNumberField("count", value.count)
jgen.writeEndObject()
} }
else -> {}
} }
whileIdle = scheduleObj.getBoolean("allowWhileIdle", false)
}
private fun onFromJson(onJson: JSObject): DateMatch { jgen.writeEndObject()
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 { internal class NotificationScheduleDeserializer: JsonDeserializer<NotificationSchedule>() {
return when (kind) { override fun deserialize(
is ScheduleKind.At -> !kind.repeating jsonParser: JsonParser,
else -> false deserializationContext: DeserializationContext
): NotificationSchedule {
val node: JsonNode = jsonParser.codec.readTree(jsonParser)
node.get("at")?.let {
return jsonParser.codec.treeToValue(it, NotificationSchedule.At::class.java)
}
node.get("interval")?.let {
return jsonParser.codec.treeToValue(it, NotificationSchedule.Interval::class.java)
}
node.get("every")?.let {
return jsonParser.codec.treeToValue(it, NotificationSchedule.Every::class.java)
} }
throw Error("unknown schedule kind $node")
} }
} }

@ -6,23 +6,23 @@ package app.tauri.notification
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import app.tauri.plugin.JSObject import com.fasterxml.jackson.databind.ObjectMapper
import org.json.JSONException import org.json.JSONException
import java.text.ParseException import java.lang.Exception
// Key for private preferences // Key for private preferences
private const val NOTIFICATION_STORE_ID = "NOTIFICATION_STORE" private const val NOTIFICATION_STORE_ID = "NOTIFICATION_STORE"
// Key used to save action types // Key used to save action types
private const val ACTION_TYPES_ID = "ACTION_TYPE_STORE" private const val ACTION_TYPES_ID = "ACTION_TYPE_STORE"
class NotificationStorage(private val context: Context) { class NotificationStorage(private val context: Context, private val jsonMapper: ObjectMapper) {
fun appendNotifications(localNotifications: List<Notification>) { fun appendNotifications(localNotifications: List<Notification>) {
val storage = getStorage(NOTIFICATION_STORE_ID) val storage = getStorage(NOTIFICATION_STORE_ID)
val editor = storage.edit() val editor = storage.edit()
for (request in localNotifications) { for (request in localNotifications) {
if (request.isScheduled) { if (request.isScheduled) {
val key: String = request.id.toString() val key: String = request.id.toString()
editor.putString(key, request.source.toString()) editor.putString(key, request.sourceJson.toString())
} }
} }
editor.apply() editor.apply()
@ -43,57 +43,29 @@ class NotificationStorage(private val context: Context) {
val notifications = ArrayList<Notification>() val notifications = ArrayList<Notification>()
for (key in all.keys) { for (key in all.keys) {
val notificationString = all[key] as String? val notificationString = all[key] as String?
val jsNotification = getNotificationFromJSONString(notificationString) try {
if (jsNotification != null) { val notification = jsonMapper.readValue(notificationString, Notification::class.java)
try { notifications.add(notification)
val notification = } catch (_: Exception) { }
Notification.fromJSObject(jsNotification)
notifications.add(notification)
} catch (_: ParseException) {
}
}
} }
return notifications return notifications
} }
return ArrayList() return ArrayList()
} }
private fun getNotificationFromJSONString(notificationString: String?): JSObject? { fun getSavedNotification(key: String): Notification? {
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 storage = getStorage(NOTIFICATION_STORE_ID)
val notificationString = try { val notificationString = try {
storage.getString(key, null) storage.getString(key, null)
} catch (ex: ClassCastException) { } catch (ex: ClassCastException) {
return null return null
} ?: return null } ?: return null
val jsNotification = try {
JSObject(notificationString)
} catch (ex: JSONException) {
return null
}
return jsNotification
}
fun getSavedNotification(key: String?): Notification? { return try {
val jsNotification = getSavedNotificationAsJSObject(key) ?: return null jsonMapper.readValue(notificationString, Notification::class.java)
val notification = try { } catch (ex: JSONException) {
Notification.fromJSObject(jsNotification) null
} catch (ex: ParseException) {
return null
} }
return notification
} }
fun deleteNotification(id: String?) { fun deleteNotification(id: String?) {
@ -106,15 +78,16 @@ class NotificationStorage(private val context: Context) {
return context.getSharedPreferences(key, Context.MODE_PRIVATE) return context.getSharedPreferences(key, Context.MODE_PRIVATE)
} }
fun writeActionGroup(typesMap: Map<String, List<NotificationAction>>) { fun writeActionGroup(actions: List<ActionType>) {
for ((id, notificationActions) in typesMap) { for (type in actions) {
val editor = getStorage(ACTION_TYPES_ID + id).edit() val i = type.id
val editor = getStorage(ACTION_TYPES_ID + type.id).edit()
editor.clear() editor.clear()
editor.putInt("count", notificationActions.size) editor.putInt("count", type.actions.size)
for (i in notificationActions.indices) { for (action in type.actions) {
editor.putString("id$i", notificationActions[i].id) editor.putString("id$i", action.id)
editor.putString("title$i", notificationActions[i].title) editor.putString("title$i", action.title)
editor.putBoolean("input$i", notificationActions[i].input) editor.putBoolean("input$i", action.input ?: false)
} }
editor.apply() editor.apply()
} }
@ -128,7 +101,12 @@ class NotificationStorage(private val context: Context) {
val id = storage.getString("id$i", "") val id = storage.getString("id$i", "")
val title = storage.getString("title$i", "") val title = storage.getString("title$i", "")
val input = storage.getBoolean("input$i", false) val input = storage.getBoolean("input$i", false)
actions[i] = NotificationAction(id, title, input)
val action = NotificationAction()
action.id = id ?: ""
action.title = title
action.input = input
actions[i] = action
} }
return actions return actions
} }

@ -26,6 +26,7 @@ import androidx.core.app.RemoteInput
import app.tauri.Logger import app.tauri.Logger
import app.tauri.plugin.JSObject import app.tauri.plugin.JSObject
import app.tauri.plugin.PluginManager import app.tauri.plugin.PluginManager
import com.fasterxml.jackson.databind.ObjectMapper
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -44,7 +45,7 @@ class TauriNotificationManager(
private val storage: NotificationStorage, private val storage: NotificationStorage,
private val activity: Activity?, private val activity: Activity?,
private val context: Context, private val context: Context,
private val config: JSObject private val config: PluginConfig?
) { ) {
private var defaultSoundID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE private var defaultSoundID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
private var defaultSmallIconID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE private var defaultSmallIconID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
@ -200,7 +201,7 @@ class TauriNotificationManager(
mBuilder.setOnlyAlertOnce(true) mBuilder.setOnlyAlertOnce(true)
mBuilder.setSmallIcon(notification.getSmallIcon(context, getDefaultSmallIcon(context))) mBuilder.setSmallIcon(notification.getSmallIcon(context, getDefaultSmallIcon(context)))
mBuilder.setLargeIcon(notification.getLargeIcon(context)) mBuilder.setLargeIcon(notification.getLargeIcon(context))
val iconColor = notification.getIconColor(config.getString("iconColor")) val iconColor = notification.getIconColor(config?.iconColor ?: "")
if (iconColor.isNotEmpty()) { if (iconColor.isNotEmpty()) {
try { try {
mBuilder.color = Color.parseColor(iconColor) mBuilder.color = Color.parseColor(iconColor)
@ -216,7 +217,7 @@ class TauriNotificationManager(
} else { } else {
notificationManager.notify(notification.id, buildNotification) notificationManager.notify(notification.id, buildNotification)
try { try {
NotificationPlugin.triggerNotification(notification.source ?: JSObject()) NotificationPlugin.triggerNotification(notification)
} catch (_: JSONException) { } catch (_: JSONException) {
} }
} }
@ -254,7 +255,7 @@ class TauriNotificationManager(
notificationAction.title, notificationAction.title,
actionPendingIntent actionPendingIntent
) )
if (notificationAction.input) { if (notificationAction.input == true) {
val remoteInput = RemoteInput.Builder(REMOTE_INPUT_KEY).setLabel( val remoteInput = RemoteInput.Builder(REMOTE_INPUT_KEY).setLabel(
notificationAction.title notificationAction.title
).build() ).build()
@ -298,7 +299,7 @@ class TauriNotificationManager(
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
intent.putExtra(NOTIFICATION_INTENT_KEY, notification.id) intent.putExtra(NOTIFICATION_INTENT_KEY, notification.id)
intent.putExtra(ACTION_INTENT_KEY, action) intent.putExtra(ACTION_INTENT_KEY, action)
intent.putExtra(NOTIFICATION_OBJ_INTENT_KEY, notification.source.toString()) intent.putExtra(NOTIFICATION_OBJ_INTENT_KEY, notification.sourceJson)
val schedule = notification.schedule val schedule = notification.schedule
intent.putExtra(NOTIFICATION_IS_REMOVABLE_KEY, schedule == null || schedule.isRemovable()) intent.putExtra(NOTIFICATION_IS_REMOVABLE_KEY, schedule == null || schedule.isRemovable())
return intent return intent
@ -326,23 +327,22 @@ class TauriNotificationManager(
var pendingIntent = var pendingIntent =
PendingIntent.getBroadcast(context, request.id, notificationIntent, flags) PendingIntent.getBroadcast(context, request.id, notificationIntent, flags)
when (val scheduleKind = schedule?.kind) { when (schedule) {
is ScheduleKind.At -> { is NotificationSchedule.At -> {
val at = scheduleKind.date if (schedule.date.time < Date().time) {
if (at.time < Date().time) {
Logger.error(Logger.tags("Notification"), "Scheduled time must be *after* current time", null) Logger.error(Logger.tags("Notification"), "Scheduled time must be *after* current time", null)
return return
} }
if (scheduleKind.repeating) { if (schedule.repeating) {
val interval: Long = at.time - Date().time val interval: Long = schedule.date.time - Date().time
alarmManager.setRepeating(AlarmManager.RTC, at.time, interval, pendingIntent) alarmManager.setRepeating(AlarmManager.RTC, schedule.date.time, interval, pendingIntent)
} else { } else {
setExactIfPossible(alarmManager, schedule, at.time, pendingIntent) setExactIfPossible(alarmManager, schedule, schedule.date.time, pendingIntent)
} }
} }
is ScheduleKind.Interval -> { is NotificationSchedule.Interval -> {
val trigger = scheduleKind.interval.nextTrigger(Date()) val trigger = schedule.interval.nextTrigger(Date())
notificationIntent.putExtra(TimedNotificationPublisher.CRON_KEY, scheduleKind.interval.toMatchString()) notificationIntent.putExtra(TimedNotificationPublisher.CRON_KEY, schedule.interval.toMatchString())
pendingIntent = pendingIntent =
PendingIntent.getBroadcast(context, request.id, notificationIntent, flags) PendingIntent.getBroadcast(context, request.id, notificationIntent, flags)
setExactIfPossible(alarmManager, schedule, trigger, pendingIntent) setExactIfPossible(alarmManager, schedule, trigger, pendingIntent)
@ -352,8 +352,8 @@ class TauriNotificationManager(
"notification " + request.id + " will next fire at " + sdf.format(Date(trigger)) "notification " + request.id + " will next fire at " + sdf.format(Date(trigger))
) )
} }
is ScheduleKind.Every -> { is NotificationSchedule.Every -> {
val everyInterval = getIntervalTime(scheduleKind.interval, scheduleKind.count) val everyInterval = getIntervalTime(schedule.interval, schedule.count)
val startTime: Long = Date().time + everyInterval val startTime: Long = Date().time + everyInterval
alarmManager.setRepeating(AlarmManager.RTC, startTime, everyInterval, pendingIntent) alarmManager.setRepeating(AlarmManager.RTC, startTime, everyInterval, pendingIntent)
} }
@ -369,13 +369,13 @@ class TauriNotificationManager(
pendingIntent: PendingIntent pendingIntent: PendingIntent
) { ) {
if (SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) { if (SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
if (SDK_INT >= Build.VERSION_CODES.M && schedule.whileIdle) { if (SDK_INT >= Build.VERSION_CODES.M && schedule.allowWhileIdle()) {
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent) alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent)
} else { } else {
alarmManager[AlarmManager.RTC, trigger] = pendingIntent alarmManager[AlarmManager.RTC, trigger] = pendingIntent
} }
} else { } else {
if (SDK_INT >= Build.VERSION_CODES.M && schedule.whileIdle) { if (SDK_INT >= Build.VERSION_CODES.M && schedule.allowWhileIdle()) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent) alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent)
} else { } else {
alarmManager.setExact(AlarmManager.RTC, trigger, pendingIntent) alarmManager.setExact(AlarmManager.RTC, trigger, pendingIntent)
@ -426,7 +426,7 @@ class TauriNotificationManager(
private fun getDefaultSound(context: Context): Int { private fun getDefaultSound(context: Context): Int {
if (defaultSoundID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSoundID if (defaultSoundID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSoundID
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
val soundConfigResourceName = AssetUtils.getResourceBaseName(config.getString("sound")) val soundConfigResourceName = AssetUtils.getResourceBaseName(config?.sound)
if (soundConfigResourceName != null) { if (soundConfigResourceName != null) {
resId = AssetUtils.getResourceID(context, soundConfigResourceName, "raw") resId = AssetUtils.getResourceID(context, soundConfigResourceName, "raw")
} }
@ -437,7 +437,7 @@ class TauriNotificationManager(
private fun getDefaultSmallIcon(context: Context): Int { private fun getDefaultSmallIcon(context: Context): Int {
if (defaultSmallIconID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSmallIconID if (defaultSmallIconID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSmallIconID
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
val smallIconConfigResourceName = AssetUtils.getResourceBaseName(config.getString("icon")) val smallIconConfigResourceName = AssetUtils.getResourceBaseName(config?.icon)
if (smallIconConfigResourceName != null) { if (smallIconConfigResourceName != null) {
resId = AssetUtils.getResourceID(context, smallIconConfigResourceName, "drawable") resId = AssetUtils.getResourceID(context, smallIconConfigResourceName, "drawable")
} }
@ -460,7 +460,7 @@ class NotificationDismissReceiver : BroadcastReceiver() {
val isRemovable = val isRemovable =
intent.getBooleanExtra(NOTIFICATION_IS_REMOVABLE_KEY, true) intent.getBooleanExtra(NOTIFICATION_IS_REMOVABLE_KEY, true)
if (isRemovable) { if (isRemovable) {
val notificationStorage = NotificationStorage(context) val notificationStorage = NotificationStorage(context, ObjectMapper())
notificationStorage.deleteNotification(intExtra.toString()) notificationStorage.deleteNotification(intExtra.toString())
} }
} }
@ -486,11 +486,13 @@ class TimedNotificationPublisher : BroadcastReceiver() {
if (id == Int.MIN_VALUE) { if (id == Int.MIN_VALUE) {
Logger.error(Logger.tags("Notification"), "No valid id supplied", null) Logger.error(Logger.tags("Notification"), "No valid id supplied", null)
} }
val storage = NotificationStorage(context) val storage = NotificationStorage(context, ObjectMapper())
val notificationJson = storage.getSavedNotificationAsJSObject(id.toString())
if (notificationJson != null) { val savedNotification = storage.getSavedNotification(id.toString())
NotificationPlugin.triggerNotification(notificationJson) if (savedNotification != null) {
NotificationPlugin.triggerNotification(savedNotification)
} }
notificationManager.notify(id, notification) notificationManager.notify(id, notification)
if (!rescheduleNotificationIfNeeded(context, intent, id)) { if (!rescheduleNotificationIfNeeded(context, intent, id)) {
storage.deleteNotification(id.toString()) storage.deleteNotification(id.toString())
@ -545,19 +547,19 @@ class LocalNotificationRestoreReceiver : BroadcastReceiver() {
) )
if (um == null || !um.isUserUnlocked) return if (um == null || !um.isUserUnlocked) return
} }
val storage = NotificationStorage(context) val storage = NotificationStorage(context, ObjectMapper())
val ids = storage.getSavedNotificationIds() val ids = storage.getSavedNotificationIds()
val notifications = mutableListOf<Notification>() val notifications = mutableListOf<Notification>()
val updatedNotifications = mutableListOf<Notification>() val updatedNotifications = mutableListOf<Notification>()
for (id in ids) { for (id in ids) {
val notification = storage.getSavedNotification(id) ?: continue val notification = storage.getSavedNotification(id) ?: continue
val schedule = notification.schedule val schedule = notification.schedule
if (schedule != null && schedule.kind is ScheduleKind.At) { if (schedule != null && schedule is NotificationSchedule.At) {
val at: Date = schedule.kind.date val at: Date = schedule.date
if (at.before(Date())) { if (at.before(Date())) {
// modify the scheduled date in order to show notifications that would have been delivered while device was off. // modify the scheduled date in order to show notifications that would have been delivered while device was off.
val newDateTime = Date().time + 15 * 1000 val newDateTime = Date().time + 15 * 1000
schedule.kind.date = Date(newDateTime) schedule.date = Date(newDateTime)
updatedNotifications.add(notification) updatedNotifications.add(notification)
} }
} }
@ -567,7 +569,13 @@ class LocalNotificationRestoreReceiver : BroadcastReceiver() {
storage.appendNotifications(updatedNotifications) storage.appendNotifications(updatedNotifications)
} }
val notificationManager = TauriNotificationManager(storage, null, context, PluginManager.loadConfig(context, "notification")) var config: PluginConfig? = null
try {
config = PluginManager.loadConfig(context, "notification", PluginConfig::class.java)
} catch (ex: Exception) {
ex.printStackTrace()
}
val notificationManager = TauriNotificationManager(storage, null, context, config)
notificationManager.schedule(notifications) notificationManager.schedule(notifications)
} }
} }

@ -10,7 +10,7 @@ fn main() {
{ {
println!("{error:#}"); println!("{error:#}");
// when building documentation for Android the plugin build result is irrelevant to the crate itself // when building documentation for Android the plugin build result is irrelevant to the crate itself
if !(cfg!(feature = "dox") && std::env::var("TARGET").unwrap().contains("android")) { if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) {
std::process::exit(1); std::process::exit(1);
} }
} }

@ -150,57 +150,62 @@ type ScheduleInterval = {
}; };
enum ScheduleEvery { enum ScheduleEvery {
Year = "Year", Year = "year",
Month = "Month", Month = "month",
TwoWeeks = "TwoWeeks", TwoWeeks = "twoWeeks",
Week = "Week", Week = "week",
Day = "Day", Day = "day",
Hour = "Hour", Hour = "hour",
Minute = "Minute", Minute = "minute",
/** /**
* Not supported on iOS. * Not supported on iOS.
*/ */
Second = "Second", Second = "second",
} }
type ScheduleData = type ScheduleData =
| { | {
kind: "At"; at: {
data: {
date: Date; date: Date;
repeating: boolean; repeating: boolean;
allowWhileIdle: boolean;
}; };
} }
| { | {
kind: "Interval"; interval: {
data: ScheduleInterval; interval: ScheduleInterval;
allowWhileIdle: boolean;
};
} }
| { | {
kind: "Every"; every: {
data: {
interval: ScheduleEvery; interval: ScheduleEvery;
count: number;
allowWhileIdle: boolean;
}; };
}; };
class Schedule { class Schedule {
kind: string; schedule: ScheduleData;
data: unknown;
private constructor(schedule: ScheduleData) { private constructor(schedule: ScheduleData) {
this.kind = schedule.kind; this.schedule = schedule;
this.data = schedule.data; }
toJSON(): string {
return JSON.stringify(this.schedule);
} }
static at(date: Date, repeating = false) { static at(date: Date, repeating = false, allowWhileIdle = false) {
return new Schedule({ kind: "At", data: { date, repeating } }); return new Schedule({ at: { date, repeating, allowWhileIdle } });
} }
static interval(interval: ScheduleInterval) { static interval(interval: ScheduleInterval, allowWhileIdle = false) {
return new Schedule({ kind: "Interval", data: interval }); return new Schedule({ interval: { interval, allowWhileIdle } });
} }
static every(kind: ScheduleEvery) { static every(kind: ScheduleEvery, count: number, allowWhileIdle = false) {
return new Schedule({ kind: "Every", data: { interval: kind } }); return new Schedule({ every: { interval: kind, count, allowWhileIdle } });
} }
} }
@ -461,7 +466,9 @@ async function active(): Promise<ActiveNotification[]> {
* *
* @since 2.0.0 * @since 2.0.0
*/ */
async function removeActive(notifications: number[]): Promise<void> { async function removeActive(
notifications: { id: number; tag?: string }[],
): Promise<void> {
return invoke("plugin:notification|remove_active", { notifications }); return invoke("plugin:notification|remove_active", { notifications });
} }
@ -483,7 +490,7 @@ async function removeAllActive(): Promise<void> {
} }
/** /**
* Removes all active notifications. * Creates a notification channel.
* *
* @example * @example
* ```typescript * ```typescript
@ -537,7 +544,7 @@ async function removeChannel(id: string): Promise<void> {
* @since 2.0.0 * @since 2.0.0
*/ */
async function channels(): Promise<Channel[]> { async function channels(): Promise<Channel[]> {
return invoke("plugin:notification|getActive"); return invoke("plugin:notification|listChannels");
} }
async function onNotificationReceived( async function onNotificationReceived(

@ -1,4 +1,4 @@
// swift-tools-version:5.3 // swift-tools-version:5.5
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT

@ -6,12 +6,7 @@ import Tauri
import UserNotifications import UserNotifications
enum NotificationError: LocalizedError { enum NotificationError: LocalizedError {
case contentNoId
case contentNoTitle
case contentNoBody
case triggerRepeatIntervalTooShort case triggerRepeatIntervalTooShort
case attachmentNoId
case attachmentNoUrl
case attachmentFileNotFound(path: String) case attachmentFileNotFound(path: String)
case attachmentUnableToCreate(String) case attachmentUnableToCreate(String)
case pastScheduledTime case pastScheduledTime
@ -19,18 +14,8 @@ enum NotificationError: LocalizedError {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .contentNoId:
return "Missing notification identifier"
case .contentNoTitle:
return "Missing notification title"
case .contentNoBody:
return "Missing notification body"
case .triggerRepeatIntervalTooShort: case .triggerRepeatIntervalTooShort:
return "Schedule interval too short, must be a least 1 minute" 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): case .attachmentFileNotFound(let path):
return "Unable to find file \(path) for attachment" return "Unable to find file \(path) for attachment"
case .attachmentUnableToCreate(let error): case .attachmentUnableToCreate(let error):
@ -43,69 +28,56 @@ enum NotificationError: LocalizedError {
} }
} }
func makeNotificationContent(_ notification: JSObject) throws -> UNNotificationContent { func makeNotificationContent(_ notification: Notification) 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() let content = UNMutableNotificationContent()
content.title = NSString.localizedUserNotificationString(forKey: title, arguments: nil) content.title = NSString.localizedUserNotificationString(
forKey: notification.title, arguments: nil)
content.body = NSString.localizedUserNotificationString( content.body = NSString.localizedUserNotificationString(
forKey: body, forKey: notification.body,
arguments: nil) arguments: nil)
content.userInfo = [ content.userInfo = [
"__EXTRA__": extra, "__EXTRA__": notification.extra as Any,
"__SCHEDULE__": schedule, "__SCHEDULE__": notification.schedule as Any,
] ]
if let actionTypeId = notification["actionTypeId"] as? String { if let actionTypeId = notification.actionTypeId {
content.categoryIdentifier = actionTypeId content.categoryIdentifier = actionTypeId
} }
if let threadIdentifier = notification["group"] as? String { if let threadIdentifier = notification.group {
content.threadIdentifier = threadIdentifier content.threadIdentifier = threadIdentifier
} }
if let summaryArgument = notification["summary"] as? String { if let summaryArgument = notification.summary {
content.summaryArgument = summaryArgument content.summaryArgument = summaryArgument
} }
if let sound = notification["sound"] as? String { if let sound = notification.sound {
content.sound = UNNotificationSound(named: UNNotificationSoundName(sound)) content.sound = UNNotificationSound(named: UNNotificationSoundName(sound))
} }
if let attachments = notification["attachments"] as? [JSObject] { if let attachments = notification.attachments {
content.attachments = try makeAttachments(attachments) content.attachments = try makeAttachments(attachments)
} }
return content return content
} }
func makeAttachments(_ attachments: [JSObject]) throws -> [UNNotificationAttachment] { func makeAttachments(_ attachments: [NotificationAttachment]) throws -> [UNNotificationAttachment] {
var createdAttachments = [UNNotificationAttachment]() var createdAttachments = [UNNotificationAttachment]()
for attachment in attachments { for attachment in attachments {
guard let id = attachment["id"] as? String else {
throw NotificationError.attachmentNoId guard let urlObject = makeAttachmentUrl(attachment.url) else {
} throw NotificationError.attachmentFileNotFound(path: attachment.url)
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 ?? [:] let options = attachment.options != nil ? makeAttachmentOptions(attachment.options!) : nil
do { do {
let newAttachment = try UNNotificationAttachment( let newAttachment = try UNNotificationAttachment(
identifier: id, url: urlObject, options: makeAttachmentOptions(options)) identifier: attachment.id, url: urlObject, options: options)
createdAttachments.append(newAttachment) createdAttachments.append(newAttachment)
} catch { } catch {
throw NotificationError.attachmentUnableToCreate(error.localizedDescription) throw NotificationError.attachmentUnableToCreate(error.localizedDescription)
@ -119,50 +91,37 @@ func makeAttachmentUrl(_ path: String) -> URL? {
return URL(string: path) return URL(string: path)
} }
func makeAttachmentOptions(_ options: JSObject) -> JSObject { func makeAttachmentOptions(_ options: NotificationAttachmentOptions) -> [AnyHashable: Any] {
var opts: JSObject = [:] var opts: [AnyHashable: Any] = [:]
if let iosUNNotificationAttachmentOptionsTypeHintKey = options[ if let value = options.iosUNNotificationAttachmentOptionsTypeHintKey {
"iosUNNotificationAttachmentOptionsTypeHintKey"] as? String opts[UNNotificationAttachmentOptionsTypeHintKey] = value
{
opts[UNNotificationAttachmentOptionsTypeHintKey] = iosUNNotificationAttachmentOptionsTypeHintKey
} }
if let iosUNNotificationAttachmentOptionsThumbnailHiddenKey = options[ if let value = options.iosUNNotificationAttachmentOptionsThumbnailHiddenKey {
"iosUNNotificationAttachmentOptionsThumbnailHiddenKey"] as? String opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] = value
{
opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] =
iosUNNotificationAttachmentOptionsThumbnailHiddenKey
} }
if let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey = options[ if let value = options.iosUNNotificationAttachmentOptionsThumbnailClippingRectKey {
"iosUNNotificationAttachmentOptionsThumbnailClippingRectKey"] as? String opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] = value
{
opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] =
iosUNNotificationAttachmentOptionsThumbnailClippingRectKey
} }
if let iosUNNotificationAttachmentOptionsThumbnailTimeKey = options[ if let value = options
"iosUNNotificationAttachmentOptionsThumbnailTimeKey"] as? String .iosUNNotificationAttachmentOptionsThumbnailTimeKey
{ {
opts[UNNotificationAttachmentOptionsThumbnailTimeKey] = opts[UNNotificationAttachmentOptionsThumbnailTimeKey] = value
iosUNNotificationAttachmentOptionsThumbnailTimeKey
} }
return opts return opts
} }
func handleScheduledNotification(_ schedule: JSObject) throws func handleScheduledNotification(_ schedule: NotificationSchedule) throws
-> UNNotificationTrigger? -> UNNotificationTrigger?
{ {
let kind = schedule["kind"] as? String ?? "" switch schedule {
let payload = schedule["data"] as? JSObject ?? [:] case .at(let date, let repeating):
switch kind {
case "At":
let date = payload["date"] as? String ?? ""
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
if let at = dateFormatter.date(from: date) { if let at = dateFormatter.date(from: date) {
let repeats = payload["repeats"] as? Bool ?? false
let dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: at) let dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: at)
if dateInfo.date! < Date() { if dateInfo.date! < Date() {
@ -172,23 +131,20 @@ func handleScheduledNotification(_ schedule: JSObject) throws
let dateInterval = DateInterval(start: Date(), end: dateInfo.date!) let dateInterval = DateInterval(start: Date(), end: dateInfo.date!)
// Notifications that repeat have to be at least a minute between each other // Notifications that repeat have to be at least a minute between each other
if repeats && dateInterval.duration < 60 { if repeating && dateInterval.duration < 60 {
throw NotificationError.triggerRepeatIntervalTooShort throw NotificationError.triggerRepeatIntervalTooShort
} }
return UNTimeIntervalNotificationTrigger( return UNTimeIntervalNotificationTrigger(
timeInterval: dateInterval.duration, repeats: repeats) timeInterval: dateInterval.duration, repeats: repeating)
} else { } else {
throw NotificationError.invalidDate(date) throw NotificationError.invalidDate(date)
} }
case "Interval": case .interval(let interval):
let dateComponents = getDateComponents(payload) let dateComponents = getDateComponents(interval)
return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
case "Every": case .every(let interval, let count):
let interval = payload["interval"] as? String ?? ""
let count = schedule["count"] as? Int ?? 1
if let repeatDateInterval = getRepeatDateInterval(interval, count) { if let repeatDateInterval = getRepeatDateInterval(interval, count) {
// Notifications that repeat have to be at least a minute between each other // Notifications that repeat have to be at least a minute between each other
if repeatDateInterval.duration < 60 { if repeatDateInterval.duration < 60 {
@ -198,9 +154,6 @@ func handleScheduledNotification(_ schedule: JSObject) throws
return UNTimeIntervalNotificationTrigger( return UNTimeIntervalNotificationTrigger(
timeInterval: repeatDateInterval.duration, repeats: true) timeInterval: repeatDateInterval.duration, repeats: true)
} }
default:
return nil
} }
return nil return nil
@ -209,30 +162,30 @@ func handleScheduledNotification(_ schedule: JSObject) throws
/// Given our schedule format, return a DateComponents object /// Given our schedule format, return a DateComponents object
/// that only contains the components passed in. /// that only contains the components passed in.
func getDateComponents(_ at: JSObject) -> DateComponents { func getDateComponents(_ at: ScheduleInterval) -> DateComponents {
// var dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: Date()) // var dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: Date())
// dateInfo.calendar = Calendar.current // dateInfo.calendar = Calendar.current
var dateInfo = DateComponents() var dateInfo = DateComponents()
if let year = at["year"] as? Int { if let year = at.year {
dateInfo.year = year dateInfo.year = year
} }
if let month = at["month"] as? Int { if let month = at.month {
dateInfo.month = month dateInfo.month = month
} }
if let day = at["day"] as? Int { if let day = at.day {
dateInfo.day = day dateInfo.day = day
} }
if let hour = at["hour"] as? Int { if let hour = at.hour {
dateInfo.hour = hour dateInfo.hour = hour
} }
if let minute = at["minute"] as? Int { if let minute = at.minute {
dateInfo.minute = minute dateInfo.minute = minute
} }
if let second = at["second"] as? Int { if let second = at.second {
dateInfo.second = second dateInfo.second = second
} }
if let weekday = at["weekday"] as? Int { if let weekday = at.weekday {
dateInfo.weekday = weekday dateInfo.weekday = weekday
} }
return dateInfo return dateInfo
@ -242,35 +195,33 @@ func getDateComponents(_ at: JSObject) -> DateComponents {
/// interval and today. For example, if every is "month", then we /// interval and today. For example, if every is "month", then we
/// return the interval between today and a month from today. /// return the interval between today and a month from today.
func getRepeatDateInterval(_ every: String, _ count: Int) -> DateInterval? { func getRepeatDateInterval(_ every: ScheduleEveryKind, _ count: Int) -> DateInterval? {
let cal = Calendar.current let cal = Calendar.current
let now = Date() let now = Date()
switch every { switch every {
case "Year": case .year:
let newDate = cal.date(byAdding: .year, value: count, to: now)! let newDate = cal.date(byAdding: .year, value: count, to: now)!
return DateInterval(start: now, end: newDate) return DateInterval(start: now, end: newDate)
case "Month": case .month:
let newDate = cal.date(byAdding: .month, value: count, to: now)! let newDate = cal.date(byAdding: .month, value: count, to: now)!
return DateInterval(start: now, end: newDate) return DateInterval(start: now, end: newDate)
case "TwoWeeks": case .twoWeeks:
let newDate = cal.date(byAdding: .weekOfYear, value: 2 * count, to: now)! let newDate = cal.date(byAdding: .weekOfYear, value: 2 * count, to: now)!
return DateInterval(start: now, end: newDate) return DateInterval(start: now, end: newDate)
case "Week": case .week:
let newDate = cal.date(byAdding: .weekOfYear, value: count, to: now)! let newDate = cal.date(byAdding: .weekOfYear, value: count, to: now)!
return DateInterval(start: now, end: newDate) return DateInterval(start: now, end: newDate)
case "Day": case .day:
let newDate = cal.date(byAdding: .day, value: count, to: now)! let newDate = cal.date(byAdding: .day, value: count, to: now)!
return DateInterval(start: now, end: newDate) return DateInterval(start: now, end: newDate)
case "Hour": case .hour:
let newDate = cal.date(byAdding: .hour, value: count, to: now)! let newDate = cal.date(byAdding: .hour, value: count, to: now)!
return DateInterval(start: now, end: newDate) return DateInterval(start: now, end: newDate)
case "Minute": case .minute:
let newDate = cal.date(byAdding: .minute, value: count, to: now)! let newDate = cal.date(byAdding: .minute, value: count, to: now)!
return DateInterval(start: now, end: newDate) return DateInterval(start: now, end: newDate)
case "Second": case .second:
let newDate = cal.date(byAdding: .second, value: count, to: now)! let newDate = cal.date(byAdding: .second, value: count, to: now)!
return DateInterval(start: now, end: newDate) return DateInterval(start: now, end: newDate)
default:
return nil
} }
} }

@ -5,21 +5,7 @@
import Tauri import Tauri
import UserNotifications import UserNotifications
enum CategoryError: LocalizedError { internal func makeCategories(_ actionTypes: [ActionType]) {
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]() var createdCategories = [UNNotificationCategory]()
let generalCategory = UNNotificationCategory( let generalCategory = UNNotificationCategory(
@ -30,22 +16,16 @@ public func makeCategories(_ actionTypes: [JSObject]) throws {
createdCategories.append(generalCategory) createdCategories.append(generalCategory)
for type in actionTypes { for type in actionTypes {
guard let id = type["id"] as? String else { let newActions = makeActions(type.actions)
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. // Create the custom actions for the TIMER_EXPIRED category.
var newCategory: UNNotificationCategory? var newCategory: UNNotificationCategory?
newCategory = UNNotificationCategory( newCategory = UNNotificationCategory(
identifier: id, identifier: type.id,
actions: newActions, actions: newActions,
intentIdentifiers: [], intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: hiddenBodyPlaceholder, hiddenPreviewsBodyPlaceholder: type.hiddenBodyPlaceholder ?? "",
options: makeCategoryOptions(type)) options: makeCategoryOptions(type))
createdCategories.append(newCategory!) createdCategories.append(newCategory!)
@ -55,37 +35,28 @@ public func makeCategories(_ actionTypes: [JSObject]) throws {
center.setNotificationCategories(Set(createdCategories)) center.setNotificationCategories(Set(createdCategories))
} }
func makeActions(_ actions: [JSObject]) throws -> [UNNotificationAction] { func makeActions(_ actions: [Action]) -> [UNNotificationAction] {
var createdActions = [UNNotificationAction]() var createdActions = [UNNotificationAction]()
for action in actions { 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 var newAction: UNNotificationAction
if input { if action.input {
let inputButtonTitle = action["inputButtonTitle"] as? String if action.inputButtonTitle != nil {
let inputPlaceholder = action["inputPlaceholder"] as? String ?? ""
if inputButtonTitle != nil {
newAction = UNTextInputNotificationAction( newAction = UNTextInputNotificationAction(
identifier: id, identifier: action.id,
title: title, title: action.title,
options: makeActionOptions(action), options: makeActionOptions(action),
textInputButtonTitle: inputButtonTitle!, textInputButtonTitle: action.inputButtonTitle ?? "",
textInputPlaceholder: inputPlaceholder) textInputPlaceholder: action.inputPlaceholder ?? "")
} else { } else {
newAction = UNTextInputNotificationAction( newAction = UNTextInputNotificationAction(
identifier: id, title: title, options: makeActionOptions(action)) identifier: action.id, title: action.title, options: makeActionOptions(action))
} }
} else { } else {
// Create the custom actions for the TIMER_EXPIRED category. // Create the custom actions for the TIMER_EXPIRED category.
newAction = UNNotificationAction( newAction = UNNotificationAction(
identifier: id, identifier: action.id,
title: title, title: action.title,
options: makeActionOptions(action)) options: makeActionOptions(action))
} }
createdActions.append(newAction) createdActions.append(newAction)
@ -94,40 +65,31 @@ func makeActions(_ actions: [JSObject]) throws -> [UNNotificationAction] {
return createdActions return createdActions
} }
func makeActionOptions(_ action: JSObject) -> UNNotificationActionOptions { func makeActionOptions(_ action: Action) -> UNNotificationActionOptions {
let foreground = action["foreground"] as? Bool ?? false if action.foreground {
let destructive = action["destructive"] as? Bool ?? false
let requiresAuthentication = action["requiresAuthentication"] as? Bool ?? false
if foreground {
return .foreground return .foreground
} }
if destructive { if action.destructive {
return .destructive return .destructive
} }
if requiresAuthentication { if action.requiresAuthentication {
return .authenticationRequired return .authenticationRequired
} }
return UNNotificationActionOptions(rawValue: 0) return UNNotificationActionOptions(rawValue: 0)
} }
func makeCategoryOptions(_ type: JSObject) -> UNNotificationCategoryOptions { func makeCategoryOptions(_ type: ActionType) -> UNNotificationCategoryOptions {
let customDismiss = type["customDismissAction"] as? Bool ?? false if type.customDismissAction {
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 return .customDismissAction
} }
if carPlay { if type.allowInCarPlay {
return .allowInCarPlay return .allowInCarPlay
} }
if hiddenPreviewsShowTitle { if type.hiddenPreviewsShowTitle {
return .hiddenPreviewsShowTitle return .hiddenPreviewsShowTitle
} }
if hiddenPreviewsShowSubtitle { if type.hiddenPreviewsShowSubtitle {
return .hiddenPreviewsShowSubtitle return .hiddenPreviewsShowSubtitle
} }

@ -9,9 +9,9 @@ public class NotificationHandler: NSObject, NotificationHandlerProtocol {
public weak var plugin: Plugin? public weak var plugin: Plugin?
private var notificationsMap = [String: JSObject]() private var notificationsMap = [String: Notification]()
public func saveNotification(_ key: String, _ notification: JSObject) { internal func saveNotification(_ key: String, _ notification: Notification) {
notificationsMap.updateValue(notification, forKey: key) notificationsMap.updateValue(notification, forKey: key)
} }
@ -30,12 +30,11 @@ public class NotificationHandler: NSObject, NotificationHandlerProtocol {
} }
public func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions { public func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions {
let notificationData = makeNotificationRequestJSObject(notification.request) let notificationData = toActiveNotification(notification.request)
self.plugin?.trigger("notification", data: notificationData) try? self.plugin?.trigger("notification", data: notificationData)
if let options = notificationsMap[notification.request.identifier] { if let options = notificationsMap[notification.request.identifier] {
let silent = options["silent"] as? Bool ?? false if options.silent {
if silent {
return UNNotificationPresentationOptions.init(rawValue: 0) return UNNotificationPresentationOptions.init(rawValue: 0)
} }
} }
@ -48,73 +47,72 @@ public class NotificationHandler: NSObject, NotificationHandlerProtocol {
} }
public func didReceive(response: UNNotificationResponse) { public func didReceive(response: UNNotificationResponse) {
var data = JSObject()
let originalNotificationRequest = response.notification.request let originalNotificationRequest = response.notification.request
let actionId = response.actionIdentifier let actionId = response.actionIdentifier
var actionIdValue: String
// We turn the two default actions (open/dismiss) into generic strings // We turn the two default actions (open/dismiss) into generic strings
if actionId == UNNotificationDefaultActionIdentifier { if actionId == UNNotificationDefaultActionIdentifier {
data["actionId"] = "tap" actionIdValue = "tap"
} else if actionId == UNNotificationDismissActionIdentifier { } else if actionId == UNNotificationDismissActionIdentifier {
data["actionId"] = "dismiss" actionIdValue = "dismiss"
} else { } else {
data["actionId"] = actionId actionIdValue = actionId
} }
var inputValue: String? = nil
// If the type of action was for an input type, get the value // If the type of action was for an input type, get the value
if let inputType = response as? UNTextInputNotificationResponse { if let inputType = response as? UNTextInputNotificationResponse {
data["inputValue"] = inputType.userText inputValue = inputType.userText
} }
data["notification"] = makeNotificationRequestJSObject(originalNotificationRequest) try? self.plugin?.trigger(
"actionPerformed",
self.plugin?.trigger("actionPerformed", data: data) data: ReceivedNotification(
actionId: actionIdValue,
inputValue: inputValue,
notification: toActiveNotification(originalNotificationRequest)
))
} }
/** func toActiveNotification(_ request: UNNotificationRequest) -> ActiveNotification {
* Turn a UNNotificationRequest into a JSObject to return back to the client. let notificationRequest = notificationsMap[request.identifier]!
*/ return ActiveNotification(
func makeNotificationRequestJSObject(_ request: UNNotificationRequest) -> JSObject { id: Int(request.identifier) ?? -1,
let notificationRequest = notificationsMap[request.identifier] ?? [:] title: request.content.title,
var notification = makePendingNotificationRequestJSObject(request) body: request.content.body,
notification["sound"] = notificationRequest["sound"] ?? "" sound: notificationRequest.sound ?? "",
notification["actionTypeId"] = request.content.categoryIdentifier actionTypeId: request.content.categoryIdentifier,
notification["attachments"] = notificationRequest["attachments"] ?? [JSObject]() attachments: notificationRequest.attachments
return notification )
} }
func makePendingNotificationRequestJSObject(_ request: UNNotificationRequest) -> JSObject { func toPendingNotification(_ request: UNNotificationRequest) -> PendingNotification {
var notification: JSObject = [ return PendingNotification(
"id": Int(request.identifier) ?? -1, id: Int(request.identifier) ?? -1,
"title": request.content.title, title: request.content.title,
"body": request.content.body, 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 { struct PendingNotification: Encodable {
// convert schedule at date to string let id: Int
if let date = schedule["at"] as? Date { let title: String
let dateString = ISO8601DateFormatter().string(from: date) let body: String
schedule["at"] = dateString }
}
notification["schedule"] = schedule struct ActiveNotification: Encodable {
} let id: Int
} let title: String
let body: String
let sound: String
let actionTypeId: String
let attachments: [NotificationAttachment]?
}
return notification struct ReceivedNotification: Encodable {
} let actionId: String
let inputValue: String?
let notification: ActiveNotification
} }

@ -19,9 +19,11 @@ import UserNotifications
center.delegate = self center.delegate = self
} }
public func userNotificationCenter(_ center: UNUserNotificationCenter, public func userNotificationCenter(
willPresent notification: UNNotification, _ center: UNUserNotificationCenter,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
var presentationOptions: UNNotificationPresentationOptions? = nil var presentationOptions: UNNotificationPresentationOptions? = nil
if notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true { if notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true {
@ -31,9 +33,11 @@ import UserNotifications
completionHandler(presentationOptions ?? []) completionHandler(presentationOptions ?? [])
} }
public func userNotificationCenter(_ center: UNUserNotificationCenter, public func userNotificationCenter(
didReceive response: UNNotificationResponse, _ center: UNUserNotificationCenter,
withCompletionHandler completionHandler: @escaping () -> Void) { didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
if response.notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true { if response.notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true {
notificationHandler?.didReceive(response: response) notificationHandler?.didReceive(response: response)
} }

@ -9,14 +9,11 @@ import UserNotifications
import WebKit import WebKit
enum ShowNotificationError: LocalizedError { enum ShowNotificationError: LocalizedError {
case noId
case make(Error) case make(Error)
case create(Error) case create(Error)
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .noId:
return "notification `id` missing"
case .make(let error): case .make(let error):
return "Unable to make notification: \(error)" return "Unable to make notification: \(error)"
case .create(let error): case .create(let error):
@ -25,13 +22,71 @@ enum ShowNotificationError: LocalizedError {
} }
} }
func showNotification(invoke: Invoke, notification: JSObject) enum ScheduleEveryKind: String, Decodable {
case year
case month
case twoWeeks
case week
case day
case hour
case minute
case second
}
struct ScheduleInterval: Decodable {
let year: Int?
let month: Int?
let day: Int?
let weekday: Int?
let hour: Int?
let minute: Int?
let second: Int?
}
enum NotificationSchedule: Decodable {
case at(date: String, repeating: Bool)
case interval(interval: ScheduleInterval)
case every(interval: ScheduleEveryKind, count: Int)
}
struct NotificationAttachmentOptions: Codable {
let iosUNNotificationAttachmentOptionsTypeHintKey: String?
let iosUNNotificationAttachmentOptionsThumbnailHiddenKey: String?
let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey: String?
let iosUNNotificationAttachmentOptionsThumbnailTimeKey: String?
}
struct NotificationAttachment: Codable {
let id: String
let url: String
let options: NotificationAttachmentOptions?
}
struct Notification: Decodable {
let id: Int
var title: String = ""
var body: String = ""
var extra: [String: String] = [:]
let schedule: NotificationSchedule?
let attachments: [NotificationAttachment]?
let sound: String?
let group: String?
let actionTypeId: String?
let summary: String?
var silent = false
}
struct RemoveActiveNotification: Decodable {
let id: Int
}
struct RemoveActiveArgs: Decodable {
let notifications: [RemoveActiveNotification]
}
func showNotification(invoke: Invoke, notification: Notification)
throws -> UNNotificationRequest throws -> UNNotificationRequest
{ {
guard let identifier = notification["id"] as? Int else {
throw ShowNotificationError.noId
}
var content: UNNotificationContent var content: UNNotificationContent
do { do {
content = try makeNotificationContent(notification) content = try makeNotificationContent(notification)
@ -42,7 +97,7 @@ func showNotification(invoke: Invoke, notification: JSObject)
var trigger: UNNotificationTrigger? var trigger: UNNotificationTrigger?
do { do {
if let schedule = notification["schedule"] as? JSObject { if let schedule = notification.schedule {
try trigger = handleScheduledNotification(schedule) try trigger = handleScheduledNotification(schedule)
} }
} catch { } catch {
@ -51,7 +106,7 @@ func showNotification(invoke: Invoke, notification: JSObject)
// Schedule the request. // Schedule the request.
let request = UNNotificationRequest( let request = UNNotificationRequest(
identifier: "\(identifier)", content: content, trigger: trigger identifier: "\(notification.id)", content: content, trigger: trigger
) )
let center = UNUserNotificationCenter.current() let center = UNUserNotificationCenter.current()
@ -64,6 +119,40 @@ func showNotification(invoke: Invoke, notification: JSObject)
return request return request
} }
struct CancelArgs: Decodable {
let notifications: [Int]
}
struct Action: Decodable {
let id: String
let title: String
var requiresAuthentication: Bool = false
var foreground: Bool = false
var destructive: Bool = false
var input: Bool = false
let inputButtonTitle: String?
let inputPlaceholder: String?
}
struct ActionType: Decodable {
let id: String
let actions: [Action]
let hiddenPreviewsBodyPlaceholder: String?
var customDismissAction = false
var allowInCarPlay = false
var hiddenPreviewsShowTitle = false
var hiddenPreviewsShowSubtitle = false
let hiddenBodyPlaceholder: String?
}
struct RegisterActionTypesArgs: Decodable {
let types: [ActionType]
}
struct BatchArgs: Decodable {
let notifications: [Notification]
}
class NotificationPlugin: Plugin { class NotificationPlugin: Plugin {
let notificationHandler = NotificationHandler() let notificationHandler = NotificationHandler()
let notificationManager = NotificationManager() let notificationManager = NotificationManager()
@ -75,29 +164,24 @@ class NotificationPlugin: Plugin {
} }
@objc public func show(_ invoke: Invoke) throws { @objc public func show(_ invoke: Invoke) throws {
let request = try showNotification(invoke: invoke, notification: invoke.data) let notification = try invoke.parseArgs(Notification.self)
notificationHandler.saveNotification(request.identifier, invoke.data)
invoke.resolve([ let request = try showNotification(invoke: invoke, notification: notification)
"id": Int(request.identifier) ?? -1 notificationHandler.saveNotification(request.identifier, notification)
]) invoke.resolve(Int(request.identifier) ?? -1)
} }
@objc public func batch(_ invoke: Invoke) throws { @objc public func batch(_ invoke: Invoke) throws {
guard let notifications = invoke.getArray("notifications", JSObject.self) else { let args = try invoke.parseArgs(BatchArgs.self)
invoke.reject("`notifications` array is required")
return
}
var ids = [Int]() var ids = [Int]()
for notification in notifications { for notification in args.notifications {
let request = try showNotification(invoke: invoke, notification: notification) let request = try showNotification(invoke: invoke, notification: notification)
notificationHandler.saveNotification(request.identifier, notification) notificationHandler.saveNotification(request.identifier, notification)
ids.append(Int(request.identifier) ?? -1) ids.append(Int(request.identifier) ?? -1)
} }
invoke.resolve([ invoke.resolve(ids)
"notifications": ids
])
} }
@objc public override func requestPermissions(_ invoke: Invoke) { @objc public override func requestPermissions(_ invoke: Invoke) {
@ -129,18 +213,11 @@ class NotificationPlugin: Plugin {
} }
} }
@objc func cancel(_ invoke: Invoke) { @objc func cancel(_ invoke: Invoke) throws {
guard let notifications = invoke.getArray("notifications", NSNumber.self), let args = try invoke.parseArgs(CancelArgs.self)
notifications.count > 0
else {
invoke.reject("`notifications` input is required")
return
}
UNUserNotificationCenter.current().removePendingNotificationRequests( UNUserNotificationCenter.current().removePendingNotificationRequests(
withIdentifiers: notifications.map({ (id) -> String in withIdentifiers: args.notifications.map { String($0) }
return id.stringValue
})
) )
invoke.resolve() invoke.resolve()
} }
@ -148,30 +225,27 @@ class NotificationPlugin: Plugin {
@objc func getPending(_ invoke: Invoke) { @objc func getPending(_ invoke: Invoke) {
UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: {
(notifications) in (notifications) in
let ret = notifications.compactMap({ [weak self] (notification) -> JSObject? in let ret = notifications.compactMap({ [weak self] (notification) -> PendingNotification? in
return self?.notificationHandler.makePendingNotificationRequestJSObject(notification) return self?.notificationHandler.toPendingNotification(notification)
}) })
invoke.resolve([ invoke.resolve(ret)
"notifications": ret
])
}) })
} }
@objc func registerActionTypes(_ invoke: Invoke) throws { @objc func registerActionTypes(_ invoke: Invoke) throws {
guard let types = invoke.getArray("types", JSObject.self) else { let args = try invoke.parseArgs(RegisterActionTypesArgs.self)
return makeCategories(args.types)
}
try makeCategories(types)
invoke.resolve() invoke.resolve()
} }
@objc func removeActive(_ invoke: Invoke) { @objc func removeActive(_ invoke: Invoke) {
if let notifications = invoke.getArray("notifications", JSObject.self) { do {
let ids = notifications.map { "\($0["id"] ?? "")" } let args = try invoke.parseArgs(RemoveActiveArgs.self)
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids) UNUserNotificationCenter.current().removeDeliveredNotifications(
withIdentifiers: args.notifications.map { String($0.id) })
invoke.resolve() invoke.resolve()
} else { } catch {
UNUserNotificationCenter.current().removeAllDeliveredNotifications() UNUserNotificationCenter.current().removeAllDeliveredNotifications()
DispatchQueue.main.async(execute: { DispatchQueue.main.async(execute: {
UIApplication.shared.applicationIconBadgeNumber = 0 UIApplication.shared.applicationIconBadgeNumber = 0
@ -183,13 +257,11 @@ class NotificationPlugin: Plugin {
@objc func getActive(_ invoke: Invoke) { @objc func getActive(_ invoke: Invoke) {
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: {
(notifications) in (notifications) in
let ret = notifications.map({ (notification) -> [String: Any] in let ret = notifications.map({ (notification) -> ActiveNotification in
return self.notificationHandler.makeNotificationRequestJSObject( return self.notificationHandler.toActiveNotification(
notification.request) notification.request)
}) })
invoke.resolve([ invoke.resolve(ret)
"notifications": ret
])
}) })
} }

@ -27,6 +27,6 @@
"tslib": "^2.4.1" "tslib": "^2.4.1"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.9" "@tauri-apps/api": "2.0.0-alpha.11"
} }
} }

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_NOTIFICATION__=function(n){"use strict";var i=Object.defineProperty,e=(n,i,e)=>{if(!i.has(n))throw TypeError("Cannot "+e)},t=(n,i,t)=>(e(n,i,"read from private field"),t?t.call(n):i.get(n));function o(n,i=!1){return window.__TAURI_INTERNALS__.transformCallback(n,i)}((n,e)=>{for(var t in e)i(n,t,{get:e[t],enumerable:!0})})({},{Channel:()=>c,PluginListener:()=>l,addPluginListener:()=>f,convertFileSrc:()=>d,invoke:()=>_,transformCallback:()=>o});var r,c=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((n,i,e)=>{if(i.has(n))throw TypeError("Cannot add the same private member more than once");i instanceof WeakSet?i.add(n):i.set(n,e)})(this,r,(()=>{})),this.id=o((n=>{t(this,r).call(this,n)}))}set onmessage(n){var i,t,o,c;o=n,e(i=this,t=r,"write to private field"),c?c.call(i,o):t.set(i,o)}get onmessage(){return t(this,r)}toJSON(){return`__CHANNEL__:${this.id}`}};r=new WeakMap;var a,s,u,l=class{constructor(n,i,e){this.plugin=n,this.event=i,this.channelId=e}async unregister(){return _(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function f(n,i,e){let t=new c;return t.onmessage=e,_(`plugin:${n}|register_listener`,{event:i,handler:t}).then((()=>new l(n,i,t.id)))}async function _(n,i={},e){return window.__TAURI_INTERNALS__.invoke(n,i,e)}function d(n,i="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(n,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"}(a||(a={})),n.Importance=void 0,(s=n.Importance||(n.Importance={}))[s.None=0]="None",s[s.Min=1]="Min",s[s.Low=2]="Low",s[s.Default=3]="Default",s[s.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 f("notification","actionPerformed",n)},n.onNotificationReceived=async function(n){return f("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__})} if("__TAURI__"in window){var __TAURI_NOTIFICATION__=function(n){"use strict";function i(n,i,e,t){if("a"===e&&!t)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof i?n!==i||!t:!i.has(n))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===e?t:"a"===e?t.call(n):t?t.value:i.get(n)}var e,t,o,r;"function"==typeof SuppressedError&&SuppressedError;class c{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,e.set(this,(()=>{})),this.id=function(n,i=!1){return window.__TAURI_INTERNALS__.transformCallback(n,i)}((n=>{i(this,e,"f").call(this,n)}))}set onmessage(n){!function(n,i,e,t,o){if("m"===t)throw new TypeError("Private method is not writable");if("a"===t&&!o)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof i?n!==i||!o:!i.has(n))throw new TypeError("Cannot write private member to an object whose class did not declare it");"a"===t?o.call(n,e):o?o.value=e:i.set(n,e)}(this,e,n,"f")}get onmessage(){return i(this,e,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}e=new WeakMap;class a{constructor(n,i,e){this.plugin=n,this.event=i,this.channelId=e}async unregister(){return u(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function s(n,i,e){const t=new c;return t.onmessage=e,u(`plugin:${n}|register_listener`,{event:i,handler:t}).then((()=>new a(n,i,t.id)))}async function u(n,i={},e){return window.__TAURI_INTERNALS__.invoke(n,i,e)}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"}(t||(t={})),n.Importance=void 0,(o=n.Importance||(n.Importance={}))[o.None=0]="None",o[o.Min=1]="Min",o[o.Low=2]="Low",o[o.Default=3]="Default",o[o.High=4]="High",n.Visibility=void 0,(r=n.Visibility||(n.Visibility={}))[r.Secret=-1]="Secret",r[r.Private=0]="Private",r[r.Public=1]="Public",n.active=async function(){return u("plugin:notification|get_active")},n.cancel=async function(n){return u("plugin:notification|cancel",{notifications:n})},n.cancelAll=async function(){return u("plugin:notification|cancel")},n.channels=async function(){return u("plugin:notification|listChannels")},n.createChannel=async function(n){return u("plugin:notification|create_channel",{...n})},n.isPermissionGranted=async function(){return"default"!==window.Notification.permission?Promise.resolve("granted"===window.Notification.permission):u("plugin:notification|is_permission_granted")},n.onAction=async function(n){return s("notification","actionPerformed",n)},n.onNotificationReceived=async function(n){return s("notification","notification",n)},n.pending=async function(){return u("plugin:notification|get_pending")},n.registerActionTypes=async function(n){return u("plugin:notification|register_action_types",{types:n})},n.removeActive=async function(n){return u("plugin:notification|remove_active",{notifications:n})},n.removeAllActive=async function(){return u("plugin:notification|remove_active")},n.removeChannel=async function(n){return u("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__})}

@ -1 +1 @@
!function(){"use strict";var e=Object.defineProperty,n=(e,n,t)=>{if(!n.has(e))throw TypeError("Cannot "+t)},t=(e,t,i)=>(n(e,t,"read from private field"),i?i.call(e):t.get(e));function i(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}((n,t)=>{for(var i in t)e(n,i,{get:t[i],enumerable:!0})})({},{Channel:()=>o,PluginListener:()=>s,addPluginListener:()=>a,convertFileSrc:()=>l,invoke:()=>c,transformCallback:()=>i});var r,o=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,n,t)=>{if(n.has(e))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(e):n.set(e,t)})(this,r,(()=>{})),this.id=i((e=>{t(this,r).call(this,e)}))}set onmessage(e){var t,i,o,s;o=e,n(t=this,i=r,"write to private field"),s?s.call(t,o):i.set(t,o)}get onmessage(){return t(this,r)}toJSON(){return`__CHANNEL__:${this.id}`}};r=new WeakMap;var s=class{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return c(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function a(e,n,t){let i=new o;return i.onmessage=t,c(`plugin:${e}|register_listener`,{event:n,handler:i}).then((()=>new s(e,n,i.id)))}async function c(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}function l(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)}!function(){let e=!1,n="default";function t(n){e=!0,window.Notification.permission=n,e=!1}window.Notification=function(e,n){const t=n||{};!function(e){"object"==typeof e&&Object.freeze(e),c("plugin:notification|notify",{options:"string"==typeof e?{title:e}:e})}(Object.assign(t,{title:e}))},window.Notification.requestPermission=function(){return c("plugin:notification|request_permission").then((e=>(t("prompt"===e?"default":e),e)))},Object.defineProperty(window.Notification,"permission",{enumerable:!0,get:()=>n,set:t=>{if(!e)throw new Error("Readonly property");n=t}}),("default"!==window.Notification.permission?Promise.resolve("granted"===window.Notification.permission):c("plugin:notification|is_permission_granted")).then((function(e){t(null===e?"default":e?"granted":"denied")}))}()}(); !function(){"use strict";async function i(i,n={},t){return window.__TAURI_INTERNALS__.invoke(i,n,t)}"function"==typeof SuppressedError&&SuppressedError,function(){let n=!1,t="default";function o(i){n=!0,window.Notification.permission=i,n=!1}window.Notification=function(n,t){const o=t||{};!function(n){"object"==typeof n&&Object.freeze(n),i("plugin:notification|notify",{options:"string"==typeof n?{title:n}:n})}(Object.assign(o,{title:n}))},window.Notification.requestPermission=function(){return i("plugin:notification|request_permission").then((i=>(o("prompt"===i?"default":i),i)))},Object.defineProperty(window.Notification,"permission",{enumerable:!0,get:()=>t,set:i=>{if(!n)throw new Error("Readonly property");t=i}}),("default"!==window.Notification.permission?Promise.resolve("granted"===window.Notification.permission):i("plugin:notification|is_permission_granted")).then((function(i){o(null===i?"default":i?"granted":"denied")}))}()}();

@ -33,7 +33,7 @@ pub fn init<R: Runtime, C: DeserializeOwned>(
impl<R: Runtime> crate::NotificationBuilder<R> { impl<R: Runtime> crate::NotificationBuilder<R> {
pub fn show(self) -> crate::Result<()> { pub fn show(self) -> crate::Result<()> {
self.handle self.handle
.run_mobile_plugin::<ShowResponse>("show", self.data) .run_mobile_plugin::<i32>("show", self.data)
.map(|_| ()) .map(|_| ())
.map_err(Into::into) .map_err(Into::into)
} }
@ -89,8 +89,7 @@ impl<R: Runtime> Notification<R> {
pub fn active(&self) -> crate::Result<Vec<ActiveNotification>> { pub fn active(&self) -> crate::Result<Vec<ActiveNotification>> {
self.0 self.0
.run_mobile_plugin::<ActiveResponse>("getActive", ()) .run_mobile_plugin("getActive", ())
.map(|r| r.notifications)
.map_err(Into::into) .map_err(Into::into)
} }
@ -102,8 +101,7 @@ impl<R: Runtime> Notification<R> {
pub fn pending(&self) -> crate::Result<Vec<PendingNotification>> { pub fn pending(&self) -> crate::Result<Vec<PendingNotification>> {
self.0 self.0
.run_mobile_plugin::<PendingResponse>("getPending", ()) .run_mobile_plugin("getPending", ())
.map(|r| r.notifications)
.map_err(Into::into) .map_err(Into::into)
} }
@ -138,34 +136,11 @@ impl<R: Runtime> Notification<R> {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
pub fn list_channels(&self) -> crate::Result<Vec<Channel>> { pub fn list_channels(&self) -> crate::Result<Vec<Channel>> {
self.0 self.0
.run_mobile_plugin::<ListChannelsResult>("listChannels", ()) .run_mobile_plugin("listChannels", ())
.map(|r| r.channels)
.map_err(Into::into) .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)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct PermissionResponse { struct PermissionResponse {

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

Loading…
Cancel
Save