diff --git a/Cargo.lock b/Cargo.lock
index b0b561c4..ed09c5f9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4408,6 +4408,17 @@ dependencies = [
"thiserror",
]
+[[package]]
+name = "tauri-plugin-camera"
+version = "0.0.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "thiserror",
+]
+
[[package]]
name = "tauri-plugin-fs-extra"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 46b0ee6a..a7f742f8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,3 +15,4 @@ edition = "2021"
authors = [ "Tauri Programme within The Commons Conservancy" ]
license = "Apache-2.0 OR MIT"
rust-version = "1.64"
+exclude = ["/examples"]
diff --git a/plugins/camera/.gitignore b/plugins/camera/.gitignore
new file mode 100644
index 00000000..d29055d3
--- /dev/null
+++ b/plugins/camera/.gitignore
@@ -0,0 +1,4 @@
+/target
+/Cargo.lock
+
+!dist-js
\ No newline at end of file
diff --git a/plugins/camera/Cargo.toml b/plugins/camera/Cargo.toml
new file mode 100644
index 00000000..5b53ef67
--- /dev/null
+++ b/plugins/camera/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "tauri-plugin-camera"
+version = "0.0.0"
+description = "Ask the user take a photo with the camera or select an image from the gallery."
+authors.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+exclude.workspace = true
+
+[dependencies]
+serde.workspace = true
+serde_json.workspace = true
+tauri.workspace = true
+thiserror.workspace = true
+
+[build-dependencies]
+tauri-build.workspace = true
diff --git a/plugins/camera/LICENSE.spdx b/plugins/camera/LICENSE.spdx
new file mode 100644
index 00000000..cdd0df5a
--- /dev/null
+++ b/plugins/camera/LICENSE.spdx
@@ -0,0 +1,20 @@
+SPDXVersion: SPDX-2.1
+DataLicense: CC0-1.0
+PackageName: tauri
+DataFormat: SPDXRef-1
+PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
+PackageHomePage: https://tauri.app
+PackageLicenseDeclared: Apache-2.0
+PackageLicenseDeclared: MIT
+PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy
+PackageSummary: Tauri is a rust project that enables developers to make secure
+and small desktop applications using a web frontend.
+
+PackageComment: The package includes the following libraries; see
+Relationship information.
+
+Created: 2019-05-20T09:00:00Z
+PackageDownloadLocation: git://github.com/tauri-apps/tauri
+PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git
+PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git
+Creator: Person: Daniel Thompson-Yvetot
\ No newline at end of file
diff --git a/plugins/camera/LICENSE_APACHE-2.0 b/plugins/camera/LICENSE_APACHE-2.0
new file mode 100644
index 00000000..4947287f
--- /dev/null
+++ b/plugins/camera/LICENSE_APACHE-2.0
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/plugins/camera/LICENSE_MIT b/plugins/camera/LICENSE_MIT
new file mode 100644
index 00000000..4d754725
--- /dev/null
+++ b/plugins/camera/LICENSE_MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 - Present Tauri Apps Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/plugins/camera/README.md b/plugins/camera/README.md
new file mode 100644
index 00000000..605789a2
--- /dev/null
+++ b/plugins/camera/README.md
@@ -0,0 +1,84 @@
+# Camera Plugin
+
+Prompt the user to take a photo using the camera or pick an image from the gallery. Mobile only.
+
+## Install
+
+There are three general methods of installation that we can recommend.
+
+1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
+2. Pull sources directly from Github using git tags / revision hashes (most secure)
+3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use)
+
+Install the Core plugin by adding the following to your `Cargo.toml` file:
+
+```toml
+[dependencies]
+tauri-plugin-camera = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/camera" }
+```
+
+You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
+
+> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
+
+```sh
+npm install 'https://gitpkg.now.sh/tauri-apps/plugins-workspace/plugins/camera?feat/camera'
+# or
+yarn add 'https://gitpkg.now.sh/tauri-apps/plugins-workspace/plugins/camera?feat/camera'
+```
+
+**NOT AVAILABLE YET, WILL BE READY WHEN WE MERGE THE BRANCH:**
+```sh
+pnpm add https://github.com/tauri-apps/tauri-plugin-camera
+# or
+npm add https://github.com/tauri-apps/tauri-plugin-camera
+# or
+yarn add https://github.com/tauri-apps/tauri-plugin-camera
+```
+
+## Usage
+
+Register the core plugin with Tauri:
+
+`src-tauri/src/lib.rs`
+
+```rust
+#[cfg_attr(mobile, tauri::mobile_entry_point)]
+pub fn run() {
+ tauri::Builder::default()
+ .plugin(tauri_plugin_camera::init())
+ .run(tauri::generate_context!())
+ .expect("error while running tauri application");
+}
+
+```
+
+Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
+
+```javascript
+import { getPhoto } from "tauri-plugin-camera-api";
+const image = await getPhoto();
+```
+
+### Android
+
+Add the following permissions on the `src-tauri/gen/android/$(APPNAME)/app/src/main/AndroidManifest.xml` file:
+
+```xml
+
+
+```
+
+### iOS
+
+Configure the following properties on `src-tauri/gen/apple/$(APPNAME)_iOS/Info.plist`:
+
+```xml
+NSCameraUsageDescription
+Description for the camera usage here
+NSPhotoLibraryAddUsageDescription
+Description for the library add usage here
+NSPhotoLibraryUsageDescription
+Description for the library usage here
+```
+
diff --git a/plugins/camera/android/.gitignore b/plugins/camera/android/.gitignore
new file mode 100644
index 00000000..fb9b198b
--- /dev/null
+++ b/plugins/camera/android/.gitignore
@@ -0,0 +1,2 @@
+/build
+/tauri-api
diff --git a/plugins/camera/android/build.gradle.kts b/plugins/camera/android/build.gradle.kts
new file mode 100644
index 00000000..1b536c19
--- /dev/null
+++ b/plugins/camera/android/build.gradle.kts
@@ -0,0 +1,46 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "app.tauri.camera"
+ compileSdk = 32
+
+ defaultConfig {
+ minSdk = 24
+ targetSdk = 32
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+
+ implementation("androidx.core:core-ktx:1.9.0")
+ implementation("androidx.appcompat:appcompat:1.6.0")
+ implementation("com.google.android.material:material:1.7.0")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ implementation("androidx.exifinterface:exifinterface:1.3.3")
+ implementation(project(":tauri-android"))
+}
diff --git a/plugins/camera/android/proguard-rules.pro b/plugins/camera/android/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/plugins/camera/android/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/plugins/camera/android/settings.gradle b/plugins/camera/android/settings.gradle
new file mode 100644
index 00000000..32f9aac2
--- /dev/null
+++ b/plugins/camera/android/settings.gradle
@@ -0,0 +1,2 @@
+include ':tauri-android'
+project(':tauri-android').projectDir = new File('./tauri-api')
diff --git a/plugins/camera/android/src/androidTest/java/app/tauri/camera/ExampleInstrumentedTest.kt b/plugins/camera/android/src/androidTest/java/app/tauri/camera/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..5f729a50
--- /dev/null
+++ b/plugins/camera/android/src/androidTest/java/app/tauri/camera/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package app.tauri.camera
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("app.tauri.camera", appContext.packageName)
+ }
+}
diff --git a/plugins/camera/android/src/main/AndroidManifest.xml b/plugins/camera/android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/plugins/camera/android/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/plugins/camera/android/src/main/java/app/tauri/camera/CameraBottomSheetDialogFragment.kt b/plugins/camera/android/src/main/java/app/tauri/camera/CameraBottomSheetDialogFragment.kt
new file mode 100644
index 00000000..655723b1
--- /dev/null
+++ b/plugins/camera/android/src/main/java/app/tauri/camera/CameraBottomSheetDialogFragment.kt
@@ -0,0 +1,102 @@
+package app.tauri.camera
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.DialogInterface
+import android.graphics.Color
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+
+class CameraBottomSheetDialogFragment : BottomSheetDialogFragment() {
+ fun interface BottomSheetOnSelectedListener {
+ fun onSelected(index: Int)
+ }
+
+ fun interface BottomSheetOnCanceledListener {
+ fun onCanceled()
+ }
+
+ private var selectedListener: BottomSheetOnSelectedListener? = null
+ private var canceledListener: BottomSheetOnCanceledListener? = null
+ private var options: List? = null
+ private var title: String? = null
+ fun setTitle(title: String?) {
+ this.title = title
+ }
+
+ fun setOptions(
+ options: List?,
+ selectedListener: BottomSheetOnSelectedListener,
+ canceledListener: BottomSheetOnCanceledListener
+ ) {
+ this.options = options
+ this.selectedListener = selectedListener
+ this.canceledListener = canceledListener
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ if (canceledListener != null) {
+ canceledListener!!.onCanceled()
+ }
+ }
+
+ private val mBottomSheetBehaviorCallback: BottomSheetCallback = object : BottomSheetCallback() {
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_HIDDEN) {
+ dismiss()
+ }
+ }
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {}
+ }
+
+ @SuppressLint("RestrictedApi")
+ override fun setupDialog(dialog: Dialog, style: Int) {
+ super.setupDialog(dialog, style)
+ if (options == null || options!!.size == 0) {
+ return
+ }
+ val scale = resources.displayMetrics.density
+ val layoutPaddingDp16 = 16.0f
+ val layoutPaddingDp12 = 12.0f
+ val layoutPaddingDp8 = 8.0f
+ val layoutPaddingPx16 = (layoutPaddingDp16 * scale + 0.5f).toInt()
+ val layoutPaddingPx12 = (layoutPaddingDp12 * scale + 0.5f).toInt()
+ val layoutPaddingPx8 = (layoutPaddingDp8 * scale + 0.5f).toInt()
+ val parentLayout = CoordinatorLayout(requireContext())
+ val layout = LinearLayout(context)
+ layout.orientation = LinearLayout.VERTICAL
+ layout.setPadding(layoutPaddingPx16, layoutPaddingPx16, layoutPaddingPx16, layoutPaddingPx16)
+ val ttv = TextView(context)
+ ttv.setTextColor(Color.parseColor("#757575"))
+ ttv.setPadding(layoutPaddingPx8, layoutPaddingPx8, layoutPaddingPx8, layoutPaddingPx8)
+ ttv.text = title
+ layout.addView(ttv)
+ for (i in options!!.indices) {
+ val tv = TextView(context)
+ tv.setTextColor(Color.parseColor("#000000"))
+ tv.setPadding(layoutPaddingPx12, layoutPaddingPx12, layoutPaddingPx12, layoutPaddingPx12)
+ tv.text = options!![i]
+ tv.setOnClickListener {
+ if (selectedListener != null) {
+ selectedListener!!.onSelected(i)
+ }
+ dismiss()
+ }
+ layout.addView(tv)
+ }
+ parentLayout.addView(layout.rootView)
+ dialog.setContentView(parentLayout.rootView)
+ val params = (parentLayout.parent as View).layoutParams as CoordinatorLayout.LayoutParams
+ val behavior = params.behavior
+ if (behavior != null && behavior is BottomSheetBehavior<*>) {
+ behavior.addBottomSheetCallback(mBottomSheetBehaviorCallback)
+ }
+ }
+}
diff --git a/plugins/camera/android/src/main/java/app/tauri/camera/CameraPlugin.kt b/plugins/camera/android/src/main/java/app/tauri/camera/CameraPlugin.kt
new file mode 100644
index 00000000..ada2984c
--- /dev/null
+++ b/plugins/camera/android/src/main/java/app/tauri/camera/CameraPlugin.kt
@@ -0,0 +1,827 @@
+package app.tauri.camera
+
+import android.Manifest
+import android.app.Activity
+import android.content.*
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Environment
+import android.os.Parcelable
+import android.provider.MediaStore
+import android.util.Base64
+import androidx.activity.result.ActivityResult
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.FileProvider
+import androidx.exifinterface.media.ExifInterface.*
+import app.tauri.*
+import app.tauri.annotation.*
+import app.tauri.plugin.*
+import org.json.JSONException
+import java.io.*
+import java.util.*
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+enum class CameraSource(val source: String) {
+ PROMPT("PROMPT"), CAMERA("CAMERA"), PHOTOS("PHOTOS");
+}
+
+enum class CameraResultType(val type: String) {
+ BASE64("base64"), URI("uri"), DATAURL("dataUrl");
+}
+
+class CameraSettings {
+ var resultType: CameraResultType = CameraResultType.BASE64
+ var quality = DEFAULT_QUALITY
+ var isShouldResize = false
+ var isShouldCorrectOrientation = DEFAULT_CORRECT_ORIENTATION
+ var isSaveToGallery = DEFAULT_SAVE_IMAGE_TO_GALLERY
+ var isAllowEditing = false
+ var width = 0
+ var height = 0
+ var source: CameraSource = CameraSource.PROMPT
+
+ companion object {
+ const val DEFAULT_QUALITY = 90
+ const val DEFAULT_SAVE_IMAGE_TO_GALLERY = false
+ const val DEFAULT_CORRECT_ORIENTATION = true
+ }
+}
+
+@TauriPlugin(
+ permissions = [
+ Permission(strings = [Manifest.permission.CAMERA], alias = "camera"),
+ Permission(
+ strings = [Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE],
+ alias = "photos"
+ )]
+)
+class CameraPlugin(private val activity: Activity): Plugin(activity) {
+ // Permission alias constants
+ val CAMERA = "camera"
+ val PHOTOS = "photos"
+
+ // Message constants
+ private val INVALID_RESULT_TYPE_ERROR = "Invalid resultType option"
+ private val PERMISSION_DENIED_ERROR_CAMERA = "User denied access to camera"
+ private val PERMISSION_DENIED_ERROR_PHOTOS = "User denied access to photos"
+ private val NO_CAMERA_ERROR = "Device doesn't have a camera available"
+ private val NO_CAMERA_ACTIVITY_ERROR = "Unable to resolve camera activity"
+ private val NO_PHOTO_ACTIVITY_ERROR = "Unable to resolve photo activity"
+ private val IMAGE_FILE_SAVE_ERROR = "Unable to create photo on disk"
+ private val IMAGE_PROCESS_NO_FILE_ERROR = "Unable to process image, file not found on disk"
+ private val UNABLE_TO_PROCESS_IMAGE = "Unable to process image"
+ private val IMAGE_EDIT_ERROR = "Unable to edit image"
+ private val IMAGE_GALLERY_SAVE_ERROR = "Unable to save the image in the gallery"
+
+ private var imageFileSavePath: String? = null
+ private var imageEditedFileSavePath: String? = null
+ private var imageFileUri: Uri? = null
+ private var imagePickedContentUri: Uri? = null
+ private var isEdited = false
+ private var isFirstRequest = true
+ private var isSaved = false
+
+ private var settings: CameraSettings = CameraSettings()
+
+ @PluginMethod
+ fun getPhoto(invoke: Invoke) {
+ isEdited = false
+ settings = getSettings(invoke)
+ doShow(invoke)
+ }
+
+ @PluginMethod
+ fun pickImages(invoke: Invoke) {
+ settings = getSettings(invoke)
+ openPhotos(invoke, multiple = true, skipPermission = false)
+ }
+
+ @PluginMethod
+ fun pickLimitedLibraryPhotos(invoke: Invoke) {
+ invoke.reject("not supported on android")
+ }
+
+ @PluginMethod
+ fun getLimitedLibraryPhotos(invoke: Invoke) {
+ invoke.reject("not supported on android")
+ }
+
+ private fun doShow(invoke: Invoke) {
+ when (settings.source) {
+ CameraSource.CAMERA -> showCamera(invoke)
+ CameraSource.PHOTOS -> showPhotos(invoke)
+ else -> showPrompt(invoke)
+ }
+ }
+
+ private fun showPrompt(invoke: Invoke) {
+ // We have all necessary permissions, open the camera
+ val options: MutableList = ArrayList()
+ options.add(invoke.getString("promptLabelPhoto", "From Photos"))
+ options.add(invoke.getString("promptLabelPicture", "Take Picture"))
+ val fragment = CameraBottomSheetDialogFragment()
+ fragment.setTitle(invoke.getString("promptLabelHeader", "Photo"))
+ fragment.setOptions(
+ options,
+ { index: Int ->
+ if (index == 0) {
+ settings.source = CameraSource.PHOTOS
+ openPhotos(invoke)
+ } else if (index == 1) {
+ settings.source = CameraSource.CAMERA
+ openCamera(invoke)
+ }
+ },
+ { invoke.reject("User cancelled photos app") })
+ fragment.show((activity as AppCompatActivity).supportFragmentManager, "capacitorModalsActionSheet")
+ }
+
+ private fun showCamera(invoke: Invoke) {
+ if (!activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
+ invoke.reject(NO_CAMERA_ERROR)
+ return
+ }
+ openCamera(invoke)
+ }
+
+ private fun showPhotos(invoke: Invoke) {
+ openPhotos(invoke)
+ }
+
+ private fun checkCameraPermissions(invoke: Invoke): Boolean {
+ // if the manifest does not contain the camera permissions key, we don't need to ask the user
+ val needCameraPerms = isPermissionDeclared(CAMERA)
+ val hasCameraPerms = !needCameraPerms || getPermissionState(CAMERA) === PermissionState.GRANTED
+ val hasPhotoPerms = getPermissionState(PHOTOS) === PermissionState.GRANTED
+
+ // If we want to save to the gallery, we need two permissions
+ if (settings.isSaveToGallery && !(hasCameraPerms && hasPhotoPerms) && isFirstRequest) {
+ isFirstRequest = false
+ val aliases = if (needCameraPerms) {
+ arrayOf(CAMERA, PHOTOS)
+ } else {
+ arrayOf(PHOTOS)
+ }
+ requestPermissionForAliases(aliases, invoke, "cameraPermissionsCallback")
+ return false
+ } else if (!hasCameraPerms) {
+ requestPermissionForAlias(CAMERA, invoke, "cameraPermissionsCallback")
+ return false
+ }
+ return true
+ }
+
+ private fun checkPhotosPermissions(invoke: Invoke): Boolean {
+ if (getPermissionState(PHOTOS) !== PermissionState.GRANTED) {
+ requestPermissionForAlias(PHOTOS, invoke, "cameraPermissionsCallback")
+ return false
+ }
+ return true
+ }
+
+ /**
+ * Completes the plugin invoke after a camera permission request
+ *
+ * @see .getPhoto
+ * @param invoke the plugin invoke
+ */
+ @PermissionCallback
+ private fun cameraPermissionsCallback(invoke: Invoke) {
+ // TODO: invoke.methodName()
+ val methodName = "pickImages"
+ if (methodName == "pickImages") {
+ openPhotos(invoke, multiple = true, skipPermission = true)
+ } else {
+ if (settings.source === CameraSource.CAMERA && getPermissionState(CAMERA) !== PermissionState.GRANTED) {
+ Logger.debug(
+ getLogTag(),
+ "User denied camera permission: " + getPermissionState(CAMERA).toString()
+ )
+ invoke.reject(PERMISSION_DENIED_ERROR_CAMERA)
+ return
+ } else if (settings.source === CameraSource.PHOTOS && getPermissionState(PHOTOS) !== PermissionState.GRANTED) {
+ Logger.debug(
+ getLogTag(),
+ "User denied photos permission: " + getPermissionState(PHOTOS).toString()
+ )
+ invoke.reject(PERMISSION_DENIED_ERROR_PHOTOS)
+ return
+ }
+ doShow(invoke)
+ }
+ }
+
+ private fun getSettings(invoke: Invoke): CameraSettings {
+ val settings = CameraSettings()
+ val resultType = getResultType(invoke.getString("resultType"))
+ if (resultType != null) {
+ settings.resultType = resultType
+ }
+ settings.isSaveToGallery =
+ invoke.getBoolean(
+ "saveToGallery",
+ CameraSettings.DEFAULT_SAVE_IMAGE_TO_GALLERY
+ )
+ settings.isAllowEditing = invoke.getBoolean("allowEditing", false)
+ settings.quality = invoke.getInt("quality", CameraSettings.DEFAULT_QUALITY)
+ settings.width = invoke.getInt("width", 0)
+ settings.height = invoke.getInt("height", 0)
+ settings.isShouldResize = settings.width > 0 || settings.height > 0
+ settings.isShouldCorrectOrientation =
+ invoke.getBoolean(
+ "correctOrientation",
+ CameraSettings.DEFAULT_CORRECT_ORIENTATION
+ )
+
+ try {
+ settings.source =
+ CameraSource.valueOf(
+ invoke.getString(
+ "source",
+ CameraSource.PROMPT.source
+ )
+ )
+
+ } catch (ex: IllegalArgumentException) {
+ settings.source = CameraSource.PROMPT
+ }
+ return settings
+ }
+
+ private fun getResultType(resultType: String?): CameraResultType? {
+ return if (resultType == null) {
+ null
+ } else try {
+ CameraResultType.valueOf(resultType.uppercase(Locale.ROOT))
+ } catch (ex: IllegalArgumentException) {
+ Logger.debug(getLogTag(), "Invalid result type \"$resultType\", defaulting to base64")
+ CameraResultType.BASE64
+ }
+ }
+
+ private fun openCamera(invoke: Invoke) {
+ if (checkCameraPermissions(invoke)) {
+ val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+ if (takePictureIntent.resolveActivity(activity.packageManager) != null) {
+ // If we will be saving the photo, send the target file along
+ try {
+ val appId: String = activity.packageName
+ val photoFile: File = CameraUtils.createImageFile(activity)
+ imageFileSavePath = photoFile.absolutePath
+ // TODO: Verify provider config exists
+ imageFileUri = FileProvider.getUriForFile(
+ activity,
+ "$appId.fileprovider", photoFile
+ )
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri)
+ } catch (ex: Exception) {
+ invoke.reject(IMAGE_FILE_SAVE_ERROR, ex)
+ return
+ }
+ startActivityForResult(invoke, takePictureIntent, "processCameraImage")
+ } else {
+ invoke.reject(NO_CAMERA_ACTIVITY_ERROR)
+ }
+ }
+ }
+
+ private fun openPhotos(invoke: Invoke) {
+ openPhotos(invoke, multiple = false, skipPermission = false)
+ }
+
+ private fun openPhotos(invoke: Invoke, multiple: Boolean, skipPermission: Boolean) {
+ if (skipPermission || checkPhotosPermissions(invoke)) {
+ val intent = Intent(Intent.ACTION_PICK)
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
+ intent.setType("image/*")
+ try {
+ if (multiple) {
+ intent.putExtra("multi-pick", multiple)
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
+ startActivityForResult(invoke, intent, "processPickedImages")
+ } else {
+ startActivityForResult(invoke, intent, "processPickedImage")
+ }
+ } catch (ex: ActivityNotFoundException) {
+ invoke.reject(NO_PHOTO_ACTIVITY_ERROR)
+ }
+ }
+ }
+
+ @ActivityCallback
+ fun processCameraImage(invoke: Invoke, result: ActivityResult?) {
+ settings = getSettings(invoke)
+ if (imageFileSavePath == null) {
+ invoke.reject(IMAGE_PROCESS_NO_FILE_ERROR)
+ return
+ }
+ // Load the image as a Bitmap
+ val f = File(imageFileSavePath!!)
+ val bmOptions: BitmapFactory.Options = BitmapFactory.Options()
+ val contentUri: Uri = Uri.fromFile(f)
+ val bitmap: Bitmap = BitmapFactory.decodeFile(imageFileSavePath, bmOptions)
+ returnResult(invoke, bitmap, contentUri)
+ }
+
+ @ActivityCallback
+ fun processPickedImage(invoke: Invoke, result: ActivityResult) {
+ settings = getSettings(invoke)
+ val data: Intent? = result.data
+ if (data == null) {
+ invoke.reject("No image picked")
+ return
+ }
+ val u: Uri = data.data!!
+ imagePickedContentUri = u
+ processPickedImage(u, invoke)
+ }
+
+ @ActivityCallback
+ fun processPickedImages(invoke: Invoke, result: ActivityResult) {
+ val data: Intent? = result.data
+ if (data != null) {
+ val executor: Executor = Executors.newSingleThreadExecutor()
+ executor.execute {
+ val ret = JSObject()
+ val photos = JSArray()
+ if (data.clipData != null) {
+ val count: Int = data.clipData!!.itemCount
+ for (i in 0 until count) {
+ val imageUri: Uri = data.clipData!!.getItemAt(i).uri
+ val processResult = processPickedImages(imageUri)
+ if (processResult.getString("error").isNotEmpty()
+ ) {
+ invoke.reject(processResult.getString("error"))
+ return@execute
+ } else {
+ photos.put(processResult)
+ }
+ }
+ } else if (data.data != null) {
+ val imageUri: Uri = data.data!!
+ val processResult = processPickedImages(imageUri)
+ if (processResult.getString("error").isNotEmpty()
+ ) {
+ invoke.reject(processResult.getString("error"))
+ return@execute
+ } else {
+ photos.put(processResult)
+ }
+ } else if (data.extras != null) {
+ val bundle: Bundle = data.extras!!
+ if (bundle.keySet().contains("selectedItems")) {
+ val fileUris: ArrayList? = bundle.getParcelableArrayList("selectedItems")
+ if (fileUris != null) {
+ for (fileUri in fileUris) {
+ if (fileUri is Uri) {
+ val imageUri: Uri = fileUri
+ try {
+ val processResult = processPickedImages(imageUri)
+ if (processResult.getString("error").isNotEmpty()
+ ) {
+ invoke.reject(processResult.getString("error"))
+ return@execute
+ } else {
+ photos.put(processResult)
+ }
+ } catch (ex: SecurityException) {
+ invoke.reject("SecurityException")
+ }
+ }
+ }
+ }
+ }
+ }
+ ret.put("photos", photos)
+ invoke.resolve(ret)
+ }
+ } else {
+ invoke.reject("No images picked")
+ }
+ }
+
+ private fun processPickedImage(imageUri: Uri, invoke: Invoke) {
+ var imageStream: InputStream? = null
+ try {
+ imageStream = activity.contentResolver.openInputStream(imageUri)
+ val bitmap = BitmapFactory.decodeStream(imageStream)
+ if (bitmap == null) {
+ invoke.reject("Unable to process bitmap")
+ return
+ }
+ returnResult(invoke, bitmap, imageUri)
+ } catch (err: OutOfMemoryError) {
+ invoke.reject("Out of memory")
+ } catch (ex: FileNotFoundException) {
+ invoke.reject("No such image found", ex)
+ } finally {
+ if (imageStream != null) {
+ try {
+ imageStream.close()
+ } catch (e: IOException) {
+ Logger.error(getLogTag(), UNABLE_TO_PROCESS_IMAGE, e)
+ }
+ }
+ }
+ }
+
+ private fun processPickedImages(imageUri: Uri): JSObject {
+ var imageStream: InputStream? = null
+ val ret = JSObject()
+ try {
+ imageStream = activity.contentResolver.openInputStream(imageUri)
+ var bitmap = BitmapFactory.decodeStream(imageStream)
+ if (bitmap == null) {
+ ret.put("error", "Unable to process bitmap")
+ return ret
+ }
+ val exif: ExifWrapper = ImageUtils.getExifData(activity, bitmap, imageUri)
+ bitmap = try {
+ prepareBitmap(bitmap, imageUri, exif)
+ } catch (e: IOException) {
+ ret.put("error", UNABLE_TO_PROCESS_IMAGE)
+ return ret
+ }
+ // Compress the final image and prepare for output to client
+ val bitmapOutputStream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.JPEG, settings.quality, bitmapOutputStream)
+ val newUri: Uri? = getTempImage(imageUri, bitmapOutputStream)
+ exif.copyExif(newUri?.path)
+ if (newUri != null) {
+ ret.put("format", "jpeg")
+ ret.put("exif", exif.toJson())
+ ret.put("data", newUri.toString())
+ ret.put("assetUrl", assetUrl(newUri))
+ } else {
+ ret.put("error", UNABLE_TO_PROCESS_IMAGE)
+ }
+ return ret
+ } catch (err: OutOfMemoryError) {
+ ret.put("error", "Out of memory")
+ } catch (ex: FileNotFoundException) {
+ ret.put("error", "No such image found")
+ Logger.error(getLogTag(), "No such image found", ex)
+ } finally {
+ if (imageStream != null) {
+ try {
+ imageStream.close()
+ } catch (e: IOException) {
+ Logger.error(getLogTag(), UNABLE_TO_PROCESS_IMAGE, e)
+ }
+ }
+ }
+ return ret
+ }
+
+ @ActivityCallback
+ private fun processEditedImage(invoke: Invoke, result: ActivityResult) {
+ isEdited = true
+ settings = getSettings(invoke)
+ if (result.resultCode == Activity.RESULT_CANCELED) {
+ // User cancelled the edit operation, if this file was picked from photos,
+ // process the original picked image, otherwise process it as a camera photo
+ if (imagePickedContentUri != null) {
+ processPickedImage(imagePickedContentUri!!, invoke)
+ } else {
+ processCameraImage(invoke, result)
+ }
+ } else {
+ processPickedImage(invoke, result)
+ }
+ }
+
+ /**
+ * Save the modified image on the same path,
+ * or on a temporary location if it's a content url
+ * @param uri
+ * @param is
+ * @return
+ * @throws IOException
+ */
+ @Throws(IOException::class)
+ private fun saveImage(uri: Uri, input: InputStream): Uri? {
+ var outFile = if (uri.scheme.equals("content")) {
+ getTempFile(uri)
+ } else {
+ uri.path?.let { File(it) }
+ }
+ try {
+ writePhoto(outFile!!, input)
+ } catch (ex: FileNotFoundException) {
+ // Some gallery apps return read only file url, create a temporary file for modifications
+ outFile = getTempFile(uri)
+ writePhoto(outFile, input)
+ }
+ return Uri.fromFile(outFile)
+ }
+
+ @Throws(IOException::class)
+ private fun writePhoto(outFile: File, input: InputStream) {
+ val fos = FileOutputStream(outFile)
+ val buffer = ByteArray(1024)
+ var len: Int
+ while (input.read(buffer).also { len = it } != -1) {
+ fos.write(buffer, 0, len)
+ }
+ fos.close()
+ }
+
+ private fun getTempFile(uri: Uri): File {
+ var filename: String = Uri.parse(Uri.decode(uri.toString())).lastPathSegment!!
+ if (!filename.contains(".jpg") && !filename.contains(".jpeg")) {
+ filename += "." + Date().time + ".jpeg"
+ }
+ val cacheDir: File = activity.getCacheDir()
+ return File(cacheDir, filename)
+ }
+
+ /**
+ * After processing the image, return the final result back to the invokeer.
+ * @param invoke
+ * @param bitmap
+ * @param u
+ */
+ private fun returnResult(invoke: Invoke, bitmap: Bitmap, u: Uri) {
+ val exif: ExifWrapper = ImageUtils.getExifData(activity, bitmap, u)
+ val preparedBitmap = try {
+ prepareBitmap(bitmap, u, exif)
+ } catch (e: IOException) {
+ invoke.reject(UNABLE_TO_PROCESS_IMAGE)
+ return
+ }
+ // Compress the final image and prepare for output to client
+ val bitmapOutputStream = ByteArrayOutputStream()
+ preparedBitmap.compress(Bitmap.CompressFormat.JPEG, settings.quality, bitmapOutputStream)
+ if (settings.isAllowEditing && !isEdited) {
+ editImage(invoke, u, bitmapOutputStream)
+ return
+ }
+ val saveToGallery: Boolean =
+ invoke.getBoolean("saveToGallery", CameraSettings.DEFAULT_SAVE_IMAGE_TO_GALLERY)
+ if (saveToGallery && (imageEditedFileSavePath != null || imageFileSavePath != null)) {
+ isSaved = true
+ try {
+ val fileToSavePath =
+ if (imageEditedFileSavePath != null) imageEditedFileSavePath!! else imageFileSavePath!!
+ val fileToSave = File(fileToSavePath)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val resolver: ContentResolver = activity.contentResolver
+ val values = ContentValues()
+ values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileToSave.name)
+ values.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
+ val contentUri: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ val uri: Uri = resolver.insert(contentUri, values)
+ ?: throw IOException("Failed to create new MediaStore record.")
+ val stream: OutputStream = resolver.openOutputStream(uri)
+ ?: throw IOException("Failed to open output stream.")
+ val inserted: Boolean =
+ preparedBitmap.compress(Bitmap.CompressFormat.JPEG, settings.quality, stream)
+ if (!inserted) {
+ isSaved = false
+ }
+ } else {
+ val inserted = MediaStore.Images.Media.insertImage(
+ activity.contentResolver,
+ fileToSavePath,
+ fileToSave.name,
+ ""
+ )
+ if (inserted == null) {
+ isSaved = false
+ }
+ }
+ } catch (e: FileNotFoundException) {
+ isSaved = false
+ Logger.error(getLogTag(), IMAGE_GALLERY_SAVE_ERROR, e)
+ } catch (e: IOException) {
+ isSaved = false
+ Logger.error(getLogTag(), IMAGE_GALLERY_SAVE_ERROR, e)
+ }
+ }
+ if (settings.resultType === CameraResultType.BASE64) {
+ returnBase64(invoke, exif, bitmapOutputStream)
+ } else if (settings.resultType === CameraResultType.URI) {
+ returnFileURI(invoke, exif, bitmap, u, bitmapOutputStream)
+ } else if (settings.resultType === CameraResultType.DATAURL) {
+ returnDataUrl(invoke, exif, bitmapOutputStream)
+ } else {
+ invoke.reject(INVALID_RESULT_TYPE_ERROR)
+ }
+ // Result returned, clear stored paths and images
+ if (settings.resultType !== CameraResultType.URI) {
+ deleteImageFile()
+ }
+ imageFileSavePath = null
+ imageFileUri = null
+ imagePickedContentUri = null
+ imageEditedFileSavePath = null
+ }
+
+ private fun deleteImageFile() {
+ if (imageFileSavePath != null && !settings.isSaveToGallery) {
+ val photoFile = File(imageFileSavePath!!)
+ if (photoFile.exists()) {
+ photoFile.delete()
+ }
+ }
+ }
+
+ private fun returnFileURI(
+ invoke: Invoke,
+ exif: ExifWrapper,
+ bitmap: Bitmap,
+ u: Uri,
+ bitmapOutputStream: ByteArrayOutputStream
+ ) {
+ val newUri: Uri? = getTempImage(u, bitmapOutputStream)
+ exif.copyExif(newUri?.path)
+ if (newUri != null) {
+ val ret = JSObject()
+ ret.put("format", "jpeg")
+ ret.put("exif", exif.toJson())
+ ret.put("data", newUri.toString())
+ ret.put("assetUrl", assetUrl(newUri))
+ ret.put("saved", isSaved)
+ invoke.resolve(ret)
+ } else {
+ invoke.reject(UNABLE_TO_PROCESS_IMAGE)
+ }
+ }
+
+ private fun getTempImage(u: Uri, bitmapOutputStream: ByteArrayOutputStream): Uri? {
+ var bis: ByteArrayInputStream? = null
+ var newUri: Uri? = null
+ try {
+ bis = ByteArrayInputStream(bitmapOutputStream.toByteArray())
+ newUri = saveImage(u, bis)
+ } catch (_: IOException) {
+ } finally {
+ if (bis != null) {
+ try {
+ bis.close()
+ } catch (e: IOException) {
+ Logger.error(getLogTag(), UNABLE_TO_PROCESS_IMAGE, e)
+ }
+ }
+ }
+ return newUri
+ }
+
+ /**
+ * Apply our standard processing of the bitmap, returning a new one and
+ * recycling the old one in the process
+ * @param bitmap
+ * @param imageUri
+ * @param exif
+ * @return
+ */
+ @Throws(IOException::class)
+ private fun prepareBitmap(bitmap: Bitmap, imageUri: Uri, exif: ExifWrapper): Bitmap {
+ var preparedBitmap: Bitmap = bitmap
+ if (settings.isShouldCorrectOrientation) {
+ val newBitmap: Bitmap = ImageUtils.correctOrientation(activity, preparedBitmap, imageUri, exif)
+ preparedBitmap = replaceBitmap(preparedBitmap, newBitmap)
+ }
+ if (settings.isShouldResize) {
+ val newBitmap: Bitmap = ImageUtils.resize(preparedBitmap, settings.width, settings.height)
+ preparedBitmap = replaceBitmap(preparedBitmap, newBitmap)
+ }
+ return preparedBitmap
+ }
+
+ private fun replaceBitmap(bitmap: Bitmap, newBitmap: Bitmap): Bitmap {
+ if (bitmap !== newBitmap) {
+ bitmap.recycle()
+ }
+ return newBitmap
+ }
+
+ private fun returnDataUrl(
+ invoke: Invoke,
+ exif: ExifWrapper,
+ bitmapOutputStream: ByteArrayOutputStream
+ ) {
+ val byteArray: ByteArray = bitmapOutputStream.toByteArray()
+ val encoded: String = Base64.encodeToString(byteArray, Base64.NO_WRAP)
+ val data = JSObject()
+ data.put("format", "jpeg")
+ data.put("data", "data:image/jpeg;base64,$encoded")
+ data.put("exif", exif.toJson())
+ invoke.resolve(data)
+ }
+
+ private fun returnBase64(
+ invoke: Invoke,
+ exif: ExifWrapper,
+ bitmapOutputStream: ByteArrayOutputStream
+ ) {
+ val byteArray: ByteArray = bitmapOutputStream.toByteArray()
+ val encoded: String = Base64.encodeToString(byteArray, Base64.NO_WRAP)
+ val data = JSObject()
+ data.put("format", "jpeg")
+ data.put("data", encoded)
+ data.put("exif", exif.toJson())
+ invoke.resolve(data)
+ }
+
+ @PluginMethod
+ override fun requestPermissions(invoke: Invoke) {
+ // If the camera permission is defined in the manifest, then we have to prompt the user
+ // or else we will get a security exception when trying to present the camera. If, however,
+ // it is not defined in the manifest then we don't need to prompt and it will just work.
+ if (isPermissionDeclared(CAMERA)) {
+ // just request normally
+ super.requestPermissions(invoke)
+ } else {
+ // the manifest does not define camera permissions, so we need to decide what to do
+ // first, extract the permissions being requested
+ val providedPerms = invoke.getArray("permissions", JSArray())
+ var permsList: List? = null
+ try {
+ permsList = providedPerms.toList()
+ } catch (_: JSONException) {
+ }
+ if (permsList != null && permsList.size == 1 && permsList.contains(CAMERA)) {
+ // the only thing being asked for was the camera so we can just return the current state
+ checkPermissions(invoke)
+ } else {
+ // we need to ask about photos so request storage permissions
+ requestPermissionForAlias(PHOTOS, invoke, "checkPermissions")
+ }
+ }
+ }
+
+ override fun getPermissionStates(): Map {
+ val permissionStates = super.getPermissionStates() as MutableMap
+
+ // If Camera is not in the manifest and therefore not required, say the permission is granted
+ if (!isPermissionDeclared(CAMERA)) {
+ permissionStates[CAMERA] = PermissionState.GRANTED
+ }
+ return permissionStates
+ }
+
+ private fun editImage(invoke: Invoke, uri: Uri, bitmapOutputStream: ByteArrayOutputStream) {
+ try {
+ val tempImage = getTempImage(uri, bitmapOutputStream)
+ val editIntent = createEditIntent(tempImage)
+ if (editIntent != null) {
+ startActivityForResult(invoke, editIntent, "processEditedImage")
+ } else {
+ invoke.reject(IMAGE_EDIT_ERROR)
+ }
+ } catch (ex: Exception) {
+ invoke.reject(IMAGE_EDIT_ERROR, ex)
+ }
+ }
+
+ private fun createEditIntent(origPhotoUri: Uri?): Intent? {
+ return try {
+ val editFile = origPhotoUri?.path?.let { File(it) }
+ val editUri: Uri = FileProvider.getUriForFile(
+ activity,
+ activity.packageName + ".fileprovider",
+ editFile!!
+ )
+ val editIntent = Intent(Intent.ACTION_EDIT)
+ editIntent.setDataAndType(editUri, "image/*")
+ imageEditedFileSavePath = editFile.absolutePath
+ val flags: Int =
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ editIntent.addFlags(flags)
+ editIntent.putExtra(MediaStore.EXTRA_OUTPUT, editUri)
+ val resInfoList: List = activity
+ .packageManager
+ .queryIntentActivities(editIntent, PackageManager.MATCH_DEFAULT_ONLY)
+ for (resolveInfo in resInfoList) {
+ val packageName: String = resolveInfo.activityInfo.packageName
+ activity.grantUriPermission(packageName, editUri, flags)
+ }
+ editIntent
+ } catch (ex: Exception) {
+ null
+ }
+ }
+
+ /*protected fun saveInstanceState(): Bundle? {
+ val bundle: Bundle = super.saveInstanceState()
+ if (bundle != null) {
+ bundle.putString("cameraImageFileSavePath", imageFileSavePath)
+ }
+ return bundle
+ }
+
+ protected fun restoreState(state: Bundle) {
+ val storedImageFileSavePath: String = state.getString("cameraImageFileSavePath")
+ if (storedImageFileSavePath != null) {
+ imageFileSavePath = storedImageFileSavePath
+ }
+ }*/
+}
diff --git a/plugins/camera/android/src/main/java/app/tauri/camera/CameraUtils.kt b/plugins/camera/android/src/main/java/app/tauri/camera/CameraUtils.kt
new file mode 100644
index 00000000..76972e3d
--- /dev/null
+++ b/plugins/camera/android/src/main/java/app/tauri/camera/CameraUtils.kt
@@ -0,0 +1,39 @@
+package app.tauri.camera
+
+import android.app.Activity
+import android.net.Uri
+import android.os.Environment
+import androidx.core.content.FileProvider
+import app.tauri.Logger
+import java.io.File
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.*
+
+object CameraUtils {
+ @Throws(IOException::class)
+ fun createImageFileUri(activity: Activity, appId: String): Uri {
+ val photoFile = createImageFile(activity)
+ return FileProvider.getUriForFile(
+ activity,
+ "$appId.fileprovider", photoFile
+ )
+ }
+
+ @Throws(IOException::class)
+ fun createImageFile(activity: Activity): File {
+ // Create an image file name
+ val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
+ val imageFileName = "JPEG_" + timeStamp + "_"
+ val storageDir =
+ activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
+ return File.createTempFile(
+ imageFileName, /* prefix */
+ ".jpg", /* suffix */
+ storageDir /* directory */
+ )
+ }
+
+ internal val logTag: String
+ internal get() = Logger.tags("CameraUtils")
+}
diff --git a/plugins/camera/android/src/main/java/app/tauri/camera/ExifWrapper.kt b/plugins/camera/android/src/main/java/app/tauri/camera/ExifWrapper.kt
new file mode 100644
index 00000000..1ef69ef4
--- /dev/null
+++ b/plugins/camera/android/src/main/java/app/tauri/camera/ExifWrapper.kt
@@ -0,0 +1,198 @@
+package app.tauri.camera
+
+import androidx.exifinterface.media.ExifInterface.*
+import androidx.exifinterface.media.ExifInterface
+import app.tauri.plugin.JSObject
+
+class ExifWrapper(private val exif: ExifInterface?) {
+ private val attributes = arrayOf(
+ TAG_APERTURE_VALUE,
+ TAG_ARTIST,
+ TAG_BITS_PER_SAMPLE,
+ TAG_BODY_SERIAL_NUMBER,
+ TAG_BRIGHTNESS_VALUE,
+ TAG_CAMERA_OWNER_NAME,
+ TAG_CFA_PATTERN,
+ TAG_COLOR_SPACE,
+ TAG_COMPONENTS_CONFIGURATION,
+ TAG_COMPRESSED_BITS_PER_PIXEL,
+ TAG_COMPRESSION,
+ TAG_CONTRAST,
+ TAG_COPYRIGHT,
+ TAG_CUSTOM_RENDERED,
+ TAG_DATETIME,
+ TAG_DATETIME_DIGITIZED,
+ TAG_DATETIME_ORIGINAL,
+ TAG_DEFAULT_CROP_SIZE,
+ TAG_DEVICE_SETTING_DESCRIPTION,
+ TAG_DIGITAL_ZOOM_RATIO,
+ TAG_DNG_VERSION,
+ TAG_EXIF_VERSION,
+ TAG_EXPOSURE_BIAS_VALUE,
+ TAG_EXPOSURE_INDEX,
+ TAG_EXPOSURE_MODE,
+ TAG_EXPOSURE_PROGRAM,
+ TAG_EXPOSURE_TIME,
+ TAG_FILE_SOURCE,
+ TAG_FLASH,
+ TAG_FLASHPIX_VERSION,
+ TAG_FLASH_ENERGY,
+ TAG_FOCAL_LENGTH,
+ TAG_FOCAL_LENGTH_IN_35MM_FILM,
+ TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+ TAG_FOCAL_PLANE_X_RESOLUTION,
+ TAG_FOCAL_PLANE_Y_RESOLUTION,
+ TAG_F_NUMBER,
+ TAG_GAIN_CONTROL,
+ TAG_GAMMA,
+ TAG_GPS_ALTITUDE,
+ TAG_GPS_ALTITUDE_REF,
+ TAG_GPS_AREA_INFORMATION,
+ TAG_GPS_DATESTAMP,
+ TAG_GPS_DEST_BEARING,
+ TAG_GPS_DEST_BEARING_REF,
+ TAG_GPS_DEST_DISTANCE,
+ TAG_GPS_DEST_DISTANCE_REF,
+ TAG_GPS_DEST_LATITUDE,
+ TAG_GPS_DEST_LATITUDE_REF,
+ TAG_GPS_DEST_LONGITUDE,
+ TAG_GPS_DEST_LONGITUDE_REF,
+ TAG_GPS_DIFFERENTIAL,
+ TAG_GPS_DOP,
+ TAG_GPS_H_POSITIONING_ERROR,
+ TAG_GPS_IMG_DIRECTION,
+ TAG_GPS_IMG_DIRECTION_REF,
+ TAG_GPS_LATITUDE,
+ TAG_GPS_LATITUDE_REF,
+ TAG_GPS_LONGITUDE,
+ TAG_GPS_LONGITUDE_REF,
+ TAG_GPS_MAP_DATUM,
+ TAG_GPS_MEASURE_MODE,
+ TAG_GPS_PROCESSING_METHOD,
+ TAG_GPS_SATELLITES,
+ TAG_GPS_SPEED,
+ TAG_GPS_SPEED_REF,
+ TAG_GPS_STATUS,
+ TAG_GPS_TIMESTAMP,
+ TAG_GPS_TRACK,
+ TAG_GPS_TRACK_REF,
+ TAG_GPS_VERSION_ID,
+ TAG_IMAGE_DESCRIPTION,
+ TAG_IMAGE_LENGTH,
+ TAG_IMAGE_UNIQUE_ID,
+ TAG_IMAGE_WIDTH,
+ TAG_INTEROPERABILITY_INDEX,
+ TAG_ISO_SPEED,
+ TAG_ISO_SPEED_LATITUDE_YYY,
+ TAG_ISO_SPEED_LATITUDE_ZZZ,
+ TAG_JPEG_INTERCHANGE_FORMAT,
+ TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+ TAG_LENS_MAKE,
+ TAG_LENS_MODEL,
+ TAG_LENS_SERIAL_NUMBER,
+ TAG_LENS_SPECIFICATION,
+ TAG_LIGHT_SOURCE,
+ TAG_MAKE,
+ TAG_MAKER_NOTE,
+ TAG_MAX_APERTURE_VALUE,
+ TAG_METERING_MODE,
+ TAG_MODEL,
+ TAG_NEW_SUBFILE_TYPE,
+ TAG_OECF,
+ TAG_OFFSET_TIME,
+ TAG_OFFSET_TIME_DIGITIZED,
+ TAG_OFFSET_TIME_ORIGINAL,
+ TAG_ORF_ASPECT_FRAME,
+ TAG_ORF_PREVIEW_IMAGE_LENGTH,
+ TAG_ORF_PREVIEW_IMAGE_START,
+ TAG_ORF_THUMBNAIL_IMAGE,
+ TAG_ORIENTATION,
+ TAG_PHOTOGRAPHIC_SENSITIVITY,
+ TAG_PHOTOMETRIC_INTERPRETATION,
+ TAG_PIXEL_X_DIMENSION,
+ TAG_PIXEL_Y_DIMENSION,
+ TAG_PLANAR_CONFIGURATION,
+ TAG_PRIMARY_CHROMATICITIES,
+ TAG_RECOMMENDED_EXPOSURE_INDEX,
+ TAG_REFERENCE_BLACK_WHITE,
+ TAG_RELATED_SOUND_FILE,
+ TAG_RESOLUTION_UNIT,
+ TAG_ROWS_PER_STRIP,
+ TAG_RW2_ISO,
+ TAG_RW2_JPG_FROM_RAW,
+ TAG_RW2_SENSOR_BOTTOM_BORDER,
+ TAG_RW2_SENSOR_LEFT_BORDER,
+ TAG_RW2_SENSOR_RIGHT_BORDER,
+ TAG_RW2_SENSOR_TOP_BORDER,
+ TAG_SAMPLES_PER_PIXEL,
+ TAG_SATURATION,
+ TAG_SCENE_CAPTURE_TYPE,
+ TAG_SCENE_TYPE,
+ TAG_SENSING_METHOD,
+ TAG_SENSITIVITY_TYPE,
+ TAG_SHARPNESS,
+ TAG_SHUTTER_SPEED_VALUE,
+ TAG_SOFTWARE,
+ TAG_SPATIAL_FREQUENCY_RESPONSE,
+ TAG_SPECTRAL_SENSITIVITY,
+ TAG_STANDARD_OUTPUT_SENSITIVITY,
+ TAG_STRIP_BYTE_COUNTS,
+ TAG_STRIP_OFFSETS,
+ TAG_SUBFILE_TYPE,
+ TAG_SUBJECT_AREA,
+ TAG_SUBJECT_DISTANCE,
+ TAG_SUBJECT_DISTANCE_RANGE,
+ TAG_SUBJECT_LOCATION,
+ TAG_SUBSEC_TIME,
+ TAG_SUBSEC_TIME_DIGITIZED,
+ TAG_SUBSEC_TIME_ORIGINAL,
+ TAG_THUMBNAIL_IMAGE_LENGTH,
+ TAG_THUMBNAIL_IMAGE_WIDTH,
+ TAG_TRANSFER_FUNCTION,
+ TAG_USER_COMMENT,
+ TAG_WHITE_BALANCE,
+ TAG_WHITE_POINT,
+ TAG_XMP,
+ TAG_X_RESOLUTION,
+ TAG_Y_CB_CR_COEFFICIENTS,
+ TAG_Y_CB_CR_POSITIONING,
+ TAG_Y_CB_CR_SUB_SAMPLING,
+ TAG_Y_RESOLUTION
+ )
+
+ fun toJson(): JSObject {
+ val ret = JSObject()
+ if (exif == null) {
+ return ret
+ }
+ for (i in attributes.indices) {
+ p(ret, attributes[i])
+ }
+ return ret
+ }
+
+ fun p(o: JSObject, tag: String?) {
+ val value = exif!!.getAttribute(tag!!)
+ o.put(tag, value)
+ }
+
+ fun copyExif(destFile: String?) {
+ try {
+ val destExif = ExifInterface(
+ destFile!!
+ )
+ for (i in attributes.indices) {
+ val value = exif!!.getAttribute(attributes[i])
+ if (value != null) {
+ destExif.setAttribute(attributes[i], value)
+ }
+ }
+ destExif.saveAttributes()
+ } catch (_: java.lang.Exception) {
+ }
+ }
+
+ fun resetOrientation() {
+ exif!!.resetOrientation()
+ }
+}
diff --git a/plugins/camera/android/src/main/java/app/tauri/camera/ImageUtils.kt b/plugins/camera/android/src/main/java/app/tauri/camera/ImageUtils.kt
new file mode 100644
index 00000000..b6369cf2
--- /dev/null
+++ b/plugins/camera/android/src/main/java/app/tauri/camera/ImageUtils.kt
@@ -0,0 +1,126 @@
+package app.tauri.camera
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Matrix
+import android.net.Uri
+import androidx.exifinterface.media.ExifInterface
+import app.tauri.Logger
+import java.io.IOException
+import java.io.InputStream
+import java.lang.Integer.min
+import kotlin.math.roundToInt
+
+object ImageUtils {
+ /**
+ * Resize an image to the given max width and max height. Constraint can be put
+ * on one dimension, or both. Resize will always preserve aspect ratio.
+ * @param bitmap
+ * @param desiredMaxWidth
+ * @param desiredMaxHeight
+ * @return a new, scaled Bitmap
+ */
+ fun resize(bitmap: Bitmap, desiredMaxWidth: Int, desiredMaxHeight: Int): Bitmap {
+ return resizePreservingAspectRatio(bitmap, desiredMaxWidth, desiredMaxHeight)
+ }
+
+ /**
+ * Resize an image to the given max width and max height. Constraint can be put
+ * on one dimension, or both. Resize will always preserve aspect ratio.
+ * @param bitmap
+ * @param desiredMaxWidth
+ * @param desiredMaxHeight
+ * @return a new, scaled Bitmap
+ */
+ private fun resizePreservingAspectRatio(
+ bitmap: Bitmap,
+ desiredMaxWidth: Int,
+ desiredMaxHeight: Int
+ ): Bitmap {
+ val width = bitmap.width
+ val height = bitmap.height
+
+ // 0 is treated as 'no restriction'
+ val maxHeight = if (desiredMaxHeight == 0) height else desiredMaxHeight
+ val maxWidth = if (desiredMaxWidth == 0) width else desiredMaxWidth
+
+ // resize with preserved aspect ratio
+ var newWidth = min(width, maxWidth).toFloat()
+ var newHeight = height * newWidth / width
+ if (newHeight > maxHeight) {
+ newWidth = (width * maxHeight / height).toFloat()
+ newHeight = maxHeight.toFloat()
+ }
+ return Bitmap.createScaledBitmap(bitmap, newWidth.roundToInt(), newHeight.roundToInt(), false)
+ }
+
+ /**
+ * Transform an image with the given matrix
+ * @param bitmap
+ * @param matrix
+ * @return
+ */
+ private fun transform(bitmap: Bitmap, matrix: Matrix): Bitmap {
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+ }
+
+ /**
+ * Correct the orientation of an image by reading its exif information and rotating
+ * the appropriate amount for portrait mode
+ * @param bitmap
+ * @param imageUri
+ * @param exif
+ * @return
+ */
+ @Throws(IOException::class)
+ fun correctOrientation(c: Context, bitmap: Bitmap, imageUri: Uri, exif: ExifWrapper): Bitmap {
+ val orientation = getOrientation(c, imageUri)
+ return if (orientation != 0) {
+ val matrix = Matrix()
+ matrix.postRotate(orientation.toFloat())
+ exif.resetOrientation()
+ transform(bitmap, matrix)
+ } else {
+ bitmap
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun getOrientation(c: Context, imageUri: Uri): Int {
+ var result = 0
+ c.getContentResolver().openInputStream(imageUri).use { iStream ->
+ val exifInterface = ExifInterface(iStream!!)
+ val orientation: Int = exifInterface.getAttributeInt(
+ ExifInterface.TAG_ORIENTATION,
+ ExifInterface.ORIENTATION_NORMAL
+ )
+ if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
+ result = 90
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
+ result = 180
+ } else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
+ result = 270
+ }
+ }
+ return result
+ }
+
+ fun getExifData(c: Context, bitmap: Bitmap?, imageUri: Uri): ExifWrapper {
+ var stream: InputStream? = null
+ try {
+ stream = c.getContentResolver().openInputStream(imageUri)
+ val exifInterface = ExifInterface(stream!!)
+ return ExifWrapper(exifInterface)
+ } catch (ex: IOException) {
+ Logger.error("Error loading exif data from image", ex)
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close()
+ } catch (ignored: IOException) {
+ }
+ }
+ }
+ return ExifWrapper(null)
+ }
+}
diff --git a/plugins/camera/android/src/test/java/app/tauri/camera/ExampleUnitTest.kt b/plugins/camera/android/src/test/java/app/tauri/camera/ExampleUnitTest.kt
new file mode 100644
index 00000000..7547c0b3
--- /dev/null
+++ b/plugins/camera/android/src/test/java/app/tauri/camera/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package app.tauri.camera
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/plugins/camera/build.rs b/plugins/camera/build.rs
new file mode 100644
index 00000000..6a5bcf5d
--- /dev/null
+++ b/plugins/camera/build.rs
@@ -0,0 +1,16 @@
+// Copyright 2019-2022 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::process::exit;
+
+fn main() {
+ if let Err(error) = tauri_build::mobile::PluginBuilder::new()
+ .android_path("android")
+ .ios_path("ios")
+ .run()
+ {
+ println!("{error:#}");
+ exit(1);
+ }
+}
diff --git a/plugins/camera/examples/tauri-app/.gitignore b/plugins/camera/examples/tauri-app/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/plugins/camera/examples/tauri-app/README.md b/plugins/camera/examples/tauri-app/README.md
new file mode 100644
index 00000000..21592436
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/README.md
@@ -0,0 +1 @@
+# Camera Example
diff --git a/plugins/camera/examples/tauri-app/index.html b/plugins/camera/examples/tauri-app/index.html
new file mode 100644
index 00000000..fad1c5d9
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Tauri + Svelte
+
+
+
+
+
+
+
diff --git a/plugins/camera/examples/tauri-app/jsconfig.json b/plugins/camera/examples/tauri-app/jsconfig.json
new file mode 100644
index 00000000..ee5e92f2
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/jsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "moduleResolution": "Node",
+ "target": "ESNext",
+ "module": "ESNext",
+ /**
+ * svelte-preprocess cannot figure out whether you have
+ * a value or a type, so tell TypeScript to enforce using
+ * `import type` instead of `import` for Types.
+ */
+ "importsNotUsedAsValues": "error",
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ /**
+ * To have warnings / errors of the Svelte compiler at the
+ * correct position, enable source maps by default.
+ */
+ "sourceMap": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ /**
+ * Typecheck JS in `.svelte` and `.js` files by default.
+ * Disable this if you'd like to use dynamic types.
+ */
+ "checkJs": true
+ },
+ /**
+ * Use global.d.ts instead of compilerOptions.types
+ * to avoid limiting type declarations.
+ */
+ "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
+}
diff --git a/plugins/camera/examples/tauri-app/package.json b/plugins/camera/examples/tauri-app/package.json
new file mode 100644
index 00000000..a6cc8e02
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "tauri-app",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "tauri": "tauri"
+ },
+ "dependencies": {
+ "@tauri-apps/api": "^1.1.0",
+ "tauri-plugin-camera-api": "link:../../"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^1.0.1",
+ "@tauri-apps/cli": "^2.0.0-alpha.0",
+ "internal-ip": "^7.0.0",
+ "svelte": "^3.49.0",
+ "vite": "^3.0.2"
+ }
+}
diff --git a/plugins/camera/examples/tauri-app/src-tauri/.gitignore b/plugins/camera/examples/tauri-app/src-tauri/.gitignore
new file mode 100644
index 00000000..c055de2d
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/.gitignore
@@ -0,0 +1,5 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+.cargo
diff --git a/plugins/camera/examples/tauri-app/src-tauri/Cargo.lock b/plugins/camera/examples/tauri-app/src-tauri/Cargo.lock
new file mode 100644
index 00000000..13a0a9d5
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/Cargo.lock
@@ -0,0 +1,3515 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
+
+[[package]]
+name = "app"
+version = "0.0.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-camera",
+]
+
+[[package]]
+name = "atk"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf"
+dependencies = [
+ "atk-sys",
+ "bitflags",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "attohttpc"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2"
+dependencies = [
+ "flate2",
+ "http",
+ "log",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "url",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "brotli"
+version = "3.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bstr"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f0778972c64420fdedc63f09919c8a88bda7b25135357fd25a5d9f3257e832"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+
+[[package]]
+name = "bytemuck"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c041d3eab048880cb0b86b256447da3f18859a163c3b8d8893f4e6368abe6393"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+
+[[package]]
+name = "cairo-rs"
+version = "0.16.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d"
+dependencies = [
+ "bitflags",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "cargo_toml"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bfbc36312494041e2cdd5f06697b7e89d4b76f42773a0b5556ac290ff22acc2"
+dependencies = [
+ "serde",
+ "toml",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+dependencies = [
+ "byteorder",
+ "fnv",
+ "uuid",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cocoa"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
+dependencies = [
+ "bitflags",
+ "block",
+ "cocoa-foundation",
+ "core-foundation",
+ "core-graphics",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318"
+dependencies = [
+ "bitflags",
+ "block",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "combine"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "core-graphics"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa 0.4.8",
+ "matches",
+ "phf 0.8.0",
+ "proc-macro2",
+ "quote",
+ "smallvec",
+ "syn",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "cty"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
+
+[[package]]
+name = "darling"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dbus"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "winapi",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version 0.4.0",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dtoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c"
+
+[[package]]
+name = "embed_plist"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "field-offset"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92"
+dependencies = [
+ "memoffset",
+ "rustc_version 0.3.3",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-task"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
+
+[[package]]
+name = "futures-util"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "gdk"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1"
+dependencies = [
+ "bitflags",
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.16.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05"
+dependencies = [
+ "bitflags",
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4511710212ed3020b61a8622a37aa6f0dd2a84516575da92e9b96928dcbe83ba"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa2bf8b5b8c414bc5d05e48b271896d0fd3ddb57464a3108438082da61de6af"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generator"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d266041a359dfa931b370ef684cceb84b166beb14f7f0421f4a6a3d0c446d12e"
+dependencies = [
+ "cc",
+ "libc",
+ "log",
+ "rustversion",
+ "windows 0.39.0",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "gio"
+version = "0.16.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.16.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd4df61a866ed7259d6189b8bcb1464989a77f1d85d25d002279bbe9dd38b2f"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf"
+dependencies = [
+ "anyhow",
+ "heck 0.4.1",
+ "proc-macro-crate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "globset"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6"
+dependencies = [
+ "atk",
+ "bitflags",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "once_cell",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff"
+dependencies = [
+ "anyhow",
+ "proc-macro-crate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "http"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa 1.0.5",
+]
+
+[[package]]
+name = "http-range"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
+
+[[package]]
+name = "ico"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae"
+dependencies = [
+ "byteorder",
+ "png",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
+dependencies = [
+ "crossbeam-utils",
+ "globset",
+ "lazy_static",
+ "log",
+ "memchr",
+ "regex",
+ "same-file",
+ "thread_local",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "image"
+version = "0.24.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "infer"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a898e4b7951673fce96614ce5751d13c40fc5674bc2d759288e46c3ab62598b3"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "itoa"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "110b9902c80c12bf113c432d0b71c7a94490b294a8234f326fd0abca2fac0b00"
+dependencies = [
+ "bitflags",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98a216519a52cd941a733a0ad3f1023cfdb1cd47f3955e8e863ed56f558f916c"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jni"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "js-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e712e62827c382a77b87f590532febb1f8b2fdbc3eefa1ee37fe7281687075ef"
+dependencies = [
+ "serde",
+ "serde_json",
+ "thiserror",
+ "treediff",
+]
+
+[[package]]
+name = "kuchiki"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358"
+dependencies = [
+ "cssparser",
+ "html5ever",
+ "matches",
+ "selectors",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f8d7ae751e1cb825c840ae5e682f59b098cdfd213c350ac268b61449a5f58a0"
+dependencies = [
+ "pkg-config",
+]
+
+[[package]]
+name = "line-wrap"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
+dependencies = [
+ "safemem",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "loom"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
+dependencies = [
+ "cfg-if",
+ "generator",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "mac-notification-sys"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e72d50edb17756489e79d52eb146927bec8eba9dd48faadf9ef08bca3791ad5"
+dependencies = [
+ "cc",
+ "dirs-next",
+ "objc-foundation",
+ "objc_id",
+ "time",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "markup5ever"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
+dependencies = [
+ "log",
+ "phf 0.8.0",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "ndk"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4"
+dependencies = [
+ "bitflags",
+ "jni-sys",
+ "ndk-sys",
+ "num_enum",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97"
+dependencies = [
+ "jni-sys",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "nom8"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "notify-rust"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "260208751689b605138bb55ab6af43ad75f628619a7e0b818d63bf6629e59467"
+dependencies = [
+ "dbus",
+ "mac-notification-sys",
+ "tauri-winrt-notification",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0072973714303aa6e3631c7e8e777970cf4bdd25dc4932e41031027b8bcc4e"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0629cbd6b897944899b1f10496d9c4a7ac5878d45fd61bc22e9e79bfbbc29597"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+ "objc_exception",
+]
+
+[[package]]
+name = "objc-foundation"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
+dependencies = [
+ "block",
+ "objc",
+ "objc_id",
+]
+
+[[package]]
+name = "objc_exception"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc_id"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
+dependencies = [
+ "objc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+
+[[package]]
+name = "open"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8"
+dependencies = [
+ "pathdiff",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "os_info"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5209b2162b2c140df493a93689e04f8deab3a67634f5bc7a553c0a98e5b8d399"
+dependencies = [
+ "log",
+ "serde",
+ "winapi",
+]
+
+[[package]]
+name = "os_pipe"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a53dbb20faf34b16087a931834cba2d7a73cc74af2b7ef345a4c8324e2409a12"
+dependencies = [
+ "libc",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "pango"
+version = "0.16.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94"
+dependencies = [
+ "bitflags",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "pathdiff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[package]]
+name = "pest"
+version = "2.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "028accff104c4e513bad663bbcd2ad7cfd5304144404c31ed0a77ac103d00660"
+dependencies = [
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_macros 0.8.0",
+ "phf_shared 0.8.0",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
+dependencies = [
+ "phf_macros 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared 0.8.0",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "plist"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5329b8f106a176ab0dce4aae5da86bfcb139bb74fb00882859e03745011f3635"
+dependencies = [
+ "base64 0.13.1",
+ "indexmap",
+ "line-wrap",
+ "quick-xml 0.26.0",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "png"
+version = "0.17.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34"
+dependencies = [
+ "once_cell",
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.8",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a"
+dependencies = [
+ "cty",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom 0.2.8",
+ "redox_syscall",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rfd"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fe3bae427b011620cf4436d5dd7405c1a86dce3dfcdc0cda12b41fd31569ac3"
+dependencies = [
+ "block",
+ "dispatch",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "js-sys",
+ "log",
+ "objc",
+ "objc-foundation",
+ "objc_id",
+ "raw-window-handle",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows 0.44.0",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
+dependencies = [
+ "semver 0.11.0",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver 1.0.16",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
+
+[[package]]
+name = "ryu"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "selectors"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
+dependencies = [
+ "bitflags",
+ "cssparser",
+ "derive_more",
+ "fxhash",
+ "log",
+ "matches",
+ "phf 0.8.0",
+ "phf_codegen",
+ "precomputed-hash",
+ "servo_arc",
+ "smallvec",
+ "thin-slice",
+]
+
+[[package]]
+name = "semver"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
+dependencies = [
+ "pest",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
+dependencies = [
+ "itoa 1.0.5",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa 1.0.5",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff"
+dependencies = [
+ "serde",
+ "serde_with_macros",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serialize-to-javascript"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serialize-to-javascript-impl",
+]
+
+[[package]]
+name = "serialize-to-javascript-impl"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432"
+dependencies = [
+ "nodrop",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shared_child"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "soup3"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82bc46048125fefd69d30b32b9d263d6556c9ffe82a7a7df181a86d912da5616"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "014bbeb1c4cdb30739dc181e8d98b7908f124d9555843afa89b5570aaf4ec62b"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "state"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b"
+dependencies = [
+ "loom",
+]
+
+[[package]]
+name = "string_cache"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08"
+dependencies = [
+ "new_debug_unreachable",
+ "once_cell",
+ "parking_lot",
+ "phf_shared 0.10.0",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "strum"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb"
+dependencies = [
+ "heck 0.3.3",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "swift-rs"
+version = "0.3.0"
+source = "git+https://github.com/Brendonovich/swift-rs?rev=eb6de914ad57501da5019154d476d45660559999#eb6de914ad57501da5019154d476d45660559999"
+dependencies = [
+ "base64 0.13.1",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff"
+dependencies = [
+ "cfg-expr",
+ "heck 0.4.1",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d16138f5d521fcde40580e1a34df784b063dd9ac05c7cbe344bf01f02a23be"
+dependencies = [
+ "bitflags",
+ "cairo-rs",
+ "cc",
+ "cocoa",
+ "core-foundation",
+ "core-graphics",
+ "crossbeam-channel",
+ "dispatch",
+ "gdk",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "glib-sys",
+ "gtk",
+ "image",
+ "instant",
+ "jni",
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "objc",
+ "once_cell",
+ "parking_lot",
+ "png",
+ "raw-window-handle",
+ "scopeguard",
+ "serde",
+ "tao-macros",
+ "unicode-segmentation",
+ "uuid",
+ "windows 0.44.0",
+ "windows-implement",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b27a4bcc5eb524658234589bdffc7e7bfb996dbae6ce9393bfd39cb4159b445"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "tauri"
+version = "2.0.0-alpha.3"
+source = "git+https://github.com/tauri-apps/tauri?branch=next#dffd8eb5a87558afc2fca21870643ff7dad9eb04"
+dependencies = [
+ "anyhow",
+ "attohttpc",
+ "cocoa",
+ "dirs-next",
+ "embed_plist",
+ "encoding_rs",
+ "flate2",
+ "futures-util",
+ "glib",
+ "glob",
+ "gtk",
+ "heck 0.4.1",
+ "http",
+ "ignore",
+ "jni",
+ "libc",
+ "log",
+ "notify-rust",
+ "objc",
+ "once_cell",
+ "open",
+ "os_info",
+ "os_pipe",
+ "percent-encoding",
+ "rand 0.8.5",
+ "raw-window-handle",
+ "regex",
+ "rfd",
+ "semver 1.0.16",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serialize-to-javascript",
+ "shared_child",
+ "state",
+ "swift-rs",
+ "tar",
+ "tauri-build",
+ "tauri-macros",
+ "tauri-runtime",
+ "tauri-runtime-wry",
+ "tauri-utils",
+ "tempfile",
+ "thiserror",
+ "tokio",
+ "url",
+ "uuid",
+ "webkit2gtk",
+ "webview2-com",
+ "windows 0.44.0",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.0.0-alpha.1"
+source = "git+https://github.com/tauri-apps/tauri?branch=next#dffd8eb5a87558afc2fca21870643ff7dad9eb04"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "filetime",
+ "heck 0.4.1",
+ "json-patch",
+ "semver 1.0.16",
+ "serde",
+ "serde_json",
+ "swift-rs",
+ "tauri-utils",
+ "tauri-winres",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.0.0-alpha.1"
+source = "git+https://github.com/tauri-apps/tauri?branch=next#dffd8eb5a87558afc2fca21870643ff7dad9eb04"
+dependencies = [
+ "base64 0.21.0",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "semver 1.0.16",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tauri-utils",
+ "thiserror",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.0.0-alpha.1"
+source = "git+https://github.com/tauri-apps/tauri?branch=next#dffd8eb5a87558afc2fca21870643ff7dad9eb04"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-plugin-camera"
+version = "0.0.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "thiserror",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "0.13.0-alpha.1"
+source = "git+https://github.com/tauri-apps/tauri?branch=next#dffd8eb5a87558afc2fca21870643ff7dad9eb04"
+dependencies = [
+ "gtk",
+ "http",
+ "http-range",
+ "jni",
+ "rand 0.8.5",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror",
+ "url",
+ "uuid",
+ "webview2-com",
+ "windows 0.44.0",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "0.13.0-alpha.1"
+source = "git+https://github.com/tauri-apps/tauri?branch=next#dffd8eb5a87558afc2fca21870643ff7dad9eb04"
+dependencies = [
+ "cocoa",
+ "gtk",
+ "jni",
+ "percent-encoding",
+ "rand 0.8.5",
+ "raw-window-handle",
+ "tauri-runtime",
+ "tauri-utils",
+ "uuid",
+ "webkit2gtk",
+ "webview2-com",
+ "windows 0.44.0",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.0.0-alpha.1"
+source = "git+https://github.com/tauri-apps/tauri?branch=next#dffd8eb5a87558afc2fca21870643ff7dad9eb04"
+dependencies = [
+ "brotli",
+ "ctor",
+ "glob",
+ "heck 0.4.1",
+ "html5ever",
+ "infer",
+ "json-patch",
+ "kuchiki",
+ "memchr",
+ "phf 0.10.1",
+ "proc-macro2",
+ "quote",
+ "semver 1.0.16",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "thiserror",
+ "url",
+ "walkdir",
+ "windows 0.44.0",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b7a78dc04f75fb5ab815e66ac561c81e92a968a40f29e7c21afd152d694fad8"
+dependencies = [
+ "toml",
+ "version_check",
+]
+
+[[package]]
+name = "tauri-winrt-notification"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c58de036c4d2e20717024de2a3c4bf56c301f07b21bc8ef9b57189fce06f1f3b"
+dependencies = [
+ "quick-xml 0.23.1",
+ "strum",
+ "windows 0.39.0",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "thin-slice"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
+
+[[package]]
+name = "thiserror"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "time"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2"
+dependencies = [
+ "itoa 1.0.5",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+
+[[package]]
+name = "time-macros"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c"
+dependencies = [
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "memchr",
+ "num_cpus",
+ "pin-project-lite",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5"
+
+[[package]]
+name = "toml_edit"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
+dependencies = [
+ "indexmap",
+ "nom8",
+ "toml_datetime",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
+dependencies = [
+ "lazy_static",
+ "log",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "treediff"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303"
+dependencies = [
+ "serde_json",
+]
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
+
+[[package]]
+name = "url"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "uuid"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
+dependencies = [
+ "getrandom 0.2.8",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[package]]
+name = "version-compare"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+
+[[package]]
+name = "web-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8eea819afe15eb8dcdff4f19d8bfda540bae84d874c10e6f4b8faf2d6704bd1"
+dependencies = [
+ "bitflags",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0ac7a95ddd3fdfcaf83d8e513b4b1ad101b95b413b6aa6662ed95f284fc3d5b"
+dependencies = [
+ "bitflags",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11296e5daf3a653b79bf47d66c380e4143d5b9c975818871179a3bda79499562"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows 0.44.0",
+ "windows-implement",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cde542bed28058a5b028d459689ee57f1d06685bb6c266da3b91b1be6703952f"
+dependencies = [
+ "regex",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "windows 0.44.0",
+ "windows-bindgen",
+ "windows-metadata",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a"
+dependencies = [
+ "windows_aarch64_msvc 0.39.0",
+ "windows_i686_gnu 0.39.0",
+ "windows_i686_msvc 0.39.0",
+ "windows_x86_64_gnu 0.39.0",
+ "windows_x86_64_msvc 0.39.0",
+]
+
+[[package]]
+name = "windows"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-bindgen"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "222204ecf46521382a4d88b4a1bbefca9f8855697b4ab7d20803901425e061a3"
+dependencies = [
+ "windows-metadata",
+ "windows-tokens",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-metadata"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee78911e3f4ce32c1ad9d3c7b0bd95389662ad8d8f1a3155688fed70bd96e2b6"
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.1",
+]
+
+[[package]]
+name = "windows-tokens"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4251900975a0d10841c5d4bde79c56681543367ef811f3fabb8d1803b0959b"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
+
+[[package]]
+name = "wry"
+version = "0.27.0"
+source = "git+https://github.com/tauri-apps/wry?branch=dev#9975dda12a122051b375ec6485650546d2d91c26"
+dependencies = [
+ "base64 0.13.1",
+ "block",
+ "cocoa",
+ "core-graphics",
+ "crossbeam-channel",
+ "dunce",
+ "gdk",
+ "gio",
+ "glib",
+ "gtk",
+ "html5ever",
+ "http",
+ "kuchiki",
+ "libc",
+ "log",
+ "objc",
+ "objc_id",
+ "once_cell",
+ "serde",
+ "serde_json",
+ "sha2",
+ "soup3",
+ "tao",
+ "thiserror",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows 0.44.0",
+ "windows-implement",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "xattr"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
+dependencies = [
+ "libc",
+]
diff --git a/plugins/camera/examples/tauri-app/src-tauri/Cargo.toml b/plugins/camera/examples/tauri-app/src-tauri/Cargo.toml
new file mode 100644
index 00000000..aa341f89
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/Cargo.toml
@@ -0,0 +1,27 @@
+workspace = {}
+
+[package]
+name = "app"
+version = "0.0.0"
+description = "A Tauri App"
+authors = ["you"]
+license = ""
+repository = ""
+edition = "2021"
+rust-version = "1.64"
+
+[lib]
+crate-type = ["staticlib", "cdylib", "rlib"]
+
+[build-dependencies]
+tauri-build = { git = "https://github.com/tauri-apps/tauri", branch = "next", features = [] }
+
+[dependencies]
+serde_json = "1.0"
+serde = { version = "1.0", features = ["derive"] }
+tauri = { git = "https://github.com/tauri-apps/tauri", branch = "next", features = ["api-all"] }
+tauri-plugin-camera = { path = "../../../" }
+
+[features]
+# DO NOT remove this
+custom-protocol = [ "tauri/custom-protocol" ]
diff --git a/plugins/camera/examples/tauri-app/src-tauri/build.rs b/plugins/camera/examples/tauri-app/src-tauri/build.rs
new file mode 100644
index 00000000..795b9b7c
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ tauri_build::build()
+}
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/.editorconfig b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/.editorconfig
new file mode 100644
index 00000000..ebe51d3b
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/.editorconfig
@@ -0,0 +1,12 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = false
+insert_final_newline = false
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/.gitignore b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/.gitignore
new file mode 100644
index 00000000..671e5074
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/.gitignore
@@ -0,0 +1,18 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+
+/tauri-plugins
+/tauri-api
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/.gitignore b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/.gitignore
new file mode 100644
index 00000000..e897324f
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/.gitignore
@@ -0,0 +1 @@
+/src/main/java/app/tauri/app/generated
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/build.gradle.kts b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/build.gradle.kts
new file mode 100644
index 00000000..7e709df9
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/build.gradle.kts
@@ -0,0 +1,117 @@
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("rustPlugin")
+}
+
+android {
+ compileSdk = 33
+ defaultConfig {
+ manifestPlaceholders["usesCleartextTraffic"] = "false"
+ applicationId = "app.tauri.app"
+ minSdk = 24
+ targetSdk = 33
+ versionCode = 1
+ versionName = "1.0"
+ }
+ sourceSets.getByName("main") {
+ // Vulkan validation layers
+ val ndkHome = System.getenv("NDK_HOME")
+ jniLibs.srcDir("${ndkHome}/sources/third_party/vulkan/src/build-android/jniLibs")
+ }
+ buildTypes {
+ getByName("debug") {
+ manifestPlaceholders["usesCleartextTraffic"] = "true"
+ isDebuggable = true
+ isJniDebuggable = true
+ isMinifyEnabled = false
+ packagingOptions {
+ jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
+
+ jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
+
+ jniLibs.keepDebugSymbols.add("*/x86/*.so")
+
+ jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
+ }
+ }
+ getByName("release") {
+ isMinifyEnabled = true
+ val proguards = fileTree(".") {
+ include("*.pro")
+ }
+ proguardFiles(*proguards.toList().toTypedArray())
+ }
+ }
+ flavorDimensions.add("abi")
+ productFlavors {
+ create("universal") {
+ val abiList = findProperty("abiList") as? String
+
+ dimension = "abi"
+ ndk {
+ abiFilters += abiList?.split(",")?.map { it.trim() } ?: listOf( "arm64-v8a", "armeabi-v7a", "x86", "x86_64",
+ )
+ }
+ }
+ create("arm64") {
+ dimension = "abi"
+ ndk {
+ abiFilters += listOf("arm64-v8a")
+ }
+ }
+
+ create("arm") {
+ dimension = "abi"
+ ndk {
+ abiFilters += listOf("armeabi-v7a")
+ }
+ }
+
+ create("x86") {
+ dimension = "abi"
+ ndk {
+ abiFilters += listOf("x86")
+ }
+ }
+
+ create("x86_64") {
+ dimension = "abi"
+ ndk {
+ abiFilters += listOf("x86_64")
+ }
+ }
+ }
+
+ assetPacks += mutableSetOf()
+ namespace = "app.tauri.app"
+}
+
+rust {
+ rootDirRel = "../../../../"
+ targets = listOf("aarch64", "armv7", "i686", "x86_64")
+ arches = listOf("arm64", "arm", "x86", "x86_64")
+}
+
+dependencies {
+ implementation("androidx.webkit:webkit:1.5.0")
+ implementation("androidx.appcompat:appcompat:1.5.1")
+ implementation("com.google.android.material:material:1.7.0")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.4")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
+ implementation(project(":tauri-android"))
+}
+
+apply(from = "tauri.build.gradle.kts")
+
+afterEvaluate {
+ android.applicationVariants.all {
+ tasks["mergeUniversalReleaseJniLibFolders"].dependsOn(tasks["rustBuildRelease"])
+ tasks["mergeUniversalDebugJniLibFolders"].dependsOn(tasks["rustBuildDebug"])
+ productFlavors.filter{ it.name != "universal" }.forEach { _ ->
+ val archAndBuildType = name.capitalize()
+ tasks["merge${archAndBuildType}JniLibFolders"].dependsOn(tasks["rustBuild${archAndBuildType}"])
+ }
+ }
+}
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/proguard-rules.pro b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/proguard-tauri.pro b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/proguard-tauri.pro
new file mode 100644
index 00000000..b4eb481e
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/proguard-tauri.pro
@@ -0,0 +1,24 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+
+-keep class app.tauri.app.TauriActivity {
+ getAppClass(...);
+ getVersion();
+}
+
+-keep class app.tauri.app.RustWebView {
+ public (...);
+ loadUrlMainThread(...);
+}
+
+-keep class app.tauri.app.Ipc {
+ public (...);
+ @android.webkit.JavascriptInterface public ;
+}
+
+-keep class app.tauri.app.RustWebChromeClient,app.tauri.app.RustWebViewClient {
+ public (...);
+}
+
+-keep class app.tauri.app.MainActivity {
+ public getPluginManager();
+}
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/AndroidManifest.xml b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..36df901f
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/java/app/tauri/app/MainActivity.kt b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/java/app/tauri/app/MainActivity.kt
new file mode 100644
index 00000000..3dff0081
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/java/app/tauri/app/MainActivity.kt
@@ -0,0 +1,7 @@
+package app.tauri.app
+
+import app.tauri.plugin.PluginManager
+
+class MainActivity : TauriActivity() {
+ var pluginManager: PluginManager = PluginManager(this)
+}
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/jniLibs/arm64-v8a/libapp.so b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/jniLibs/arm64-v8a/libapp.so
new file mode 120000
index 00000000..9f1bb9d3
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/jniLibs/arm64-v8a/libapp.so
@@ -0,0 +1 @@
+/home/lucas/projects/tauri/plugins-workspace/plugins/camera/examples/tauri-app/src-tauri/target/aarch64-linux-android/debug/libapp.so
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/jniLibs/x86/libapp.so b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/jniLibs/x86/libapp.so
new file mode 120000
index 00000000..da9e43a8
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/jniLibs/x86/libapp.so
@@ -0,0 +1 @@
+/home/lucas/projects/tauri/plugins-workspace/plugins/camera/examples/tauri-app/src-tauri/target/i686-linux-android/debug/libapp.so
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/drawable/ic_launcher_background.xml b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/layout/activity_main.xml b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..4fc24441
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-hdpi/ic_launcher.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..28f1aa11
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..85d0c88a
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..28f1aa11
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-mdpi/ic_launcher.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..73e48dbf
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..13dd2147
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..73e48dbf
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..1d98044f
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..a888b336
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..1d98044f
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..08183246
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..a2a838e7
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..08183246
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..b18bceb6
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..3f8a57f3
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..b18bceb6
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values-night/themes.xml b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000..7cd5d350
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values/colors.xml b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..f8c6127d
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values/strings.xml b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..f664d9a2
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ Tauri Camera
+ Camera
+
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values/themes.xml b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..04239cf4
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/xml/file_paths.xml b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 00000000..782d63b9
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/tauri.build.gradle.kts b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/tauri.build.gradle.kts
new file mode 100644
index 00000000..27d35939
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/app/tauri.build.gradle.kts
@@ -0,0 +1,8 @@
+// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+
+val implementation by configurations
+
+dependencies {
+ implementation(project(":tauri-plugin-camera"))
+
+}
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/build.gradle.kts b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/build.gradle.kts
new file mode 100644
index 00000000..8c6fe584
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/build.gradle.kts
@@ -0,0 +1,25 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath("com.android.tools.build:gradle:7.3.1")
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+tasks.register("clean").configure {
+ delete("build")
+}
+
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/buildSrc/build.gradle.kts b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/buildSrc/build.gradle.kts
new file mode 100644
index 00000000..2713738b
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/buildSrc/build.gradle.kts
@@ -0,0 +1,23 @@
+plugins {
+ `kotlin-dsl`
+}
+
+gradlePlugin {
+ plugins {
+ create("pluginsForCoolKids") {
+ id = "rustPlugin"
+ implementationClass = "app.tauri.RustPlugin"
+ }
+ }
+}
+
+repositories {
+ google()
+ mavenCentral()
+}
+
+dependencies {
+ compileOnly(gradleApi())
+ implementation("com.android.tools.build:gradle:7.3.1")
+}
+
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/buildSrc/src/main/java/app/tauri/app/kotlin/BuildTask.kt b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/buildSrc/src/main/java/app/tauri/app/kotlin/BuildTask.kt
new file mode 100644
index 00000000..c1f75733
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/buildSrc/src/main/java/app/tauri/app/kotlin/BuildTask.kt
@@ -0,0 +1,43 @@
+package app.tauri
+
+import java.io.File
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.logging.LogLevel
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+open class BuildTask : DefaultTask() {
+ @InputDirectory
+ @PathSensitive(PathSensitivity.RELATIVE)
+ var rootDirRel: File? = null
+ @Input
+ var target: String? = null
+ @Input
+ var release: Boolean? = null
+
+ @TaskAction
+ fun build() {
+ val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
+ val target = target ?: throw GradleException("target cannot be null")
+ val release = release ?: throw GradleException("release cannot be null")
+ project.exec {
+ workingDir(File(project.projectDir, rootDirRel.path))
+ executable("""pnpm""")
+ args(listOf("run", "tauri", "android", "android-studio-script"))
+ if (project.logger.isEnabled(LogLevel.DEBUG)) {
+ args("-vv")
+ } else if (project.logger.isEnabled(LogLevel.INFO)) {
+ args("-v")
+ }
+ if (release) {
+ args("--release")
+ }
+ args(listOf("--target", target))
+ }.assertNormalExitValue()
+ }
+}
+
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/buildSrc/src/main/java/app/tauri/app/kotlin/RustPlugin.kt b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/buildSrc/src/main/java/app/tauri/app/kotlin/RustPlugin.kt
new file mode 100644
index 00000000..f40ad054
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/buildSrc/src/main/java/app/tauri/app/kotlin/RustPlugin.kt
@@ -0,0 +1,59 @@
+package app.tauri
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import java.io.File
+import java.util.*
+
+const val TASK_GROUP = "rust"
+
+open class Config {
+ var rootDirRel: String? = null
+ var targets: List? = null
+ var arches: List? = null
+}
+
+open class RustPlugin : Plugin {
+ private lateinit var config: Config
+
+ override fun apply(project: Project) {
+ config = project.extensions.create("rust", Config::class.java)
+ project.afterEvaluate {
+ if (config.targets == null) {
+ throw GradleException("targets cannot be null")
+ }
+ if (config.arches == null) {
+ throw GradleException("arches cannot be null")
+ }
+ for (profile in listOf("debug", "release")) {
+ val profileCapitalized = profile.capitalize(Locale.ROOT)
+ val buildTask = project.tasks.maybeCreate(
+ "rustBuild$profileCapitalized",
+ DefaultTask::class.java
+ ).apply {
+ group = TASK_GROUP
+ description = "Build dynamic library in $profile mode for all targets"
+ }
+ for (targetPair in config.targets!!.withIndex()) {
+ val targetName = targetPair.value
+ val targetArch = config.arches!![targetPair.index]
+ val targetArchCapitalized = targetArch.capitalize(Locale.ROOT)
+ val targetBuildTask = project.tasks.maybeCreate(
+ "rustBuild$targetArchCapitalized$profileCapitalized",
+ BuildTask::class.java
+ ).apply {
+ group = TASK_GROUP
+ description = "Build dynamic library in $profile mode for $targetArch"
+ rootDirRel = config.rootDirRel?.let { File(it) }
+ target = targetName
+ release = profile == "release"
+ }
+ buildTask.dependsOn(targetBuildTask)
+ project.tasks.findByName("preBuild")?.mustRunAfter(targetBuildTask)
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradle.properties b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradle.properties
new file mode 100644
index 00000000..cd0519bb
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradle/wrapper/gradle-wrapper.jar b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..e708b1c0
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradle/wrapper/gradle-wrapper.properties b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..de8c362b
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue May 10 19:22:52 CST 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradlew b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradlew
new file mode 100755
index 00000000..4f906e0c
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradlew.bat b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradlew.bat
new file mode 100644
index 00000000..ac1b06f9
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/settings.gradle b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/settings.gradle
new file mode 100644
index 00000000..a1a06b10
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/settings.gradle
@@ -0,0 +1,6 @@
+include ':app'
+
+include ':tauri-android'
+project(':tauri-android').projectDir = new File('./tauri-api')
+
+apply from: 'tauri.settings.gradle'
diff --git a/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/tauri.settings.gradle b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/tauri.settings.gradle
new file mode 100644
index 00000000..d07d8e7f
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/gen/android/app/tauri.settings.gradle
@@ -0,0 +1,3 @@
+// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+include ':tauri-plugin-camera'
+project(':tauri-plugin-camera').projectDir = new File('./tauri-plugins/tauri-plugin-camera')
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src-tauri/icons/128x128.png b/plugins/camera/examples/tauri-app/src-tauri/icons/128x128.png
new file mode 100644
index 00000000..77e7d233
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/icons/128x128.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/icons/128x128@2x.png b/plugins/camera/examples/tauri-app/src-tauri/icons/128x128@2x.png
new file mode 100644
index 00000000..0f7976f1
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/icons/128x128@2x.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/icons/32x32.png b/plugins/camera/examples/tauri-app/src-tauri/icons/32x32.png
new file mode 100644
index 00000000..98fda06f
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/icons/32x32.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/icons/icon.icns b/plugins/camera/examples/tauri-app/src-tauri/icons/icon.icns
new file mode 100644
index 00000000..29d6685a
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/icons/icon.icns differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/icons/icon.ico b/plugins/camera/examples/tauri-app/src-tauri/icons/icon.ico
new file mode 100644
index 00000000..06c23c82
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/icons/icon.ico differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/icons/icon.png b/plugins/camera/examples/tauri-app/src-tauri/icons/icon.png
new file mode 100644
index 00000000..d1756ce4
Binary files /dev/null and b/plugins/camera/examples/tauri-app/src-tauri/icons/icon.png differ
diff --git a/plugins/camera/examples/tauri-app/src-tauri/src/lib.rs b/plugins/camera/examples/tauri-app/src-tauri/src/lib.rs
new file mode 100644
index 00000000..498c6a4b
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/src/lib.rs
@@ -0,0 +1,11 @@
+// Copyright 2019-2022 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#[cfg_attr(mobile, tauri::mobile_entry_point)]
+pub fn run() {
+ tauri::Builder::default()
+ .plugin(tauri_plugin_camera::init())
+ .run(tauri::generate_context!())
+ .expect("error while running tauri application");
+}
diff --git a/plugins/camera/examples/tauri-app/src-tauri/tauri.conf.json b/plugins/camera/examples/tauri-app/src-tauri/tauri.conf.json
new file mode 100644
index 00000000..94c3ac24
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src-tauri/tauri.conf.json
@@ -0,0 +1,66 @@
+{
+ "build": {
+ "beforeDevCommand": "pnpm dev",
+ "beforeBuildCommand": "pnpm build",
+ "devPath": "http://localhost:5173",
+ "distDir": "../dist",
+ "withGlobalTauri": false
+ },
+ "package": {
+ "productName": "app",
+ "version": "0.0.0"
+ },
+ "tauri": {
+ "allowlist": {
+ "all": true
+ },
+ "bundle": {
+ "active": true,
+ "category": "DeveloperTool",
+ "copyright": "",
+ "deb": {
+ "depends": []
+ },
+ "externalBin": [],
+ "icon": [
+ "icons/32x32.png",
+ "icons/128x128.png",
+ "icons/128x128@2x.png",
+ "icons/icon.icns",
+ "icons/icon.ico"
+ ],
+ "identifier": "app.tauri.camera-example",
+ "longDescription": "",
+ "macOS": {
+ "entitlements": null,
+ "exceptionDomain": "",
+ "frameworks": [],
+ "providerShortName": null,
+ "signingIdentity": null
+ },
+ "resources": [],
+ "shortDescription": "",
+ "targets": "all",
+ "windows": {
+ "certificateThumbprint": null,
+ "digestAlgorithm": "sha256",
+ "timestampUrl": ""
+ }
+ },
+ "security": {
+ "csp": null
+ },
+ "updater": {
+ "active": false
+ },
+ "windows": [
+ {
+ "fullscreen": false,
+ "height": 600,
+ "resizable": true,
+ "title": "tauri-app",
+ "width": 800
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/plugins/camera/examples/tauri-app/src/App.svelte b/plugins/camera/examples/tauri-app/src/App.svelte
new file mode 100644
index 00000000..1a554bf9
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src/App.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/plugins/camera/examples/tauri-app/src/lib/GetPhoto.svelte b/plugins/camera/examples/tauri-app/src/lib/GetPhoto.svelte
new file mode 100644
index 00000000..709e49a2
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src/lib/GetPhoto.svelte
@@ -0,0 +1,31 @@
+
+
+
+ {#if imageSrc}
+

+ {/if}
+
+
+
+
diff --git a/plugins/camera/examples/tauri-app/src/main.js b/plugins/camera/examples/tauri-app/src/main.js
new file mode 100644
index 00000000..6b4e1a96
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src/main.js
@@ -0,0 +1,8 @@
+import "./style.css";
+import App from "./App.svelte";
+
+const app = new App({
+ target: document.getElementById("app"),
+});
+
+export default app;
diff --git a/plugins/camera/examples/tauri-app/src/style.css b/plugins/camera/examples/tauri-app/src/style.css
new file mode 100644
index 00000000..c0f9e3bc
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src/style.css
@@ -0,0 +1,102 @@
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color: #0f0f0f;
+ background-color: #f6f6f6;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+}
+
+.container {
+ margin: 0;
+ padding-top: 10vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: 0.75s;
+}
+
+.logo.tauri:hover {
+ filter: drop-shadow(0 0 2em #24c8db);
+}
+
+.row {
+ display: flex;
+ justify-content: center;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+
+a:hover {
+ color: #535bf2;
+}
+
+h1 {
+ text-align: center;
+}
+
+input,
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ color: #0f0f0f;
+ background-color: #ffffff;
+ transition: border-color 0.25s;
+ box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
+}
+
+button {
+ cursor: pointer;
+}
+
+button:hover {
+ border-color: #396cd8;
+}
+
+input,
+button {
+ outline: none;
+}
+
+#greet-input {
+ margin-right: 5px;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ color: #f6f6f6;
+ background-color: #2f2f2f;
+ }
+
+ a:hover {
+ color: #24c8db;
+ }
+
+ input,
+ button {
+ color: #ffffff;
+ background-color: #0f0f0f98;
+ }
+}
diff --git a/plugins/camera/examples/tauri-app/src/vite-env.d.ts b/plugins/camera/examples/tauri-app/src/vite-env.d.ts
new file mode 100644
index 00000000..4078e747
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/plugins/camera/examples/tauri-app/vite.config.js b/plugins/camera/examples/tauri-app/vite.config.js
new file mode 100644
index 00000000..b7427654
--- /dev/null
+++ b/plugins/camera/examples/tauri-app/vite.config.js
@@ -0,0 +1,40 @@
+import { defineConfig } from "vite";
+import { svelte } from "@sveltejs/vite-plugin-svelte";
+import { internalIpV4 } from "internal-ip";
+
+// https://vitejs.dev/config/
+export default defineConfig(async () => {
+ const host = process.env.TAURI_PLATFORM === 'android' || process.env.TAURI_PLATFORM === 'ios' ? (await internalIpV4()) : 'localhost'
+ return {
+ plugins: [svelte()],
+
+ // Vite optons tailored for Tauri development and only applied in `tauri dev` or `tauri build`
+ // prevent vite from obscuring rust errors
+ clearScreen: false,
+ // tauri expects a fixed port, fail if that port is not available
+ server: {
+ host: '0.0.0.0',
+ port: 5173,
+ strictPort: true,
+ hmr: {
+ protocol: 'ws',
+ host,
+ port: 5183
+ },
+ fs: {
+ allow: ['.', '../../tooling/api/dist']
+ }
+ },
+ // to make use of `TAURI_DEBUG` and other env variables
+ // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
+ envPrefix: ["VITE_", "TAURI_"],
+ build: {
+ // Tauri supports es2021
+ target: ["es2021", "chrome100", "safari13"],
+ // don't minify for debug builds
+ minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
+ // produce sourcemaps for debug builds
+ sourcemap: !!process.env.TAURI_DEBUG,
+ },
+ }
+});
diff --git a/plugins/camera/guest-js/index.ts b/plugins/camera/guest-js/index.ts
new file mode 100644
index 00000000..be4fc462
--- /dev/null
+++ b/plugins/camera/guest-js/index.ts
@@ -0,0 +1,50 @@
+// Copyright 2019-2022 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+import { invoke } from '@tauri-apps/api/tauri'
+
+export enum Source {
+ Prompt = 'PROMPT',
+ Camera = 'CAMERA',
+ Photos = 'PHOTOS'
+}
+
+export enum ResultType {
+ Uri = 'uri',
+ Base64 = 'base64',
+ DataUrl = 'dataUrl'
+}
+
+export enum CameraDirection {
+ Rear = 'REAR',
+ Front = 'FRONT'
+}
+
+export interface ImageOptions {
+ quality?: number
+ allowEditing?: boolean
+ resultType?: ResultType
+ saveToGallery?: boolean
+ width?: number
+ height?: number
+ correctOrientation?: boolean
+ source?: Source
+ direction?: CameraDirection
+ presentationStyle?: 'fullscreen' | 'popover'
+ promptLabelHeader?: string
+ promptLabelCancel?: string
+ promptLabelPhoto?: string
+ promptLabelPicture?: string
+}
+
+export interface Image {
+ data: string
+ assetUrl?: string
+ format: string
+ saved: boolean
+ exif: unknown
+}
+
+export async function getPhoto(options?: ImageOptions): Promise {
+ return await invoke('plugin:camera|getPhoto', { ...options })
+}
diff --git a/plugins/camera/ios/.gitignore b/plugins/camera/ios/.gitignore
new file mode 100644
index 00000000..7d98e3ad
--- /dev/null
+++ b/plugins/camera/ios/.gitignore
@@ -0,0 +1,11 @@
+.DS_Store
+/.build
+/Packages
+/*.xcodeproj
+xcuserdata/
+DerivedData/
+.swiftpm/config/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
+Package.resolved
+/tauri-api
diff --git a/plugins/camera/ios/Package.swift b/plugins/camera/ios/Package.swift
new file mode 100644
index 00000000..45b30dc2
--- /dev/null
+++ b/plugins/camera/ios/Package.swift
@@ -0,0 +1,31 @@
+// swift-tools-version:5.7
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "tauri-plugin-camera",
+ platforms: [
+ .iOS(.v11),
+ ],
+ products: [
+ // Products define the executables and libraries a package produces, and make them visible to other packages.
+ .library(
+ name: "tauri-plugin-camera",
+ type: .static,
+ targets: ["tauri-plugin-camera"]),
+ ],
+ dependencies: [
+ .package(name: "Tauri", path: "../../../../../core/tauri/mobile/ios-api")
+ ],
+ targets: [
+ // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+ // Targets can depend on other targets in this package, and on products in packages this package depends on.
+ .target(
+ name: "tauri-plugin-camera",
+ dependencies: [
+ .byName(name: "Tauri")
+ ],
+ path: "Sources")
+ ]
+)
diff --git a/plugins/camera/ios/README.md b/plugins/camera/ios/README.md
new file mode 100644
index 00000000..5293ed5b
--- /dev/null
+++ b/plugins/camera/ios/README.md
@@ -0,0 +1,3 @@
+# Tauri Plugin camera
+
+A description of this package.
diff --git a/plugins/camera/ios/Sources/CameraExtensions.swift b/plugins/camera/ios/Sources/CameraExtensions.swift
new file mode 100644
index 00000000..938b61c0
--- /dev/null
+++ b/plugins/camera/ios/Sources/CameraExtensions.swift
@@ -0,0 +1,105 @@
+import UIKit
+import Photos
+
+internal protocol CameraAuthorizationState {
+ var authorizationState: String { get }
+}
+
+extension AVAuthorizationStatus: CameraAuthorizationState {
+ var authorizationState: String {
+ switch self {
+ case .denied, .restricted:
+ return "denied"
+ case .authorized:
+ return "granted"
+ case .notDetermined:
+ fallthrough
+ @unknown default:
+ return "prompt"
+ }
+ }
+}
+
+extension PHAuthorizationStatus: CameraAuthorizationState {
+ var authorizationState: String {
+ switch self {
+ case .denied, .restricted:
+ return "denied"
+ case .authorized:
+ return "granted"
+ #if swift(>=5.3)
+ // poor proxy for Xcode 12/iOS 14, should be removed once building with Xcode 12 is required
+ case .limited:
+ return "limited"
+ #endif
+ case .notDetermined:
+ fallthrough
+ @unknown default:
+ return "prompt"
+ }
+ }
+}
+
+internal extension PHAsset {
+ /**
+ Retrieves the image metadata for the asset.
+ */
+ var imageData: [String: Any] {
+ let options = PHImageRequestOptions()
+ options.isSynchronous = true
+ options.resizeMode = .none
+ options.isNetworkAccessAllowed = false
+ options.version = .current
+
+ var result: [String: Any] = [:]
+ _ = PHCachingImageManager().requestImageDataAndOrientation(for: self, options: options) { (data, _, _, _) in
+ if let data = data as NSData? {
+ let options = [kCGImageSourceShouldCache as String: kCFBooleanFalse] as CFDictionary
+ if let imgSrc = CGImageSourceCreateWithData(data, options),
+ let metadata = CGImageSourceCopyPropertiesAtIndex(imgSrc, 0, options) as? [String: Any] {
+ result = metadata
+ }
+ }
+ }
+ return result
+ }
+}
+
+internal extension UIImage {
+ /**
+ Generates a new image from the existing one, implicitly resetting any orientation.
+ Dimensions greater than 0 will resize the image while preserving the aspect ratio.
+ */
+ func reformat(to size: CGSize? = nil) -> UIImage {
+ let imageHeight = self.size.height
+ let imageWidth = self.size.width
+ // determine the max dimensions, 0 is treated as 'no restriction'
+ var maxWidth: CGFloat
+ if let size = size, size.width > 0 {
+ maxWidth = size.width
+ } else {
+ maxWidth = imageWidth
+ }
+ let maxHeight: CGFloat
+ if let size = size, size.height > 0 {
+ maxHeight = size.height
+ } else {
+ maxHeight = imageHeight
+ }
+ // adjust to preserve aspect ratio
+ var targetWidth = min(imageWidth, maxWidth)
+ var targetHeight = (imageHeight * targetWidth) / imageWidth
+ if targetHeight > maxHeight {
+ targetWidth = (imageWidth * maxHeight) / imageHeight
+ targetHeight = maxHeight
+ }
+ // generate the new image and return
+ let format: UIGraphicsImageRendererFormat = UIGraphicsImageRendererFormat.default()
+ format.scale = 1.0
+ format.opaque = false
+ let renderer = UIGraphicsImageRenderer(size: CGSize(width: targetWidth, height: targetHeight), format: format)
+ return renderer.image { (_) in
+ self.draw(in: CGRect(origin: .zero, size: CGSize(width: targetWidth, height: targetHeight)))
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/camera/ios/Sources/CameraPlugin.swift b/plugins/camera/ios/Sources/CameraPlugin.swift
new file mode 100644
index 00000000..aca9ad2d
--- /dev/null
+++ b/plugins/camera/ios/Sources/CameraPlugin.swift
@@ -0,0 +1,576 @@
+import UIKit
+import WebKit
+import Tauri
+import Photos
+import PhotosUI
+
+public class CameraPlugin: Plugin {
+ private var invoke: Invoke?
+ private var settings = CameraSettings()
+ private let defaultSource = CameraSource.prompt
+ private let defaultDirection = CameraDirection.rear
+ private var multiple = false
+
+ private var imageCounter = 0
+
+ @objc override public func checkPermissions(_ invoke: Invoke) {
+ var result: [String: Any] = [:]
+ for permission in CameraPermissionType.allCases {
+ let state: String
+ switch permission {
+ case .camera:
+ state = AVCaptureDevice.authorizationStatus(for: .video).authorizationState
+ case .photos:
+ if #available(iOS 14, *) {
+ state = PHPhotoLibrary.authorizationStatus(for: .readWrite).authorizationState
+ } else {
+ state = PHPhotoLibrary.authorizationStatus().authorizationState
+ }
+ }
+ result[permission.rawValue] = state
+ }
+ invoke.resolve(result)
+ }
+
+ @objc override public func requestPermissions(_ invoke: Invoke) {
+ // get the list of desired types, if passed
+ let typeList = invoke.getArray("permissions", String.self)?.compactMap({ (type) -> CameraPermissionType? in
+ return CameraPermissionType(rawValue: type)
+ }) ?? []
+ // otherwise check everything
+ let permissions: [CameraPermissionType] = (typeList.count > 0) ? typeList : CameraPermissionType.allCases
+ // request the permissions
+ let group = DispatchGroup()
+ for permission in permissions {
+ switch permission {
+ case .camera:
+ group.enter()
+ AVCaptureDevice.requestAccess(for: .video) { _ in
+ group.leave()
+ }
+ case .photos:
+ group.enter()
+ if #available(iOS 14, *) {
+ PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in
+ group.leave()
+ }
+ } else {
+ PHPhotoLibrary.requestAuthorization({ (_) in
+ group.leave()
+ })
+ }
+ }
+ }
+ group.notify(queue: DispatchQueue.main) { [weak self] in
+ self?.checkPermissions(invoke)
+ }
+ }
+
+ @objc func pickLimitedLibraryPhotos(_ invoke: Invoke) {
+ if #available(iOS 14, *) {
+ PHPhotoLibrary.requestAuthorization(for: .readWrite) { (granted) in
+ if granted == .limited {
+ if let viewController = self.manager.viewController {
+ if #available(iOS 15, *) {
+ PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController) { _ in
+ self.getLimitedLibraryPhotos(invoke)
+ }
+ } else {
+ PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController)
+ invoke.resolve([
+ "photos": []
+ ])
+ }
+ }
+ } else {
+ invoke.resolve([
+ "photos": []
+ ])
+ }
+ }
+ } else {
+ invoke.unavailable("Not available on iOS 13")
+ }
+ }
+
+ @objc func getLimitedLibraryPhotos(_ invoke: Invoke) {
+ if #available(iOS 14, *) {
+ PHPhotoLibrary.requestAuthorization(for: .readWrite) { (granted) in
+ if granted == .limited {
+
+ self.invoke = invoke
+
+ DispatchQueue.global(qos: .utility).async {
+ let assets = PHAsset.fetchAssets(with: .image, options: nil)
+ var processedImages: [ProcessedImage] = []
+
+ let imageManager = PHImageManager.default()
+ let options = PHImageRequestOptions()
+ options.deliveryMode = .highQualityFormat
+
+ let group = DispatchGroup()
+
+ for index in 0...(assets.count - 1) {
+ let asset = assets.object(at: index)
+ let fullSize = CGSize(width: asset.pixelWidth, height: asset.pixelHeight)
+
+ group.enter()
+ imageManager.requestImage(for: asset, targetSize: fullSize, contentMode: .default, options: options) { image, _ in
+ guard let image = image else {
+ group.leave()
+ return
+ }
+ processedImages.append(self.processedImage(from: image, with: asset.imageData))
+ group.leave()
+ }
+ }
+
+ group.notify(queue: .global(qos: .utility)) { [weak self] in
+ self?.returnImages(processedImages)
+ }
+ }
+ } else {
+ invoke.resolve([
+ "photos": []
+ ])
+ }
+ }
+ } else {
+ invoke.unavailable("Not available on iOS 13")
+ }
+ }
+
+ @objc func getPhoto(_ invoke: Invoke) {
+ self.multiple = false
+ self.invoke = invoke
+ self.settings = cameraSettings(from: invoke)
+
+ // Make sure they have all the necessary info.plist settings
+ if let missingUsageDescription = checkUsageDescriptions() {
+ Logger.error("[PLUGIN]", "Camera", "-", missingUsageDescription)
+ invoke.reject(missingUsageDescription)
+ return
+ }
+
+ DispatchQueue.main.async {
+ switch self.settings.source {
+ case .prompt:
+ self.showPrompt()
+ case .camera:
+ self.showCamera()
+ case .photos:
+ self.showPhotos()
+ }
+ }
+ }
+
+ @objc func pickImages(_ invoke: Invoke) {
+ self.multiple = true
+ self.invoke = invoke
+ self.settings = cameraSettings(from: invoke)
+ DispatchQueue.main.async {
+ self.showPhotos()
+ }
+ }
+
+ private func checkUsageDescriptions() -> String? {
+ if let dict = Bundle.main.infoDictionary {
+ for key in CameraPropertyListKeys.allCases where dict[key.rawValue] == nil {
+ return key.missingMessage
+ }
+ }
+ return nil
+ }
+
+ private func cameraSettings(from invoke: Invoke) -> CameraSettings {
+ var settings = CameraSettings()
+ settings.jpegQuality = min(abs(CGFloat(invoke.getFloat("quality") ?? 100.0)) / 100.0, 1.0)
+ settings.allowEditing = invoke.getBool("allowEditing") ?? false
+ settings.source = CameraSource(rawValue: invoke.getString("source") ?? defaultSource.rawValue) ?? defaultSource
+ settings.direction = CameraDirection(rawValue: invoke.getString("direction") ?? defaultDirection.rawValue) ?? defaultDirection
+ if let typeString = invoke.getString("resultType"), let type = CameraResultType(rawValue: typeString) {
+ settings.resultType = type
+ }
+ settings.saveToGallery = invoke.getBool("saveToGallery") ?? false
+
+ // Get the new image dimensions if provided
+ settings.width = CGFloat(invoke.getInt("width") ?? 0)
+ settings.height = CGFloat(invoke.getInt("height") ?? 0)
+ if settings.width > 0 || settings.height > 0 {
+ // We resize only if a dimension was provided
+ settings.shouldResize = true
+ }
+ settings.shouldCorrectOrientation = invoke.getBool("correctOrientation") ?? true
+ settings.userPromptText = CameraPromptText(title: invoke.getString("promptLabelHeader"),
+ photoAction: invoke.getString("promptLabelPhoto"),
+ cameraAction: invoke.getString("promptLabelPicture"),
+ cancelAction: invoke.getString("promptLabelCancel"))
+ if let styleString = invoke.getString("presentationStyle"), styleString == "popover" {
+ settings.presentationStyle = .popover
+ } else {
+ settings.presentationStyle = .fullScreen
+ }
+
+ return settings
+ }
+}
+
+// public delegate methods
+extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {
+ public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
+ picker.dismiss(animated: true)
+ self.invoke?.reject("User cancelled photos app")
+ }
+
+ public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
+ self.invoke?.reject("User cancelled photos app")
+ }
+
+ public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
+ self.invoke?.reject("User cancelled photos app")
+ }
+
+ public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
+ picker.dismiss(animated: true) {
+ if let processedImage = self.processImage(from: info) {
+ self.returnProcessedImage(processedImage)
+ } else {
+ self.invoke?.reject("Error processing image")
+ }
+ }
+ }
+}
+
+@available(iOS 14, *)
+extension CameraPlugin: PHPickerViewControllerDelegate {
+ public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
+ picker.dismiss(animated: true, completion: nil)
+ guard let result = results.first else {
+ self.invoke?.reject("User cancelled photos app")
+ return
+ }
+ if multiple {
+ var images: [ProcessedImage] = []
+ var processedCount = 0
+ for img in results {
+ guard img.itemProvider.canLoadObject(ofClass: UIImage.self) else {
+ self.invoke?.reject("Error loading image")
+ return
+ }
+ // extract the image
+ img.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (reading, _) in
+ if let image = reading as? UIImage {
+ var asset: PHAsset?
+ if let assetId = img.assetIdentifier {
+ asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject
+ }
+ if let processedImage = self?.processedImage(from: image, with: asset?.imageData) {
+ images.append(processedImage)
+ }
+ processedCount += 1
+ if processedCount == results.count {
+ self?.returnImages(images)
+ }
+ } else {
+ self?.invoke?.reject("Error loading image")
+ }
+ }
+ }
+
+ } else {
+ guard result.itemProvider.canLoadObject(ofClass: UIImage.self) else {
+ self.invoke?.reject("Error loading image")
+ return
+ }
+ // extract the image
+ result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (reading, _) in
+ if let image = reading as? UIImage {
+ var asset: PHAsset?
+ if let assetId = result.assetIdentifier {
+ asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject
+ }
+ if var processedImage = self?.processedImage(from: image, with: asset?.imageData) {
+ processedImage.flags = .gallery
+ self?.returnProcessedImage(processedImage)
+ return
+ }
+ }
+ self?.invoke?.reject("Error loading image")
+ }
+ }
+ }
+}
+
+private extension CameraPlugin {
+ func returnImage(_ processedImage: ProcessedImage, isSaved: Bool) {
+ guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else {
+ self.invoke?.reject("Unable to convert image to jpeg")
+ return
+ }
+
+ if settings.resultType == CameraResultType.uri || multiple {
+ guard let fileURL = try? saveTemporaryImage(jpeg),
+ let webURL = manager.assetUrl(fromLocalURL: fileURL) else {
+ invoke?.reject("Unable to get asset URL to file")
+ return
+ }
+ if self.multiple {
+ invoke?.resolve([
+ "photos": [[
+ "data": fileURL.absoluteString,
+ "exif": processedImage.exifData,
+ "assetUrl": webURL.absoluteString,
+ "format": "jpeg"
+ ]]
+ ])
+ return
+ }
+ invoke?.resolve([
+ "data": fileURL.absoluteString,
+ "exif": processedImage.exifData,
+ "assetUrl": webURL.absoluteString,
+ "format": "jpeg",
+ "saved": isSaved
+ ])
+ } else if settings.resultType == CameraResultType.base64 {
+ self.invoke?.resolve([
+ "data": jpeg.base64EncodedString(),
+ "exif": processedImage.exifData,
+ "format": "jpeg",
+ "saved": isSaved
+ ])
+ } else if settings.resultType == CameraResultType.dataURL {
+ invoke?.resolve([
+ "data": "data:image/jpeg;base64," + jpeg.base64EncodedString(),
+ "exif": processedImage.exifData,
+ "format": "jpeg",
+ "saved": isSaved
+ ])
+ }
+ }
+
+ func returnImages(_ processedImages: [ProcessedImage]) {
+ var photos: [JsonObject] = []
+ for processedImage in processedImages {
+ guard let jpeg = processedImage.generateJPEG(with: settings.jpegQuality) else {
+ self.invoke?.reject("Unable to convert image to jpeg")
+ return
+ }
+
+ guard let fileURL = try? saveTemporaryImage(jpeg),
+ let webURL = manager.assetUrl(fromLocalURL: fileURL) else {
+ invoke?.reject("Unable to get asset URL to file")
+ return
+ }
+
+ photos.append([
+ "path": fileURL.absoluteString,
+ "exif": processedImage.exifData,
+ "assetUrl": webURL.absoluteString,
+ "format": "jpeg"
+ ])
+ }
+ invoke?.resolve([
+ "photos": photos
+ ])
+ }
+
+ func returnProcessedImage(_ processedImage: ProcessedImage) {
+ // conditionally save the image
+ if settings.saveToGallery && (processedImage.flags.contains(.edited) == true || processedImage.flags.contains(.gallery) == false) {
+ _ = ImageSaver(image: processedImage.image) { error in
+ var isSaved = false
+ if error == nil {
+ isSaved = true
+ }
+ self.returnImage(processedImage, isSaved: isSaved)
+ }
+ } else {
+ self.returnImage(processedImage, isSaved: false)
+ }
+ }
+
+ func showPrompt() {
+ // Build the action sheet
+ let alert = UIAlertController(title: settings.userPromptText.title, message: nil, preferredStyle: UIAlertController.Style.actionSheet)
+ alert.addAction(UIAlertAction(title: settings.userPromptText.photoAction, style: .default, handler: { [weak self] (_: UIAlertAction) in
+ self?.showPhotos()
+ }))
+
+ alert.addAction(UIAlertAction(title: settings.userPromptText.cameraAction, style: .default, handler: { [weak self] (_: UIAlertAction) in
+ self?.showCamera()
+ }))
+
+ alert.addAction(UIAlertAction(title: settings.userPromptText.cancelAction, style: .cancel, handler: { [weak self] (_: UIAlertAction) in
+ self?.invoke?.reject("User cancelled photos app prompt")
+ }))
+ UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: alert)
+ self.manager.viewController?.present(alert, animated: true, completion: nil)
+ }
+
+ func showCamera() {
+ // check if we have a camera
+ if manager.isSimEnvironment || !UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) {
+ Logger.error("[PLUGIN]", "Camera", "-", "Camera not available in simulator")
+ invoke?.reject("Camera not available while running in Simulator")
+ return
+ }
+ // check for permission
+ let authStatus = AVCaptureDevice.authorizationStatus(for: .video)
+ if authStatus == .restricted || authStatus == .denied {
+ invoke?.reject("User denied access to camera")
+ return
+ }
+ // we either already have permission or can prompt
+ AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
+ if granted {
+ DispatchQueue.main.async {
+ self?.presentCameraPicker()
+ }
+ } else {
+ self?.invoke?.reject("User denied access to camera")
+ }
+ }
+ }
+
+ func showPhotos() {
+ // check for permission
+ let authStatus = PHPhotoLibrary.authorizationStatus()
+ if authStatus == .restricted || authStatus == .denied {
+ invoke?.reject("User denied access to photos")
+ return
+ }
+ // we either already have permission or can prompt
+ if authStatus == .authorized {
+ presentSystemAppropriateImagePicker()
+ } else {
+ PHPhotoLibrary.requestAuthorization({ [weak self] (status) in
+ if status == PHAuthorizationStatus.authorized {
+ DispatchQueue.main.async { [weak self] in
+ self?.presentSystemAppropriateImagePicker()
+ }
+ } else {
+ self?.invoke?.reject("User denied access to photos")
+ }
+ })
+ }
+ }
+
+ func presentCameraPicker() {
+ let picker = UIImagePickerController()
+ picker.delegate = self
+ picker.allowsEditing = self.settings.allowEditing
+ // select the input
+ picker.sourceType = .camera
+ if settings.direction == .rear, UIImagePickerController.isCameraDeviceAvailable(.rear) {
+ picker.cameraDevice = .rear
+ } else if settings.direction == .front, UIImagePickerController.isCameraDeviceAvailable(.front) {
+ picker.cameraDevice = .front
+ }
+ // present
+ picker.modalPresentationStyle = settings.presentationStyle
+ if settings.presentationStyle == .popover {
+ picker.popoverPresentationController?.delegate = self
+ UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: picker)
+ }
+ manager.viewController?.present(picker, animated: true, completion: nil)
+ }
+
+ func presentSystemAppropriateImagePicker() {
+ if #available(iOS 14, *) {
+ presentPhotoPicker()
+ } else {
+ presentImagePicker()
+ }
+ }
+
+ func presentImagePicker() {
+ let picker = UIImagePickerController()
+ picker.delegate = self
+ picker.allowsEditing = self.settings.allowEditing
+ // select the input
+ picker.sourceType = .photoLibrary
+ // present
+ picker.modalPresentationStyle = settings.presentationStyle
+ if settings.presentationStyle == .popover {
+ picker.popoverPresentationController?.delegate = self
+ UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: picker)
+ }
+ manager.viewController?.present(picker, animated: true, completion: nil)
+ }
+
+ @available(iOS 14, *)
+ func presentPhotoPicker() {
+ var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
+ configuration.selectionLimit = self.multiple ? (self.invoke?.getInt("limit") ?? 0) : 1
+ configuration.filter = .images
+ let picker = PHPickerViewController(configuration: configuration)
+ picker.delegate = self
+ // present
+ picker.modalPresentationStyle = settings.presentationStyle
+ if settings.presentationStyle == .popover {
+ picker.popoverPresentationController?.delegate = self
+ UIUtils.centerPopover(rootViewController: manager.viewController, popoverController: picker)
+ }
+ manager.viewController?.present(picker, animated: true, completion: nil)
+ }
+
+ func saveTemporaryImage(_ data: Data) throws -> URL {
+ var url: URL
+ repeat {
+ imageCounter += 1
+ url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("photo-\(imageCounter).jpg")
+ } while FileManager.default.fileExists(atPath: url.path)
+
+ try data.write(to: url, options: .atomic)
+ return url
+ }
+
+ func processImage(from info: [UIImagePickerController.InfoKey: Any]) -> ProcessedImage? {
+ var selectedImage: UIImage?
+ var flags: PhotoFlags = []
+ // get the image
+ if let edited = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
+ selectedImage = edited // use the edited version
+ flags = flags.union([.edited])
+ } else if let original = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
+ selectedImage = original // use the original version
+ }
+ guard let image = selectedImage else {
+ return nil
+ }
+ var metadata: [String: Any] = [:]
+ // get the image's metadata from the picker or from the photo album
+ if let photoMetadata = info[UIImagePickerController.InfoKey.mediaMetadata] as? [String: Any] {
+ metadata = photoMetadata
+ } else {
+ flags = flags.union([.gallery])
+ }
+ if let asset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset {
+ metadata = asset.imageData
+ }
+ // get the result
+ var result = processedImage(from: image, with: metadata)
+ result.flags = flags
+ return result
+ }
+
+ func processedImage(from image: UIImage, with metadata: [String: Any]?) -> ProcessedImage {
+ var result = ProcessedImage(image: image, metadata: metadata ?? [:])
+ // resizing the image only makes sense if we have real values to which to constrain it
+ if settings.shouldResize, settings.width > 0 || settings.height > 0 {
+ result.image = result.image.reformat(to: CGSize(width: settings.width, height: settings.height))
+ result.overwriteMetadataOrientation(to: 1)
+ } else if settings.shouldCorrectOrientation {
+ // resizing implicitly reformats the image so this is only needed if we aren't resizing
+ result.image = result.image.reformat()
+ result.overwriteMetadataOrientation(to: 1)
+ }
+ return result
+ }
+}
+
+@_cdecl("init_plugin_camera")
+func initCameraPlugin(webview: WKWebView?) {
+ Tauri.registerPlugin(webview: webview, name: "camera", plugin: CameraPlugin())
+}
diff --git a/plugins/camera/ios/Sources/CameraTypes.swift b/plugins/camera/ios/Sources/CameraTypes.swift
new file mode 100644
index 00000000..adfb5640
--- /dev/null
+++ b/plugins/camera/ios/Sources/CameraTypes.swift
@@ -0,0 +1,142 @@
+import UIKit
+
+// MARK: - Public
+
+public enum CameraSource: String {
+ case prompt = "PROMPT"
+ case camera = "CAMERA"
+ case photos = "PHOTOS"
+}
+
+public enum CameraDirection: String {
+ case rear = "REAR"
+ case front = "FRONT"
+}
+
+public enum CameraResultType: String {
+ case base64
+ case uri
+ case dataURL = "dataUrl"
+}
+
+struct CameraPromptText {
+ let title: String
+ let photoAction: String
+ let cameraAction: String
+ let cancelAction: String
+
+ init(title: String? = nil, photoAction: String? = nil, cameraAction: String? = nil, cancelAction: String? = nil) {
+ self.title = title ?? "Photo"
+ self.photoAction = photoAction ?? "From Photos"
+ self.cameraAction = cameraAction ?? "Take Picture"
+ self.cancelAction = cancelAction ?? "Cancel"
+ }
+}
+
+public struct CameraSettings {
+ var source: CameraSource = CameraSource.prompt
+ var direction: CameraDirection = CameraDirection.rear
+ var resultType = CameraResultType.base64
+ var userPromptText = CameraPromptText()
+ var jpegQuality: CGFloat = 1.0
+ var width: CGFloat = 0
+ var height: CGFloat = 0
+ var allowEditing = false
+ var shouldResize = false
+ var shouldCorrectOrientation = true
+ var saveToGallery = false
+ var presentationStyle = UIModalPresentationStyle.fullScreen
+}
+
+public struct CameraResult {
+ let image: UIImage?
+ let metadata: [AnyHashable: Any]
+}
+
+// MARK: - Internal
+
+internal enum CameraPermissionType: String, CaseIterable {
+ case camera
+ case photos
+}
+
+internal enum CameraPropertyListKeys: String, CaseIterable {
+ case photoLibraryAddUsage = "NSPhotoLibraryAddUsageDescription"
+ case photoLibraryUsage = "NSPhotoLibraryUsageDescription"
+ case cameraUsage = "NSCameraUsageDescription"
+
+ var link: String {
+ switch self {
+ case .photoLibraryAddUsage:
+ return "https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW73"
+ case .photoLibraryUsage:
+ return "https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW17"
+ case .cameraUsage:
+ return "https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW24"
+ }
+ }
+
+ var missingMessage: String {
+ return "You are missing \(self.rawValue) in your Info.plist file." +
+ " Camera will not function without it. Learn more: \(self.link)"
+ }
+}
+
+internal struct PhotoFlags: OptionSet {
+ let rawValue: Int
+
+ static let edited = PhotoFlags(rawValue: 1 << 0)
+ static let gallery = PhotoFlags(rawValue: 1 << 1)
+
+ static let all: PhotoFlags = [.edited, .gallery]
+}
+
+internal struct ProcessedImage {
+ var image: UIImage
+ var metadata: [String: Any]
+ var flags: PhotoFlags = []
+
+ var exifData: [String: Any] {
+ var exifData = metadata["{Exif}"] as? [String: Any]
+ exifData?["Orientation"] = metadata["Orientation"]
+ exifData?["GPS"] = metadata["{GPS}"]
+ return exifData ?? [:]
+ }
+
+ mutating func overwriteMetadataOrientation(to orientation: Int) {
+ replaceDictionaryOrientation(atNode: &metadata, to: orientation)
+ }
+
+ func replaceDictionaryOrientation(atNode node: inout [String: Any], to orientation: Int) {
+ for key in node.keys {
+ if key == "Orientation", (node[key] as? Int) != nil {
+ node[key] = orientation
+ } else if var child = node[key] as? [String: Any] {
+ replaceDictionaryOrientation(atNode: &child, to: orientation)
+ node[key] = child
+ }
+ }
+ }
+
+ func generateJPEG(with quality: CGFloat) -> Data? {
+ // convert the UIImage to a jpeg
+ guard let data = self.image.jpegData(compressionQuality: quality) else {
+ return nil
+ }
+ // define our jpeg data as an image source and get its type
+ guard let source = CGImageSourceCreateWithData(data as CFData, nil), let type = CGImageSourceGetType(source) else {
+ return data
+ }
+ // allocate an output buffer and create the destination to receive the new data
+ guard let output = NSMutableData(capacity: data.count), let destination = CGImageDestinationCreateWithData(output, type, 1, nil) else {
+ return data
+ }
+ // pipe the source into the destination while overwriting the metadata, this encodes the metadata information into the image
+ CGImageDestinationAddImageFromSource(destination, source, 0, self.metadata as CFDictionary)
+ // finish
+ guard CGImageDestinationFinalize(destination) else {
+ return data
+ }
+ return output as Data
+ }
+}
\ No newline at end of file
diff --git a/plugins/camera/ios/Sources/ImageSaver.swift b/plugins/camera/ios/Sources/ImageSaver.swift
new file mode 100644
index 00000000..a33b43b0
--- /dev/null
+++ b/plugins/camera/ios/Sources/ImageSaver.swift
@@ -0,0 +1,20 @@
+import UIKit
+
+class ImageSaver: NSObject {
+
+ var onResult: ((Error?) -> Void) = {_ in }
+
+ init(image: UIImage, onResult:@escaping ((Error?) -> Void)) {
+ self.onResult = onResult
+ super.init()
+ UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveResult), nil)
+ }
+
+ @objc func saveResult(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
+ if let error = error {
+ onResult(error)
+ } else {
+ onResult(nil)
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/camera/ios/Tests/PluginTests/PluginTests.swift b/plugins/camera/ios/Tests/PluginTests/PluginTests.swift
new file mode 100644
index 00000000..4f8e9ace
--- /dev/null
+++ b/plugins/camera/ios/Tests/PluginTests/PluginTests.swift
@@ -0,0 +1,8 @@
+import XCTest
+@testable import ExamplePlugin
+
+final class ExamplePluginTests: XCTestCase {
+ func testExample() throws {
+ let plugin = ExamplePlugin()
+ }
+}
diff --git a/plugins/camera/package.json b/plugins/camera/package.json
new file mode 100644
index 00000000..e252ed1b
--- /dev/null
+++ b/plugins/camera/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "tauri-plugin-camera-api",
+ "version": "0.0.0",
+ "description": "Ask the user take a photo with the camera or select an image from the gallery.",
+ "license": "MIT or APACHE-2.0",
+ "authors": [
+ "Tauri Programme within The Commons Conservancy"
+ ],
+ "type": "module",
+ "browser": "dist-js/index.min.js",
+ "module": "dist-js/index.mjs",
+ "types": "dist-js/index.d.ts",
+ "exports": {
+ "import": "./dist-js/index.mjs",
+ "types": "./dist-js/index.d.ts",
+ "browser": "./dist-js/index.min.js"
+ },
+ "scripts": {
+ "build": "rollup -c"
+ },
+ "files": [
+ "dist-js",
+ "!dist-js/**/*.map",
+ "README.md",
+ "LICENSE"
+ ],
+ "devDependencies": {
+ "tslib": "^2.4.1"
+ },
+ "dependencies": {
+ "@tauri-apps/api": "^2.0.0-alpha.0"
+ }
+}
diff --git a/plugins/camera/rollup.config.mjs b/plugins/camera/rollup.config.mjs
new file mode 100644
index 00000000..6555e98b
--- /dev/null
+++ b/plugins/camera/rollup.config.mjs
@@ -0,0 +1,11 @@
+import { readFileSync } from "fs";
+
+import { createConfig } from "../../shared/rollup.config.mjs";
+
+export default createConfig({
+ input: "guest-js/index.ts",
+ pkg: JSON.parse(
+ readFileSync(new URL("./package.json", import.meta.url), "utf8")
+ ),
+ external: [/^@tauri-apps\/api/],
+});
diff --git a/plugins/camera/src/error.rs b/plugins/camera/src/error.rs
new file mode 100644
index 00000000..393cc95e
--- /dev/null
+++ b/plugins/camera/src/error.rs
@@ -0,0 +1,24 @@
+// Copyright 2019-2022 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use serde::{ser::Serializer, Serialize};
+
+pub type Result = std::result::Result;
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+ #[error(transparent)]
+ PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
+}
+
+impl Serialize for Error {
+ fn serialize(&self, serializer: S) -> std::result::Result
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.to_string().as_ref())
+ }
+}
diff --git a/plugins/camera/src/lib.rs b/plugins/camera/src/lib.rs
new file mode 100644
index 00000000..b598d523
--- /dev/null
+++ b/plugins/camera/src/lib.rs
@@ -0,0 +1,60 @@
+// Copyright 2019-2022 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#![cfg(mobile)]
+
+use tauri::{
+ plugin::{Builder, PluginHandle, TauriPlugin},
+ Manager, Runtime,
+};
+
+pub use models::*;
+mod error;
+pub mod models;
+pub use error::*;
+
+#[cfg(target_os = "android")]
+const PLUGIN_IDENTIFIER: &str = "app.tauri.camera";
+
+#[cfg(target_os = "ios")]
+extern "C" {
+ fn init_plugin_camera(webview: tauri::cocoa::base::id);
+}
+
+/// A helper class to access the mobile camera APIs.
+pub struct Camera(PluginHandle);
+
+impl Camera {
+ pub fn get_photo(&self, options: ImageOptions) -> Result {
+ self
+ .0
+ .run_mobile_plugin("getPhoto", options)
+ .map_err(Into::into)
+ }
+}
+
+/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the camera APIs.
+pub trait CameraExt {
+ fn camera(&self) -> &Camera;
+}
+
+impl> CameraExt for T {
+ fn camera(&self) -> &Camera {
+ self.state::>().inner()
+ }
+}
+
+/// Initializes the plugin.
+pub fn init() -> TauriPlugin {
+ Builder::new("camera")
+ .setup(|app, api| {
+ #[cfg(target_os = "android")]
+ let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "CameraPlugin")?;
+ #[cfg(target_os = "ios")]
+ let handle = api.register_ios_plugin(init_plugin_camera)?;
+ app.manage(Camera(handle));
+ Ok(())
+ })
+ .build()
+}
diff --git a/plugins/camera/src/models.rs b/plugins/camera/src/models.rs
new file mode 100644
index 00000000..65294857
--- /dev/null
+++ b/plugins/camera/src/models.rs
@@ -0,0 +1,38 @@
+// Copyright 2019-2022 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Default, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ImageOptions {
+ pub quality: Option,
+ #[serde(default)]
+ pub allow_editing: bool,
+ pub result_type: Option,
+ #[serde(default)]
+ pub save_to_gallery: bool,
+ pub width: Option,
+ pub height: Option,
+ #[serde(default)]
+ pub correct_orientation: bool,
+ pub source: Option,
+ pub direction: Option,
+ pub presentation_style: Option,
+ pub prompt_label_header: Option,
+ pub prompt_label_cancel: Option,
+ pub prompt_label_photo: Option,
+ pub prompt_label_picture: Option,
+}
+
+#[derive(Debug, Clone, Default, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Image {
+ pub data: String,
+ pub asset_url: Option,
+ pub format: String,
+ #[serde(default)]
+ pub saved: bool,
+ pub exif: serde_json::Value,
+}
diff --git a/plugins/camera/tsconfig.json b/plugins/camera/tsconfig.json
new file mode 100644
index 00000000..5098169a
--- /dev/null
+++ b/plugins/camera/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["guest-js/*.ts"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a8fb00f2..9f3afc22 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -52,6 +52,34 @@ importers:
devDependencies:
tslib: 2.4.1
+ plugins/camera:
+ specifiers:
+ '@tauri-apps/api': ^2.0.0-alpha.0
+ tslib: ^2.4.1
+ dependencies:
+ '@tauri-apps/api': 2.0.0-alpha.0
+ devDependencies:
+ tslib: 2.4.1
+
+ plugins/camera/examples/tauri-app:
+ specifiers:
+ '@sveltejs/vite-plugin-svelte': ^1.0.1
+ '@tauri-apps/api': ^1.1.0
+ '@tauri-apps/cli': ^2.0.0-alpha.0
+ internal-ip: ^7.0.0
+ svelte: ^3.49.0
+ tauri-plugin-camera-api: link:../../
+ vite: ^3.0.2
+ dependencies:
+ '@tauri-apps/api': 1.2.0
+ tauri-plugin-camera-api: link:../..
+ devDependencies:
+ '@sveltejs/vite-plugin-svelte': 1.4.0_svelte@3.55.1+vite@3.2.5
+ '@tauri-apps/cli': 2.0.0-alpha.2
+ internal-ip: 7.0.0
+ svelte: 3.55.1
+ vite: 3.2.5
+
plugins/fs-extra:
specifiers:
'@tauri-apps/api': ^1.2.0
@@ -164,6 +192,15 @@ importers:
packages:
+ /@esbuild/android-arm/0.15.18:
+ resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/android-arm/0.16.16:
resolution: {integrity: sha512-BUuWMlt4WSXod1HSl7aGK8fJOsi+Tab/M0IDK1V1/GstzoOpqc/v3DqmN8MkuapPKQ9Br1WtLAN4uEgWR8x64A==}
engines: {node: '>=12'}
@@ -254,6 +291,15 @@ packages:
dev: true
optional: true
+ /@esbuild/linux-loong64/0.15.18:
+ resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/linux-loong64/0.16.16:
resolution: {integrity: sha512-MdUFggHjRiCCwNE9+1AibewoNq6wf94GLB9Q9aXwl+a75UlRmbRK3h6WJyrSGA6ZstDJgaD2wiTSP7tQNUYxwA==}
engines: {node: '>=12'}
@@ -565,6 +611,25 @@ packages:
- supports-color
dev: true
+ /@sveltejs/vite-plugin-svelte/1.4.0_svelte@3.55.1+vite@3.2.5:
+ resolution: {integrity: sha512-6QupI/jemMfK+yI2pMtJcu5iO2gtgTfcBdGwMZZt+lgbFELhszbDl6Qjh000HgAV8+XUA+8EY8DusOFk8WhOIg==}
+ engines: {node: ^14.18.0 || >= 16}
+ peerDependencies:
+ svelte: ^3.44.0
+ vite: ^3.0.0
+ dependencies:
+ debug: 4.3.4
+ deepmerge: 4.2.2
+ kleur: 4.1.5
+ magic-string: 0.26.7
+ svelte: 3.55.1
+ svelte-hmr: 0.15.1_svelte@3.55.1
+ vite: 3.2.5
+ vitefu: 0.2.4_vite@3.2.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@sveltejs/vite-plugin-svelte/2.0.2_svelte@3.55.1+vite@4.0.4:
resolution: {integrity: sha512-xCEan0/NNpQuL0l5aS42FjwQ6wwskdxC3pW1OeFtEKNZwRg7Evro9lac9HesGP6TdFsTv2xMes5ASQVKbCacxg==}
engines: {node: ^14.18.0 || >= 16}
@@ -589,6 +654,11 @@ packages:
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
dev: false
+ /@tauri-apps/api/2.0.0-alpha.0:
+ resolution: {integrity: sha512-PQdy1Ao6JwKwW2/C11nP+IqnrWHB7+UgbM71zbzA1W3+1yyd9Zg+K7rzZ7f3yhvD7kdxmXUN3KgSfGeiDFzZ2A==}
+ engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
+ dev: false
+
/@tauri-apps/cli-darwin-arm64/1.2.2:
resolution: {integrity: sha512-W+Cp2weUMlvmGkRJeUjypbz9Lpl6o98xkgKAtobZSum5SNwpsBQfawJTESakNoD+FXyVg/snIk5sRdHge+tAaA==}
engines: {node: '>= 10'}
@@ -598,6 +668,15 @@ packages:
dev: false
optional: true
+ /@tauri-apps/cli-darwin-arm64/2.0.0-alpha.2:
+ resolution: {integrity: sha512-Wu5QdZUgh0DEE0b3EKdJRkZzFoVngezxgvncQlMdXNaiKjdT767K2fB0XvQps+ycbtVLbUlG15jAwPZbWqRYGw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@tauri-apps/cli-darwin-x64/1.2.2:
resolution: {integrity: sha512-vmVAqt+ECH2d6cbcGJ7ddcCAZgmKe5xmxlL5r4xoaphu7OqU4gnv4VFURYkVltOfwzIFQVOPVSqwYyIDToCYNQ==}
engines: {node: '>= 10'}
@@ -607,6 +686,15 @@ packages:
dev: false
optional: true
+ /@tauri-apps/cli-darwin-x64/2.0.0-alpha.2:
+ resolution: {integrity: sha512-e5VLsT/exSW1swUWkhCEAQ/fM8mZaUMoGeyESYtO7VfTNVglS0j+VfQ9a8taRxtOkajDZmqMDvmii4tA5I1Bbw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@tauri-apps/cli-linux-arm-gnueabihf/1.2.2:
resolution: {integrity: sha512-yYTdQurgi4QZR8z+fANjl522jdQz/VtesFpw+C/A0+zXg7tiRjicsywBDdPsvNzCqFeGKKkmTR+Lny5qxhGaeQ==}
engines: {node: '>= 10'}
@@ -616,6 +704,15 @@ packages:
dev: false
optional: true
+ /@tauri-apps/cli-linux-arm-gnueabihf/2.0.0-alpha.2:
+ resolution: {integrity: sha512-+/emaFpDPuqnTIyh+WcDqCbzc/SoREFfLDyumqdnFjRU1Uvc2Z9Eo/sMVnfuUw5vDMc2EPzYtT3uiZGez65ZTA==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@tauri-apps/cli-linux-arm64-gnu/1.2.2:
resolution: {integrity: sha512-ZSOVT6Eq1ay2+27B8KfA0MnpO7KYzONU6TjenH7DNcQki6eWGG5JoNu8QQ9Mdn3dAzY0XBP9i1ZHQOFu4iPtEg==}
engines: {node: '>= 10'}
@@ -625,6 +722,15 @@ packages:
dev: false
optional: true
+ /@tauri-apps/cli-linux-arm64-gnu/2.0.0-alpha.2:
+ resolution: {integrity: sha512-UHAyqt8fFbp9MEbUHSiEKjJC98w/Dta3r9auE70K+/uNpt9pnP/lGturDWWAJagRIFwYKPyqSEqL5qFcKadYqw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@tauri-apps/cli-linux-arm64-musl/1.2.2:
resolution: {integrity: sha512-/NHSkqNQ+Pr4PshvyD1CeNFaPCaCpe1OeuAQgVi0rboSecC9fXN96G5dQbSBoxOUcCo6f8aTVE7zkZ4WchFVog==}
engines: {node: '>= 10'}
@@ -634,6 +740,15 @@ packages:
dev: false
optional: true
+ /@tauri-apps/cli-linux-arm64-musl/2.0.0-alpha.2:
+ resolution: {integrity: sha512-3euwm11RWvmTX+ISR/Y+N0TaWTJCRIj1pDgB+r2ZptRKlVTMNTJZDTXQlyJKcWEpi1azlbdxAzRvhN8NrgDmyA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@tauri-apps/cli-linux-x64-gnu/1.2.2:
resolution: {integrity: sha512-4YTmfPuyvlHsvCkATDMwhklfuQm3HKxYXv/IOW9H0ra6pS9efVhrFYIC9Vfv6XaKN85Vnn/FYTEGMJLwCxZw2Q==}
engines: {node: '>= 10'}
@@ -643,6 +758,15 @@ packages:
dev: false
optional: true
+ /@tauri-apps/cli-linux-x64-gnu/2.0.0-alpha.2:
+ resolution: {integrity: sha512-ijJ8Wij5mVd9p6lXQ+pXoFlx3Iv1JS1KQTeySICds43xzE8esGp5+HXRXDwWqQLdVmtI77P5VRIe2ssXiaeDUg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@tauri-apps/cli-linux-x64-musl/1.2.2:
resolution: {integrity: sha512-wr46tbscwFuCcA931R+ItOiUTT0djMmgKLd1HFCmFF82V9BKE2reIjr6O9l0NCXCo2WeD4pe3jA/Pt1dxDu+JA==}
engines: {node: '>= 10'}
@@ -652,6 +776,15 @@ packages:
dev: false
optional: true
+ /@tauri-apps/cli-linux-x64-musl/2.0.0-alpha.2:
+ resolution: {integrity: sha512-sg8OTQfG/zJ4+6MA/+hk08hVb57iJn5VZDzBb3o6IpJ0cwtM8YDNv5C+6HWttBuxsn4oEoYxGml/FvowMfsOCg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@tauri-apps/cli-win32-ia32-msvc/1.2.2:
resolution: {integrity: sha512-6VmbVJOWUZJK5/JKhb3mNFKrKGfq0KV7lJGumfN95WJgkHeyL61p8bZit+o6ZgUGUhrOabkAawhDkrRY+ZQhIw==}
engines: {node: '>= 10'}
@@ -661,6 +794,15 @@ packages:
dev: false
optional: true
+ /@tauri-apps/cli-win32-ia32-msvc/2.0.0-alpha.2:
+ resolution: {integrity: sha512-R1AmO3GEm97ptM0tjxZjZ1fLnxzN3ZeOEKc85nR7ayqVqKVhMu+dhq5lKa/Y3GdMUR6Yj9GoCnaLp2xy4bV6JQ==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@tauri-apps/cli-win32-x64-msvc/1.2.2:
resolution: {integrity: sha512-YRPJguJma+zSuRZpFoSZqls6+laggG1vqG0FPQWQTi+ywATgMpai2b2RZnffDlpHKp9mt4V/s2dtqOy6bpGZHg==}
engines: {node: '>= 10'}
@@ -670,6 +812,15 @@ packages:
dev: false
optional: true
+ /@tauri-apps/cli-win32-x64-msvc/2.0.0-alpha.2:
+ resolution: {integrity: sha512-cLJJWxCdvvQP+I0B4h6h0TMMNYISoatQu57QVxPqypbkC/lK/ljjrbD5nu7M9wTFBkLkCTGyMC7N99esCmgIBQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@tauri-apps/cli/1.2.2:
resolution: {integrity: sha512-D8zib3A0vWCvPPSyYLxww/OdDlVcY7fpcDVBH6qUvheOjj2aCyU7H9AYMRBwpgCfz8zY5+vomee+laLeB0H13w==}
engines: {node: '>= 10'}
@@ -686,6 +837,22 @@ packages:
'@tauri-apps/cli-win32-x64-msvc': 1.2.2
dev: false
+ /@tauri-apps/cli/2.0.0-alpha.2:
+ resolution: {integrity: sha512-M5o2ESOv9jGr7oIDl3sR3Q5++DXSW4xyfxzKCyu1JVGlOc+C9Q4y0dbKhlpd0wPCAxRa0ikbfu7z8qfEhHSpVQ==}
+ engines: {node: '>= 10'}
+ hasBin: true
+ optionalDependencies:
+ '@tauri-apps/cli-darwin-arm64': 2.0.0-alpha.2
+ '@tauri-apps/cli-darwin-x64': 2.0.0-alpha.2
+ '@tauri-apps/cli-linux-arm-gnueabihf': 2.0.0-alpha.2
+ '@tauri-apps/cli-linux-arm64-gnu': 2.0.0-alpha.2
+ '@tauri-apps/cli-linux-arm64-musl': 2.0.0-alpha.2
+ '@tauri-apps/cli-linux-x64-gnu': 2.0.0-alpha.2
+ '@tauri-apps/cli-linux-x64-musl': 2.0.0-alpha.2
+ '@tauri-apps/cli-win32-ia32-msvc': 2.0.0-alpha.2
+ '@tauri-apps/cli-win32-x64-msvc': 2.0.0-alpha.2
+ dev: true
+
/@types/cookie/0.5.1:
resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==}
dev: true
@@ -1086,6 +1253,13 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /default-gateway/6.0.3:
+ resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==}
+ engines: {node: '>= 10'}
+ dependencies:
+ execa: 5.1.1
+ dev: true
+
/define-properties/1.1.4:
resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
engines: {node: '>= 0.4'}
@@ -1174,6 +1348,216 @@ packages:
resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
dev: true
+ /esbuild-android-64/0.15.18:
+ resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-android-arm64/0.15.18:
+ resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-darwin-64/0.15.18:
+ resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-darwin-arm64/0.15.18:
+ resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-freebsd-64/0.15.18:
+ resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-freebsd-arm64/0.15.18:
+ resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-32/0.15.18:
+ resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-64/0.15.18:
+ resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-arm/0.15.18:
+ resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-arm64/0.15.18:
+ resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-mips64le/0.15.18:
+ resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-ppc64le/0.15.18:
+ resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-riscv64/0.15.18:
+ resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-s390x/0.15.18:
+ resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-netbsd-64/0.15.18:
+ resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-openbsd-64/0.15.18:
+ resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-sunos-64/0.15.18:
+ resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-windows-32/0.15.18:
+ resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-windows-64/0.15.18:
+ resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-windows-arm64/0.15.18:
+ resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild/0.15.18:
+ resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==}
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@esbuild/android-arm': 0.15.18
+ '@esbuild/linux-loong64': 0.15.18
+ esbuild-android-64: 0.15.18
+ esbuild-android-arm64: 0.15.18
+ esbuild-darwin-64: 0.15.18
+ esbuild-darwin-arm64: 0.15.18
+ esbuild-freebsd-64: 0.15.18
+ esbuild-freebsd-arm64: 0.15.18
+ esbuild-linux-32: 0.15.18
+ esbuild-linux-64: 0.15.18
+ esbuild-linux-arm: 0.15.18
+ esbuild-linux-arm64: 0.15.18
+ esbuild-linux-mips64le: 0.15.18
+ esbuild-linux-ppc64le: 0.15.18
+ esbuild-linux-riscv64: 0.15.18
+ esbuild-linux-s390x: 0.15.18
+ esbuild-netbsd-64: 0.15.18
+ esbuild-openbsd-64: 0.15.18
+ esbuild-sunos-64: 0.15.18
+ esbuild-windows-32: 0.15.18
+ esbuild-windows-64: 0.15.18
+ esbuild-windows-arm64: 0.15.18
+ dev: true
+
/esbuild/0.16.16:
resolution: {integrity: sha512-24JyKq10KXM5EBIgPotYIJ2fInNWVVqflv3gicIyQqfmUqi4HvDW1VR790cBgLJHCl96Syy7lhoz7tLFcmuRmg==}
engines: {node: '>=12'}
@@ -1502,6 +1886,21 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /execa/5.1.1:
+ resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
+ engines: {node: '>=10'}
+ dependencies:
+ cross-spawn: 7.0.3
+ get-stream: 6.0.1
+ human-signals: 2.1.0
+ is-stream: 2.0.1
+ merge-stream: 2.0.0
+ npm-run-path: 4.0.1
+ onetime: 5.1.2
+ signal-exit: 3.0.7
+ strip-final-newline: 2.0.0
+ dev: true
+
/fast-deep-equal/3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
@@ -1603,6 +2002,11 @@ packages:
has-symbols: 1.0.3
dev: true
+ /get-stream/6.0.1:
+ resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
+ engines: {node: '>=10'}
+ dev: true
+
/get-symbol-description/1.0.0:
resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
engines: {node: '>= 0.4'}
@@ -1711,6 +2115,11 @@ packages:
function-bind: 1.1.1
dev: true
+ /human-signals/2.1.0:
+ resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
+ engines: {node: '>=10.17.0'}
+ dev: true
+
/ignore/5.2.1:
resolution: {integrity: sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==}
engines: {node: '>= 4'}
@@ -1744,6 +2153,16 @@ packages:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
+ /internal-ip/7.0.0:
+ resolution: {integrity: sha512-qE4TeD4brqC45Vq/+VASeMiS1KRyfBkR6HT2sh9pZVVCzSjPkaCEfKFU+dL0PRv7NHJtvoKN2r82G6wTfzorkw==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dependencies:
+ default-gateway: 6.0.3
+ ipaddr.js: 2.0.1
+ is-ip: 3.1.0
+ p-event: 4.2.0
+ dev: true
+
/internal-slot/1.0.4:
resolution: {integrity: sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==}
engines: {node: '>= 0.4'}
@@ -1753,6 +2172,16 @@ packages:
side-channel: 1.0.4
dev: true
+ /ip-regex/4.3.0:
+ resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /ipaddr.js/2.0.1:
+ resolution: {integrity: sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==}
+ engines: {node: '>= 10'}
+ dev: true
+
/is-bigint/1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies:
@@ -1811,6 +2240,13 @@ packages:
is-extglob: 2.1.1
dev: true
+ /is-ip/3.1.0:
+ resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==}
+ engines: {node: '>=8'}
+ dependencies:
+ ip-regex: 4.3.0
+ dev: true
+
/is-module/1.0.0:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
dev: true
@@ -1851,6 +2287,11 @@ packages:
call-bind: 1.0.2
dev: true
+ /is-stream/2.0.1:
+ resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
+ engines: {node: '>=8'}
+ dev: true
+
/is-string/1.0.7:
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
engines: {node: '>= 0.4'}
@@ -1938,6 +2379,13 @@ packages:
sourcemap-codec: 1.4.8
dev: true
+ /magic-string/0.26.7:
+ resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==}
+ engines: {node: '>=12'}
+ dependencies:
+ sourcemap-codec: 1.4.8
+ dev: true
+
/magic-string/0.27.0:
resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
engines: {node: '>=12'}
@@ -1945,6 +2393,10 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
+ /merge-stream/2.0.0:
+ resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+ dev: true
+
/merge2/1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -1964,6 +2416,11 @@ packages:
hasBin: true
dev: true
+ /mimic-fn/2.1.0:
+ resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+ engines: {node: '>=6'}
+ dev: true
+
/min-indent/1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
@@ -2027,6 +2484,13 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /npm-run-path/4.0.1:
+ resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
+ engines: {node: '>=8'}
+ dependencies:
+ path-key: 3.1.1
+ dev: true
+
/object-inspect/1.12.2:
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
dev: true
@@ -2061,6 +2525,13 @@ packages:
wrappy: 1.0.2
dev: true
+ /onetime/5.1.2:
+ resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
+ engines: {node: '>=6'}
+ dependencies:
+ mimic-fn: 2.1.0
+ dev: true
+
/optionator/0.9.1:
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
engines: {node: '>= 0.8.0'}
@@ -2073,6 +2544,18 @@ packages:
word-wrap: 1.2.3
dev: true
+ /p-event/4.2.0:
+ resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ p-timeout: 3.2.0
+ dev: true
+
+ /p-finally/1.0.0:
+ resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
+ engines: {node: '>=4'}
+ dev: true
+
/p-limit/3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@@ -2087,6 +2570,13 @@ packages:
p-limit: 3.1.0
dev: true
+ /p-timeout/3.2.0:
+ resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
+ engines: {node: '>=8'}
+ dependencies:
+ p-finally: 1.0.0
+ dev: true
+
/parent-module/1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -2216,6 +2706,14 @@ packages:
glob: 7.2.3
dev: true
+ /rollup/2.79.1:
+ resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
+ engines: {node: '>=10.0.0'}
+ hasBin: true
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: true
+
/rollup/3.7.4:
resolution: {integrity: sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
@@ -2296,6 +2794,10 @@ packages:
object-inspect: 1.12.2
dev: true
+ /signal-exit/3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+ dev: true
+
/sirv/2.0.2:
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
engines: {node: '>= 10'}
@@ -2379,6 +2881,11 @@ packages:
engines: {node: '>=4'}
dev: true
+ /strip-final-newline/2.0.0:
+ resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
+ engines: {node: '>=6'}
+ dev: true
+
/strip-indent/3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
@@ -2597,6 +3104,39 @@ packages:
punycode: 2.1.1
dev: true
+ /vite/3.2.5:
+ resolution: {integrity: sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': '>= 14'
+ less: '*'
+ sass: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ dependencies:
+ esbuild: 0.15.18
+ postcss: 8.4.21
+ resolve: 1.22.1
+ rollup: 2.79.1
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: true
+
/vite/4.0.4:
resolution: {integrity: sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -2630,6 +3170,17 @@ packages:
fsevents: 2.3.2
dev: true
+ /vitefu/0.2.4_vite@3.2.5:
+ resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+ dependencies:
+ vite: 3.2.5
+ dev: true
+
/vitefu/0.2.4_vite@4.0.4:
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
peerDependencies: