mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-06-12 14:17:48 +02:00
Merge branch 'v2' into feat/camera
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-plugin-authenticator"
|
||||
version = "0.1.0"
|
||||
version = "0.0.0"
|
||||
description = "Use hardware security-keys in your Tauri App."
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
@@ -18,7 +18,7 @@ thiserror.workspace = true
|
||||
authenticator = "0.3.1"
|
||||
once_cell = "1"
|
||||
sha2 = "0.10"
|
||||
base64 = { version = "^0.13" }
|
||||
base64 = "0.21"
|
||||
u2f = "0.2"
|
||||
chrono = "0.4"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||
[dependencies]
|
||||
tauri-plugin-authenticator = "0.1"
|
||||
# or through git
|
||||
tauri-plugin-authenticator = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
||||
tauri-plugin-authenticator = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
```
|
||||
|
||||
You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
|
||||
@@ -28,11 +28,11 @@ You can install the JavaScript Guest bindings using your preferred JavaScript pa
|
||||
> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
|
||||
|
||||
```sh
|
||||
pnpm add https://github.com/tauri-apps/tauri-plugin-authenticator
|
||||
pnpm add https://github.com/tauri-apps/tauri-plugin-authenticator#v2
|
||||
# or
|
||||
npm add https://github.com/tauri-apps/tauri-plugin-authenticator
|
||||
npm add https://github.com/tauri-apps/tauri-plugin-authenticator#v2
|
||||
# or
|
||||
yarn add https://github.com/tauri-apps/tauri-plugin-authenticator
|
||||
yarn add https://github.com/tauri-apps/tauri-plugin-authenticator#v2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
|
||||
@@ -6,7 +6,7 @@ use authenticator::{
|
||||
authenticatorservice::AuthenticatorService, statecallback::StateCallback,
|
||||
AuthenticatorTransports, KeyHandle, RegisterFlags, SignFlags, StatusUpdate,
|
||||
};
|
||||
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -75,9 +75,9 @@ pub fn register(application: String, timeout: u64, challenge: String) -> crate::
|
||||
|
||||
let (key_handle, public_key) =
|
||||
_u2f_get_key_handle_and_public_key_from_register_response(®ister_data).unwrap();
|
||||
let key_handle_base64 = encode_config(key_handle, URL_SAFE_NO_PAD);
|
||||
let public_key_base64 = encode_config(public_key, URL_SAFE_NO_PAD);
|
||||
let register_data_base64 = encode_config(®ister_data, URL_SAFE_NO_PAD);
|
||||
let key_handle_base64 = URL_SAFE_NO_PAD.encode(key_handle);
|
||||
let public_key_base64 = URL_SAFE_NO_PAD.encode(public_key);
|
||||
let register_data_base64 = URL_SAFE_NO_PAD.encode(®ister_data);
|
||||
println!("Key Handle: {}", &key_handle_base64);
|
||||
println!("Public Key: {}", &public_key_base64);
|
||||
|
||||
@@ -108,7 +108,7 @@ pub fn sign(
|
||||
challenge: String,
|
||||
key_handle: String,
|
||||
) -> crate::Result<String> {
|
||||
let credential = match decode_config(key_handle, URL_SAFE_NO_PAD) {
|
||||
let credential = match URL_SAFE_NO_PAD.decode(key_handle) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return Err(e.into());
|
||||
@@ -152,19 +152,16 @@ pub fn sign(
|
||||
|
||||
let (_, handle_used, sign_data, device_info) = sign_result.unwrap();
|
||||
|
||||
let sig = encode_config(sign_data, URL_SAFE_NO_PAD);
|
||||
let sig = URL_SAFE_NO_PAD.encode(sign_data);
|
||||
|
||||
println!("Sign result: {sig}");
|
||||
println!(
|
||||
"Key handle used: {}",
|
||||
encode_config(&handle_used, URL_SAFE_NO_PAD)
|
||||
);
|
||||
println!("Key handle used: {}", URL_SAFE_NO_PAD.encode(&handle_used));
|
||||
println!("Device info: {}", &device_info);
|
||||
println!("Done.");
|
||||
|
||||
let res = serde_json::to_string(&Signature {
|
||||
sign_data: sig,
|
||||
key_handle: encode_config(&handle_used, URL_SAFE_NO_PAD),
|
||||
key_handle: URL_SAFE_NO_PAD.encode(&handle_used),
|
||||
})?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use chrono::prelude::*;
|
||||
use serde::Serialize;
|
||||
use std::convert::Into;
|
||||
@@ -15,7 +15,7 @@ static VERSION: &str = "U2F_V2";
|
||||
pub fn make_challenge(app_id: &str, challenge_bytes: Vec<u8>) -> Challenge {
|
||||
let utc: DateTime<Utc> = Utc::now();
|
||||
Challenge {
|
||||
challenge: encode_config(challenge_bytes, URL_SAFE_NO_PAD),
|
||||
challenge: URL_SAFE_NO_PAD.encode(challenge_bytes),
|
||||
timestamp: format!("{utc:?}"),
|
||||
app_id: app_id.to_string(),
|
||||
}
|
||||
@@ -35,10 +35,10 @@ pub fn verify_registration(
|
||||
register_data: String,
|
||||
client_data: String,
|
||||
) -> crate::Result<String> {
|
||||
let challenge_bytes = decode_config(challenge, URL_SAFE_NO_PAD)?;
|
||||
let challenge_bytes = URL_SAFE_NO_PAD.decode(challenge)?;
|
||||
let challenge = make_challenge(&app_id, challenge_bytes);
|
||||
let client_data_bytes: Vec<u8> = client_data.as_bytes().into();
|
||||
let client_data_base64 = encode_config(client_data_bytes, URL_SAFE_NO_PAD);
|
||||
let client_data_base64 = URL_SAFE_NO_PAD.encode(client_data_bytes);
|
||||
let client = U2f::new(app_id);
|
||||
match client.register_response(
|
||||
challenge,
|
||||
@@ -50,8 +50,8 @@ pub fn verify_registration(
|
||||
) {
|
||||
Ok(v) => {
|
||||
let rv = RegistrationVerification {
|
||||
key_handle: encode_config(&v.key_handle, URL_SAFE_NO_PAD),
|
||||
pubkey: encode_config(&v.pub_key, URL_SAFE_NO_PAD),
|
||||
key_handle: URL_SAFE_NO_PAD.encode(&v.key_handle),
|
||||
pubkey: URL_SAFE_NO_PAD.encode(&v.pub_key),
|
||||
device_name: v.device_name,
|
||||
};
|
||||
Ok(serde_json::to_string(&rv)?)
|
||||
@@ -74,12 +74,12 @@ pub fn verify_signature(
|
||||
key_handle: String,
|
||||
pub_key: String,
|
||||
) -> crate::Result<u32> {
|
||||
let challenge_bytes = decode_config(challenge, URL_SAFE_NO_PAD)?;
|
||||
let challenge_bytes = URL_SAFE_NO_PAD.decode(challenge)?;
|
||||
let chal = make_challenge(&app_id, challenge_bytes);
|
||||
let client_data_bytes: Vec<u8> = client_data.as_bytes().into();
|
||||
let client_data_base64 = encode_config(client_data_bytes, URL_SAFE_NO_PAD);
|
||||
let key_handle_bytes = decode_config(&key_handle, URL_SAFE_NO_PAD)?;
|
||||
let pubkey_bytes = decode_config(pub_key, URL_SAFE_NO_PAD)?;
|
||||
let client_data_base64 = URL_SAFE_NO_PAD.encode(client_data_bytes);
|
||||
let key_handle_bytes = URL_SAFE_NO_PAD.decode(&key_handle)?;
|
||||
let pubkey_bytes = URL_SAFE_NO_PAD.decode(pub_key)?;
|
||||
let client = U2f::new(app_id);
|
||||
let mut _counter: u32 = 0;
|
||||
match client.sign_response(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-plugin-autostart"
|
||||
version = "0.1.0"
|
||||
version = "0.0.0"
|
||||
description = "Automatically launch your application at startup."
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -18,7 +18,7 @@ Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
||||
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
```
|
||||
|
||||
You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
|
||||
@@ -26,11 +26,11 @@ You can install the JavaScript Guest bindings using your preferred JavaScript pa
|
||||
> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
|
||||
|
||||
```sh
|
||||
pnpm add https://github.com/tauri-apps/tauri-plugin-autostart
|
||||
pnpm add https://github.com/tauri-apps/tauri-plugin-autostart#v2
|
||||
# or
|
||||
npm add https://github.com/tauri-apps/tauri-plugin-autostart
|
||||
npm add https://github.com/tauri-apps/tauri-plugin-autostart#v2
|
||||
# or
|
||||
yarn add https://github.com/tauri-apps/tauri-plugin-autostart
|
||||
yarn add https://github.com/tauri-apps/tauri-plugin-autostart#v2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "tauri-plugin-cli"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
clap = { version = "4", features = ["string"] }
|
||||
@@ -0,0 +1,65 @@
|
||||

|
||||
|
||||
<!-- description -->
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
There are three general methods of installation that we can recommend.
|
||||
|
||||
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
|
||||
2. Pull sources directly from Github using git tags / revision hashes (most secure)
|
||||
3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use)
|
||||
|
||||
Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||
|
||||
`src-tauri/Cargo.toml`
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri-plugin-cli = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "next" }
|
||||
```
|
||||
|
||||
You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
|
||||
|
||||
> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
|
||||
|
||||
```sh
|
||||
pnpm add https://github.com/tauri-apps/tauri-plugin-cli#v2
|
||||
# or
|
||||
npm add https://github.com/tauri-apps/tauri-plugin-cli#v2
|
||||
# or
|
||||
yarn add https://github.com/tauri-apps/tauri-plugin-cli#v2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
First you need to register the core plugin with Tauri:
|
||||
|
||||
`src-tauri/src/main.rs`
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_cli::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
```
|
||||
|
||||
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
|
||||
|
||||
```javascript
|
||||
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
|
||||
|
||||
## License
|
||||
|
||||
Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy.
|
||||
|
||||
MIT or MIT/Apache 2.0 where applicable.
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Parse arguments from your Command Line Interface.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface ArgMatch {
|
||||
/**
|
||||
* string if takes value
|
||||
* boolean if flag
|
||||
* string[] or null if takes multiple values
|
||||
*/
|
||||
value: string | boolean | string[] | null;
|
||||
/**
|
||||
* Number of occurrences
|
||||
*/
|
||||
occurrences: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface SubcommandMatch {
|
||||
name: string;
|
||||
matches: CliMatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface CliMatches {
|
||||
args: Record<string, ArgMatch>;
|
||||
subcommand: SubcommandMatch | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the arguments provided to the current process and get the matches using the configuration defined [`tauri.cli`](https://tauri.app/v1/api/config/#tauriconfig.cli) in `tauri.conf.json`
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getMatches } from 'tauri-plugin-cli-api';
|
||||
* const matches = await getMatches();
|
||||
* if (matches.subcommand?.name === 'run') {
|
||||
* // `./your-app run $ARGS` was executed
|
||||
* const args = matches.subcommand?.matches.args
|
||||
* if ('debug' in args) {
|
||||
* // `./your-app run --debug` was executed
|
||||
* }
|
||||
* } else {
|
||||
* const args = matches.args
|
||||
* // `./your-app $ARGS` was executed
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function getMatches(): Promise<CliMatches> {
|
||||
return await invoke("plugin:cli|cli_matches");
|
||||
}
|
||||
|
||||
export type { ArgMatch, SubcommandMatch, CliMatches };
|
||||
|
||||
export { getMatches };
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "tauri-plugin-fs-extra-api",
|
||||
"name": "tauri-plugin-cli-api",
|
||||
"version": "0.0.0",
|
||||
"description": "Additional file system methods not included in the core API.",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"authors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
@@ -0,0 +1,174 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
/// A CLI argument definition.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct Arg {
|
||||
/// The short version of the argument, without the preceding -.
|
||||
///
|
||||
/// NOTE: Any leading `-` characters will be stripped, and only the first non-character will be used as the short version.
|
||||
pub short: Option<char>,
|
||||
/// The unique argument name
|
||||
pub name: String,
|
||||
/// The argument description which will be shown on the help information.
|
||||
/// Typically, this is a short (one line) description of the arg.
|
||||
pub description: Option<String>,
|
||||
/// The argument long description which will be shown on the help information.
|
||||
/// Typically this a more detailed (multi-line) message that describes the argument.
|
||||
#[serde(alias = "long-description")]
|
||||
pub long_description: Option<String>,
|
||||
/// Specifies that the argument takes a value at run time.
|
||||
///
|
||||
/// NOTE: values for arguments may be specified in any of the following methods
|
||||
/// - Using a space such as -o value or --option value
|
||||
/// - Using an equals and no space such as -o=value or --option=value
|
||||
/// - Use a short and no space such as -ovalue
|
||||
#[serde(default, alias = "takes-value")]
|
||||
pub takes_value: bool,
|
||||
/// Specifies that the argument may have an unknown number of multiple values. Without any other settings, this argument may appear only once.
|
||||
///
|
||||
/// For example, --opt val1 val2 is allowed, but --opt val1 val2 --opt val3 is not.
|
||||
///
|
||||
/// NOTE: Setting this requires `takes_value` to be set to true.
|
||||
#[serde(default)]
|
||||
pub multiple: bool,
|
||||
/// Specifies how many values are required to satisfy this argument. For example, if you had a
|
||||
/// `-f <file>` argument where you wanted exactly 3 'files' you would set
|
||||
/// `number_of_values = 3`, and this argument wouldn't be satisfied unless the user provided
|
||||
/// 3 and only 3 values.
|
||||
///
|
||||
/// **NOTE:** Does *not* require `multiple_occurrences = true` to be set. Setting
|
||||
/// `multiple_occurrences = true` would allow `-f <file> <file> <file> -f <file> <file> <file>` where
|
||||
/// as *not* setting it would only allow one occurrence of this argument.
|
||||
///
|
||||
/// **NOTE:** implicitly sets `takes_value = true` and `multiple_values = true`.
|
||||
#[serde(alias = "number-of-values")]
|
||||
pub number_of_values: Option<usize>,
|
||||
/// Specifies a list of possible values for this argument.
|
||||
/// At runtime, the CLI verifies that only one of the specified values was used, or fails with an error message.
|
||||
#[serde(alias = "possible-values")]
|
||||
pub possible_values: Option<Vec<String>>,
|
||||
/// Specifies the minimum number of values for this argument.
|
||||
/// For example, if you had a -f `<file>` argument where you wanted at least 2 'files',
|
||||
/// you would set `minValues: 2`, and this argument would be satisfied if the user provided, 2 or more values.
|
||||
#[serde(alias = "min-values")]
|
||||
pub min_values: Option<usize>,
|
||||
/// Specifies the maximum number of values are for this argument.
|
||||
/// For example, if you had a -f `<file>` argument where you wanted up to 3 'files',
|
||||
/// you would set .max_values(3), and this argument would be satisfied if the user provided, 1, 2, or 3 values.
|
||||
#[serde(alias = "max-values")]
|
||||
pub max_values: Option<usize>,
|
||||
/// Sets whether or not the argument is required by default.
|
||||
///
|
||||
/// - Required by default means it is required, when no other conflicting rules have been evaluated
|
||||
/// - Conflicting rules take precedence over being required.
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
/// Sets an arg that override this arg's required setting
|
||||
/// i.e. this arg will be required unless this other argument is present.
|
||||
#[serde(alias = "required-unless-present")]
|
||||
pub required_unless_present: Option<String>,
|
||||
/// Sets args that override this arg's required setting
|
||||
/// i.e. this arg will be required unless all these other arguments are present.
|
||||
#[serde(alias = "required-unless-present-all")]
|
||||
pub required_unless_present_all: Option<Vec<String>>,
|
||||
/// Sets args that override this arg's required setting
|
||||
/// i.e. this arg will be required unless at least one of these other arguments are present.
|
||||
#[serde(alias = "required-unless-present-any")]
|
||||
pub required_unless_present_any: Option<Vec<String>>,
|
||||
/// Sets a conflicting argument by name
|
||||
/// i.e. when using this argument, the following argument can't be present and vice versa.
|
||||
#[serde(alias = "conflicts-with")]
|
||||
pub conflicts_with: Option<String>,
|
||||
/// The same as conflictsWith but allows specifying multiple two-way conflicts per argument.
|
||||
#[serde(alias = "conflicts-with-all")]
|
||||
pub conflicts_with_all: Option<Vec<String>>,
|
||||
/// Tets an argument by name that is required when this one is present
|
||||
/// i.e. when using this argument, the following argument must be present.
|
||||
pub requires: Option<String>,
|
||||
/// Sts multiple arguments by names that are required when this one is present
|
||||
/// i.e. when using this argument, the following arguments must be present.
|
||||
#[serde(alias = "requires-all")]
|
||||
pub requires_all: Option<Vec<String>>,
|
||||
/// Allows a conditional requirement with the signature [arg, value]
|
||||
/// the requirement will only become valid if `arg`'s value equals `${value}`.
|
||||
#[serde(alias = "requires-if")]
|
||||
pub requires_if: Option<(String, String)>,
|
||||
/// Allows specifying that an argument is required conditionally with the signature [arg, value]
|
||||
/// the requirement will only become valid if the `arg`'s value equals `${value}`.
|
||||
#[serde(alias = "required-if-eq")]
|
||||
pub required_if_eq: Option<(String, String)>,
|
||||
/// Requires that options use the --option=val syntax
|
||||
/// i.e. an equals between the option and associated value.
|
||||
#[serde(alias = "requires-equals")]
|
||||
pub require_equals: Option<bool>,
|
||||
/// The positional argument index, starting at 1.
|
||||
///
|
||||
/// The index refers to position according to other positional argument.
|
||||
/// It does not define position in the argument list as a whole. When utilized with multiple=true,
|
||||
/// only the last positional argument may be defined as multiple (i.e. the one with the highest index).
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
|
||||
/// describes a CLI configuration
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
/// Command description which will be shown on the help information.
|
||||
pub description: Option<String>,
|
||||
/// Command long description which will be shown on the help information.
|
||||
#[serde(alias = "long-description")]
|
||||
pub long_description: Option<String>,
|
||||
/// Adds additional help information to be displayed in addition to auto-generated help.
|
||||
/// This information is displayed before the auto-generated help information.
|
||||
/// This is often used for header information.
|
||||
#[serde(alias = "before-help")]
|
||||
pub before_help: Option<String>,
|
||||
/// Adds additional help information to be displayed in addition to auto-generated help.
|
||||
/// This information is displayed after the auto-generated help information.
|
||||
/// This is often used to describe how to use the arguments, or caveats to be noted.
|
||||
#[serde(alias = "after-help")]
|
||||
pub after_help: Option<String>,
|
||||
/// List of arguments for the command
|
||||
pub args: Option<Vec<Arg>>,
|
||||
/// List of subcommands of this command
|
||||
pub subcommands: Option<HashMap<String, Config>>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// List of arguments for the command
|
||||
pub fn args(&self) -> Option<&Vec<Arg>> {
|
||||
self.args.as_ref()
|
||||
}
|
||||
|
||||
/// List of subcommands of this command
|
||||
pub fn subcommands(&self) -> Option<&HashMap<String, Config>> {
|
||||
self.subcommands.as_ref()
|
||||
}
|
||||
|
||||
/// Command description which will be shown on the help information.
|
||||
pub fn description(&self) -> Option<&String> {
|
||||
self.description.as_ref()
|
||||
}
|
||||
|
||||
/// Command long description which will be shown on the help information.
|
||||
pub fn long_description(&self) -> Option<&String> {
|
||||
self.description.as_ref()
|
||||
}
|
||||
|
||||
/// Adds additional help information to be displayed in addition to auto-generated help.
|
||||
/// This information is displayed before the auto-generated help information.
|
||||
/// This is often used for header information.
|
||||
pub fn before_help(&self) -> Option<&String> {
|
||||
self.before_help.as_ref()
|
||||
}
|
||||
|
||||
/// Adds additional help information to be displayed in addition to auto-generated help.
|
||||
/// This information is displayed after the auto-generated help information.
|
||||
/// This is often used to describe how to use the arguments, or caveats to be noted.
|
||||
pub fn after_help(&self) -> Option<&String> {
|
||||
self.after_help.as_ref()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("failed to parse arguments: {0}")]
|
||||
ParseCli(#[from] clap::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use tauri::{
|
||||
plugin::{Builder, PluginApi, TauriPlugin},
|
||||
AppHandle, Manager, Runtime, State,
|
||||
};
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
mod parser;
|
||||
|
||||
use config::{Arg, Config};
|
||||
pub use error::Error;
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// TODO: use PluginApi#app when 2.0.0-alpha.9 is released
|
||||
pub struct Cli<R: Runtime>(PluginApi<R, Config>, AppHandle<R>);
|
||||
|
||||
impl<R: Runtime> Cli<R> {
|
||||
pub fn matches(&self) -> Result<parser::Matches> {
|
||||
parser::get_matches(self.0.config(), self.1.package_info())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CliExt<R: Runtime> {
|
||||
fn cli(&self) -> &Cli<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> CliExt<R> for T {
|
||||
fn cli(&self) -> &Cli<R> {
|
||||
self.state::<Cli<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn cli_matches<R: Runtime>(_app: AppHandle<R>, cli: State<'_, Cli<R>>) -> Result<parser::Matches> {
|
||||
cli.matches()
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R, Config> {
|
||||
Builder::new("cli")
|
||||
.invoke_handler(tauri::generate_handler![cli_matches])
|
||||
.setup(|app, api| {
|
||||
app.manage(Cli(api, app.clone()));
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use clap::{
|
||||
builder::{PossibleValue, PossibleValuesParser},
|
||||
error::ErrorKind,
|
||||
Arg as ClapArg, ArgAction, ArgMatches, Command,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use tauri::PackageInfo;
|
||||
|
||||
use crate::{Arg, Config};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
/// The resolution of a argument match.
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct ArgData {
|
||||
/// - [`Value::Bool`] if it's a flag,
|
||||
/// - [`Value::Array`] if it's multiple,
|
||||
/// - [`Value::String`] if it has value,
|
||||
/// - [`Value::Null`] otherwise.
|
||||
pub value: Value,
|
||||
/// The number of occurrences of the argument.
|
||||
/// e.g. `./app --arg 1 --arg 2 --arg 2 3 4` results in three occurrences.
|
||||
pub occurrences: u8,
|
||||
}
|
||||
|
||||
/// The matched subcommand.
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct SubcommandMatches {
|
||||
/// The subcommand name.
|
||||
pub name: String,
|
||||
/// The subcommand argument matches.
|
||||
pub matches: Matches,
|
||||
}
|
||||
|
||||
/// The argument matches of a command.
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct Matches {
|
||||
/// Data structure mapping each found arg with its resolution.
|
||||
pub args: HashMap<String, ArgData>,
|
||||
/// The matched subcommand if found.
|
||||
pub subcommand: Option<Box<SubcommandMatches>>,
|
||||
}
|
||||
|
||||
impl Matches {
|
||||
/// Set a arg match.
|
||||
pub(crate) fn set_arg(&mut self, name: String, value: ArgData) {
|
||||
self.args.insert(name, value);
|
||||
}
|
||||
|
||||
/// Sets the subcommand matches.
|
||||
pub(crate) fn set_subcommand(&mut self, name: String, matches: Matches) {
|
||||
self.subcommand = Some(Box::new(SubcommandMatches { name, matches }));
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the argument matches of the CLI definition.
|
||||
///
|
||||
/// This is a low level API. If the application has been built,
|
||||
/// prefer [`App::get_cli_matches`](`crate::App#method.get_cli_matches`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri_plugin_cli::get_matches;
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let matches = get_matches(app.config().tauri.cli.as_ref().unwrap(), app.package_info())?;
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub fn get_matches(cli: &Config, package_info: &PackageInfo) -> crate::Result<Matches> {
|
||||
let about = cli
|
||||
.description()
|
||||
.unwrap_or(&package_info.description.to_string())
|
||||
.to_string();
|
||||
let version = package_info.version.to_string();
|
||||
let app = get_app(
|
||||
package_info,
|
||||
version,
|
||||
package_info.name.clone(),
|
||||
Some(&about),
|
||||
cli,
|
||||
);
|
||||
match app.try_get_matches() {
|
||||
Ok(matches) => Ok(get_matches_internal(cli, &matches)),
|
||||
Err(e) => match e.kind() {
|
||||
ErrorKind::DisplayHelp => {
|
||||
let mut matches = Matches::default();
|
||||
let help_text = e.to_string();
|
||||
matches.args.insert(
|
||||
"help".to_string(),
|
||||
ArgData {
|
||||
value: Value::String(help_text),
|
||||
occurrences: 0,
|
||||
},
|
||||
);
|
||||
Ok(matches)
|
||||
}
|
||||
ErrorKind::DisplayVersion => {
|
||||
let mut matches = Matches::default();
|
||||
matches
|
||||
.args
|
||||
.insert("version".to_string(), Default::default());
|
||||
Ok(matches)
|
||||
}
|
||||
_ => Err(e.into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_matches_internal(config: &Config, matches: &ArgMatches) -> Matches {
|
||||
let mut cli_matches = Matches::default();
|
||||
map_matches(config, matches, &mut cli_matches);
|
||||
|
||||
if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() {
|
||||
if let Some(subcommand_config) = config
|
||||
.subcommands
|
||||
.as_ref()
|
||||
.and_then(|s| s.get(subcommand_name))
|
||||
{
|
||||
cli_matches.set_subcommand(
|
||||
subcommand_name.to_string(),
|
||||
get_matches_internal(subcommand_config, subcommand_matches),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cli_matches
|
||||
}
|
||||
|
||||
fn map_matches(config: &Config, matches: &ArgMatches, cli_matches: &mut Matches) {
|
||||
if let Some(args) = config.args() {
|
||||
for arg in args {
|
||||
let (occurrences, value) = if arg.takes_value {
|
||||
if arg.multiple {
|
||||
matches
|
||||
.get_many::<String>(&arg.name)
|
||||
.map(|v| {
|
||||
let mut values = Vec::new();
|
||||
for value in v {
|
||||
values.push(Value::String(value.into()));
|
||||
}
|
||||
(values.len() as u8, Value::Array(values))
|
||||
})
|
||||
.unwrap_or((0, Value::Null))
|
||||
} else {
|
||||
matches
|
||||
.get_one::<String>(&arg.name)
|
||||
.map(|v| (1, Value::String(v.clone())))
|
||||
.unwrap_or((0, Value::Null))
|
||||
}
|
||||
} else {
|
||||
let occurrences = matches.get_count(&arg.name);
|
||||
(occurrences, Value::Bool(occurrences > 0))
|
||||
};
|
||||
|
||||
cli_matches.set_arg(arg.name.clone(), ArgData { value, occurrences });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_app(
|
||||
package_info: &PackageInfo,
|
||||
version: String,
|
||||
command_name: String,
|
||||
about: Option<&String>,
|
||||
config: &Config,
|
||||
) -> Command {
|
||||
let mut app = Command::new(command_name)
|
||||
.author(package_info.authors)
|
||||
.version(version.clone());
|
||||
|
||||
if let Some(about) = about {
|
||||
app = app.about(about);
|
||||
}
|
||||
if let Some(long_description) = config.long_description() {
|
||||
app = app.long_about(long_description);
|
||||
}
|
||||
if let Some(before_help) = config.before_help() {
|
||||
app = app.before_help(before_help);
|
||||
}
|
||||
if let Some(after_help) = config.after_help() {
|
||||
app = app.after_help(after_help);
|
||||
}
|
||||
|
||||
if let Some(args) = config.args() {
|
||||
for arg in args {
|
||||
app = app.arg(get_arg(arg.name.clone(), arg));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(subcommands) = config.subcommands() {
|
||||
for (subcommand_name, subcommand) in subcommands {
|
||||
let clap_subcommand = get_app(
|
||||
package_info,
|
||||
version.clone(),
|
||||
subcommand_name.to_string(),
|
||||
subcommand.description(),
|
||||
subcommand,
|
||||
);
|
||||
app = app.subcommand(clap_subcommand);
|
||||
}
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
fn get_arg(arg_name: String, arg: &Arg) -> ClapArg {
|
||||
let mut clap_arg = ClapArg::new(arg_name.clone());
|
||||
|
||||
if arg.index.is_none() {
|
||||
clap_arg = clap_arg.long(arg_name);
|
||||
if let Some(short) = arg.short {
|
||||
clap_arg = clap_arg.short(short);
|
||||
}
|
||||
}
|
||||
|
||||
clap_arg = bind_string_arg!(arg, clap_arg, description, help);
|
||||
clap_arg = bind_string_arg!(arg, clap_arg, long_description, long_help);
|
||||
|
||||
let action = if arg.multiple {
|
||||
ArgAction::Append
|
||||
} else if arg.takes_value {
|
||||
ArgAction::Set
|
||||
} else {
|
||||
ArgAction::Count
|
||||
};
|
||||
|
||||
clap_arg = clap_arg.action(action);
|
||||
|
||||
clap_arg = bind_value_arg!(arg, clap_arg, number_of_values);
|
||||
|
||||
if let Some(values) = &arg.possible_values {
|
||||
clap_arg = clap_arg.value_parser(PossibleValuesParser::new(
|
||||
values
|
||||
.iter()
|
||||
.map(PossibleValue::new)
|
||||
.collect::<Vec<PossibleValue>>(),
|
||||
));
|
||||
}
|
||||
|
||||
clap_arg = match (arg.min_values, arg.max_values) {
|
||||
(Some(min), Some(max)) => clap_arg.num_args(min..=max),
|
||||
(Some(min), None) => clap_arg.num_args(min..),
|
||||
(None, Some(max)) => clap_arg.num_args(0..max),
|
||||
(None, None) => clap_arg,
|
||||
};
|
||||
clap_arg = clap_arg.required(arg.required);
|
||||
clap_arg = bind_string_arg!(
|
||||
arg,
|
||||
clap_arg,
|
||||
required_unless_present,
|
||||
required_unless_present
|
||||
);
|
||||
clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_all);
|
||||
clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_any);
|
||||
clap_arg = bind_string_arg!(arg, clap_arg, conflicts_with, conflicts_with);
|
||||
if let Some(value) = &arg.conflicts_with_all {
|
||||
clap_arg = clap_arg.conflicts_with_all(value);
|
||||
}
|
||||
clap_arg = bind_string_arg!(arg, clap_arg, requires, requires);
|
||||
if let Some(value) = &arg.requires_all {
|
||||
clap_arg = clap_arg.requires_all(value);
|
||||
}
|
||||
clap_arg = bind_if_arg!(arg, clap_arg, requires_if);
|
||||
clap_arg = bind_if_arg!(arg, clap_arg, required_if_eq);
|
||||
clap_arg = bind_value_arg!(arg, clap_arg, require_equals);
|
||||
clap_arg = bind_value_arg!(arg, clap_arg, index);
|
||||
|
||||
clap_arg
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
macro_rules! bind_string_arg {
|
||||
($arg:expr, $clap_arg:expr, $arg_name:ident, $clap_field:ident) => {{
|
||||
let arg = $arg;
|
||||
let mut clap_arg = $clap_arg;
|
||||
if let Some(value) = &arg.$arg_name {
|
||||
clap_arg = clap_arg.$clap_field(value);
|
||||
}
|
||||
clap_arg
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! bind_value_arg {
|
||||
($arg:expr, $clap_arg:expr, $field:ident) => {{
|
||||
let arg = $arg;
|
||||
let mut clap_arg = $clap_arg;
|
||||
if let Some(value) = arg.$field {
|
||||
clap_arg = clap_arg.$field(value);
|
||||
}
|
||||
clap_arg
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! bind_string_slice_arg {
|
||||
($arg:expr, $clap_arg:expr, $field:ident) => {{
|
||||
let arg = $arg;
|
||||
let mut clap_arg = $clap_arg;
|
||||
if let Some(value) = &arg.$field {
|
||||
clap_arg = clap_arg.$field(value);
|
||||
}
|
||||
clap_arg
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! bind_if_arg {
|
||||
($arg:expr, $clap_arg:expr, $field:ident) => {{
|
||||
let arg = $arg;
|
||||
let mut clap_arg = $clap_arg;
|
||||
if let Some((value, arg)) = &arg.$field {
|
||||
clap_arg = clap_arg.$field(value, arg);
|
||||
}
|
||||
clap_arg
|
||||
}};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["guest-js/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/.tauri
|
||||
Generated
+3584
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "tauri-plugin-clipboard"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
arboard = "3"
|
||||
@@ -0,0 +1,20 @@
|
||||
SPDXVersion: SPDX-2.1
|
||||
DataLicense: CC0-1.0
|
||||
PackageName: tauri
|
||||
DataFormat: SPDXRef-1
|
||||
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
|
||||
PackageHomePage: https://tauri.app
|
||||
PackageLicenseDeclared: Apache-2.0
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy
|
||||
PackageSummary: <text>Tauri is a rust project that enables developers to make secure
|
||||
and small desktop applications using a web frontend.
|
||||
</text>
|
||||
PackageComment: <text>The package includes the following libraries; see
|
||||
Relationship information.
|
||||
</text>
|
||||
Created: 2019-05-20T09:00:00Z
|
||||
PackageDownloadLocation: git://github.com/tauri-apps/tauri
|
||||
PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git
|
||||
PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git
|
||||
Creator: Person: Daniel Thompson-Yvetot
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,65 @@
|
||||

|
||||
|
||||
<!-- description -->
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
There are three general methods of installation that we can recommend.
|
||||
|
||||
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
|
||||
2. Pull sources directly from Github using git tags / revision hashes (most secure)
|
||||
3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use)
|
||||
|
||||
Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||
|
||||
`src-tauri/Cargo.toml`
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri-plugin-clipboard = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
```
|
||||
|
||||
You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
|
||||
|
||||
> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
|
||||
|
||||
```sh
|
||||
pnpm add https://github.com/tauri-apps/tauri-plugin-clipboard#v2
|
||||
# or
|
||||
npm add https://github.com/tauri-apps/tauri-plugin-clipboard#v2
|
||||
# or
|
||||
yarn add https://github.com/tauri-apps/tauri-plugin-clipboard#v2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
First you need to register the core plugin with Tauri:
|
||||
|
||||
`src-tauri/src/main.rs`
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_clipboard::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
```
|
||||
|
||||
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
|
||||
|
||||
```javascript
|
||||
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
|
||||
|
||||
## License
|
||||
|
||||
Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy.
|
||||
|
||||
MIT or MIT/Apache 2.0 where applicable.
|
||||
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.tauri
|
||||
@@ -0,0 +1,45 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.tauri.clipboard"
|
||||
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(project(":tauri-android"))
|
||||
}
|
||||
+21
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
include ':tauri-android'
|
||||
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.tauri.clipboard
|
||||
|
||||
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.clipboard", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
||||
@@ -0,0 +1,70 @@
|
||||
package app.tauri.clipboard
|
||||
|
||||
import android.R.attr.value
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
|
||||
|
||||
@TauriPlugin
|
||||
class ClipboardPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
private val manager: ClipboardManager =
|
||||
activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
@Command
|
||||
@Suppress("MoveVariableDeclarationIntoWhen")
|
||||
fun write(invoke: Invoke) {
|
||||
val options = invoke.getObject("options")
|
||||
if (options == null) {
|
||||
invoke.reject("Missing `options` input")
|
||||
return
|
||||
}
|
||||
val kind = invoke.getString("kind", "")
|
||||
|
||||
val clipData = when (kind) {
|
||||
"PlainText" -> {
|
||||
val label = options.getString("label", "")
|
||||
val text = options.getString("text", "")
|
||||
ClipData.newPlainText(label, text)
|
||||
}
|
||||
|
||||
else -> {
|
||||
invoke.reject("Unknown kind $kind")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
manager.setPrimaryClip(clipData)
|
||||
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@Command
|
||||
fun read(invoke: Invoke) {
|
||||
val (kind, options) = if (manager.hasPrimaryClip()) {
|
||||
if (manager.primaryClipDescription?.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) == true) {
|
||||
val item: ClipData.Item = manager.primaryClip!!.getItemAt(0)
|
||||
Pair("PlainText", item.text)
|
||||
} else {
|
||||
// TODO
|
||||
invoke.reject("Clipboard content reader not implemented")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Clipboard is empty")
|
||||
return
|
||||
}
|
||||
|
||||
val response = JSObject()
|
||||
response.put("kind", kind)
|
||||
response.put("options", options)
|
||||
invoke.resolve(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package app.tauri.clipboard
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Read and write to the system clipboard.
|
||||
*
|
||||
* The APIs must be added to [`tauri.allowlist.clipboard`](https://tauri.app/v1/api/config/#allowlistconfig.clipboard) in `tauri.conf.json`:
|
||||
* ```json
|
||||
* {
|
||||
* "tauri": {
|
||||
* "allowlist": {
|
||||
* "clipboard": {
|
||||
* "all": true, // enable all Clipboard APIs
|
||||
* "writeText": true,
|
||||
* "readText": true
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
* It is recommended to allowlist only the APIs you use for optimal bundle size and security.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
|
||||
interface Clip<K, T> {
|
||||
kind: K;
|
||||
options: T;
|
||||
}
|
||||
|
||||
type ClipResponse = Clip<"PlainText", string>;
|
||||
|
||||
/**
|
||||
* Writes plain text to the clipboard.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { writeText, readText } from 'tauri-plugin-clipboard-api';
|
||||
* await writeText('Tauri is awesome!');
|
||||
* assert(await readText(), 'Tauri is awesome!');
|
||||
* ```
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 1.0.0.
|
||||
*/
|
||||
async function writeText(
|
||||
text: string,
|
||||
opts?: { label?: string }
|
||||
): Promise<void> {
|
||||
return invoke("plugin:clipboard|write", {
|
||||
data: {
|
||||
kind: "PlainText",
|
||||
options: {
|
||||
label: opts?.label,
|
||||
text,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the clipboard content as plain text.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { readText } from 'tauri-plugin-clipboard-api';
|
||||
* const clipboardText = await readText();
|
||||
* ```
|
||||
* @since 1.0.0.
|
||||
*/
|
||||
async function readText(): Promise<string> {
|
||||
const kind: ClipResponse = await invoke("plugin:clipboard|read");
|
||||
return kind.options;
|
||||
}
|
||||
|
||||
export { writeText, readText };
|
||||
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
||||
@@ -0,0 +1,31 @@
|
||||
// swift-tools-version:5.3
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-clipboard",
|
||||
platforms: [
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-clipboard",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-clipboard"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-clipboard",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tauri Plugin {{ plugin_name_original }}
|
||||
|
||||
A description of this package.
|
||||
@@ -0,0 +1,42 @@
|
||||
import UIKit
|
||||
import WebKit
|
||||
import Tauri
|
||||
import SwiftRs
|
||||
|
||||
class ClipboardPlugin: Plugin {
|
||||
@objc public func write(_ invoke: Invoke) throws {
|
||||
let options = invoke.getObject("options")
|
||||
if let options = options {
|
||||
let clipboard = UIPasteboard.general
|
||||
let kind = invoke.getString("kind", "")
|
||||
switch kind {
|
||||
case "PlainText":
|
||||
let text = options["text"] as? String
|
||||
clipboard.string = text
|
||||
default:
|
||||
invoke.reject("Unknown kind \(kind)")
|
||||
return
|
||||
}
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Missing `options` input")
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func read(_ invoke: Invoke) throws {
|
||||
let clipboard = UIPasteboard.general
|
||||
if let text = clipboard.string {
|
||||
invoke.resolve([
|
||||
"kind": "PlainText",
|
||||
"options": text
|
||||
])
|
||||
} else {
|
||||
invoke.reject("Clipboard is empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_clipboard")
|
||||
func initPlugin(name: SRString, webview: WKWebView?) {
|
||||
Tauri.registerPlugin(webview: webview, name: name.toString(), plugin: ClipboardPlugin())
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import XCTest
|
||||
@testable import ClipboardPlugin
|
||||
|
||||
final class ClipboardPluginTests: XCTestCase {
|
||||
func testClipboard() throws {
|
||||
let plugin = ClipboardPlugin()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "tauri-plugin-clipboard-api",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"authors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"type": "module",
|
||||
"browser": "dist-js/index.min.js",
|
||||
"module": "dist-js/index.mjs",
|
||||
"types": "dist-js/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist-js/index.mjs",
|
||||
"types": "./dist-js/index.d.ts",
|
||||
"browser": "./dist-js/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist-js",
|
||||
"!dist-js/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
}
|
||||
}
|
||||
@@ -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/],
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use tauri::{command, AppHandle, Runtime, State};
|
||||
|
||||
use crate::{ClipKind, Clipboard, ClipboardContents, Result};
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn write<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
clipboard: State<'_, Clipboard<R>>,
|
||||
data: ClipKind,
|
||||
) -> Result<()> {
|
||||
clipboard.write(data)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn read<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
clipboard: State<'_, Clipboard<R>>,
|
||||
) -> Result<ClipboardContents> {
|
||||
clipboard.read()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{plugin::PluginApi, AppHandle, Runtime};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
app: &AppHandle<R>,
|
||||
_api: PluginApi<R, C>,
|
||||
) -> crate::Result<Clipboard<R>> {
|
||||
Ok(Clipboard {
|
||||
app: app.clone(),
|
||||
clipboard: arboard::Clipboard::new().map(Mutex::new),
|
||||
})
|
||||
}
|
||||
|
||||
/// Access to the clipboard APIs.
|
||||
pub struct Clipboard<R: Runtime> {
|
||||
#[allow(dead_code)]
|
||||
app: AppHandle<R>,
|
||||
clipboard: Result<Mutex<arboard::Clipboard>, arboard::Error>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Clipboard<R> {
|
||||
pub fn write(&self, kind: ClipKind) -> crate::Result<()> {
|
||||
let ClipKind::PlainText { text, .. } = kind;
|
||||
match &self.clipboard {
|
||||
Ok(clipboard) => clipboard.lock().unwrap().set_text(text).map_err(Into::into),
|
||||
Err(e) => Err(crate::Error::Clipboard(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self) -> crate::Result<ClipboardContents> {
|
||||
match &self.clipboard {
|
||||
Ok(clipboard) => {
|
||||
let text = clipboard.lock().unwrap().get_text()?;
|
||||
Ok(ClipboardContents::PlainText(text))
|
||||
}
|
||||
Err(e) => Err(crate::Error::Clipboard(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[cfg(mobile)]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
#[cfg(desktop)]
|
||||
#[error("{0}")]
|
||||
Clipboard(String),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
impl From<arboard::Error> for Error {
|
||||
fn from(error: arboard::Error) -> Self {
|
||||
Self::Clipboard(error.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime,
|
||||
};
|
||||
|
||||
pub use models::*;
|
||||
|
||||
#[cfg(desktop)]
|
||||
mod desktop;
|
||||
#[cfg(mobile)]
|
||||
mod mobile;
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
mod models;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
#[cfg(desktop)]
|
||||
use desktop::Clipboard;
|
||||
#[cfg(mobile)]
|
||||
use mobile::Clipboard;
|
||||
|
||||
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the clipboard APIs.
|
||||
pub trait ClipboardExt<R: Runtime> {
|
||||
fn clipboard(&self) -> &Clipboard<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> crate::ClipboardExt<R> for T {
|
||||
fn clipboard(&self) -> &Clipboard<R> {
|
||||
self.state::<Clipboard<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("clipboard")
|
||||
.invoke_handler(tauri::generate_handler![commands::write, commands::read])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let clipboard = mobile::init(app, api)?;
|
||||
#[cfg(desktop)]
|
||||
let clipboard = desktop::init(app, api)?;
|
||||
app.manage(clipboard);
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{
|
||||
plugin::{PluginApi, PluginHandle},
|
||||
AppHandle, Runtime,
|
||||
};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
const PLUGIN_IDENTIFIER: &str = "app.tauri.clipboard";
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
tauri::ios_plugin_binding!(init_plugin_clipboard);
|
||||
|
||||
// initializes the Kotlin or Swift plugin classes
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
_app: &AppHandle<R>,
|
||||
api: PluginApi<R, C>,
|
||||
) -> crate::Result<Clipboard<R>> {
|
||||
#[cfg(target_os = "android")]
|
||||
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ClipboardPlugin")?;
|
||||
#[cfg(target_os = "ios")]
|
||||
let handle = api.register_ios_plugin(init_plugin_clipboard)?;
|
||||
Ok(Clipboard(handle))
|
||||
}
|
||||
|
||||
/// Access to the clipboard APIs.
|
||||
pub struct Clipboard<R: Runtime>(PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> Clipboard<R> {
|
||||
pub fn write(&self, kind: ClipKind) -> crate::Result<()> {
|
||||
self.0.run_mobile_plugin("write", kind).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn read(&self) -> crate::Result<ClipboardContents> {
|
||||
self.0.run_mobile_plugin("read", ()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", content = "options")]
|
||||
pub enum ClipKind {
|
||||
PlainText { label: Option<String>, text: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", content = "options")]
|
||||
pub enum ClipboardContents {
|
||||
PlainText(String),
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["guest-js/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.tauri
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
glib = "0.16"
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
rfd = { version = "0.11", features = [ "gtk3", "common-controls-v6" ] }
|
||||
raw-window-handle = "0.5"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build.workspace = true
|
||||
@@ -0,0 +1,20 @@
|
||||
SPDXVersion: SPDX-2.1
|
||||
DataLicense: CC0-1.0
|
||||
PackageName: tauri
|
||||
DataFormat: SPDXRef-1
|
||||
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
|
||||
PackageHomePage: https://tauri.app
|
||||
PackageLicenseDeclared: Apache-2.0
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy
|
||||
PackageSummary: <text>Tauri is a rust project that enables developers to make secure
|
||||
and small desktop applications using a web frontend.
|
||||
</text>
|
||||
PackageComment: <text>The package includes the following libraries; see
|
||||
Relationship information.
|
||||
</text>
|
||||
Created: 2019-05-20T09:00:00Z
|
||||
PackageDownloadLocation: git://github.com/tauri-apps/tauri
|
||||
PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git
|
||||
PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git
|
||||
Creator: Person: Daniel Thompson-Yvetot
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,65 @@
|
||||

|
||||
|
||||
<!-- description -->
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
There are three general methods of installation that we can recommend.
|
||||
|
||||
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
|
||||
2. Pull sources directly from Github using git tags / revision hashes (most secure)
|
||||
3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use)
|
||||
|
||||
Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||
|
||||
`src-tauri/Cargo.toml`
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri-plugin-dialog-api = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
```
|
||||
|
||||
You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
|
||||
|
||||
> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
|
||||
|
||||
```sh
|
||||
pnpm add tauri-plugin-dialog-api
|
||||
# or
|
||||
npm add tauri-plugin-dialog-api
|
||||
# or
|
||||
yarn add tauri-plugin-dialog-api
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
First you need to register the core plugin with Tauri:
|
||||
|
||||
`src-tauri/src/main.rs`
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
```
|
||||
|
||||
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
|
||||
|
||||
```javascript
|
||||
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
|
||||
|
||||
## License
|
||||
|
||||
Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy.
|
||||
|
||||
MIT or MIT/Apache 2.0 where applicable.
|
||||
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.tauri
|
||||
@@ -0,0 +1,45 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.tauri.dialog"
|
||||
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(project(":tauri-android"))
|
||||
}
|
||||
+21
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
include ':tauri-android'
|
||||
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.tauri.dialog
|
||||
|
||||
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.dialog", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
||||
@@ -0,0 +1,205 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.activity.result.ActivityResult
|
||||
import app.tauri.Logger
|
||||
import app.tauri.annotation.ActivityCallback
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSArray
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import org.json.JSONException
|
||||
|
||||
|
||||
@TauriPlugin
|
||||
class DialogPlugin(private val activity: Activity): Plugin(activity) {
|
||||
@Command
|
||||
fun showFilePicker(invoke: Invoke) {
|
||||
try {
|
||||
val filters = invoke.getArray("filters", JSArray())
|
||||
val multiple = invoke.getBoolean("multiple", false)
|
||||
val parsedTypes = parseFiltersOption(filters)
|
||||
|
||||
val intent = if (parsedTypes != null && parsedTypes.isNotEmpty()) {
|
||||
val intent = Intent(Intent.ACTION_PICK)
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, parsedTypes)
|
||||
|
||||
var uniqueMimeType = true
|
||||
var mimeKind: String? = null
|
||||
for (mime in parsedTypes) {
|
||||
val kind = mime.split("/")[0]
|
||||
if (mimeKind == null) {
|
||||
mimeKind = kind
|
||||
} else if (mimeKind != kind) {
|
||||
uniqueMimeType = false
|
||||
}
|
||||
}
|
||||
|
||||
intent.type = if (uniqueMimeType) Intent.normalizeMimeType("$mimeKind/*") else "*/*"
|
||||
intent
|
||||
} else {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "*/*"
|
||||
intent
|
||||
}
|
||||
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
|
||||
|
||||
startActivityForResult(invoke, intent, "filePickerResult")
|
||||
} catch (ex: Exception) {
|
||||
val message = ex.message ?: "Failed to pick file"
|
||||
Logger.error(message)
|
||||
invoke.reject(message)
|
||||
}
|
||||
}
|
||||
|
||||
@ActivityCallback
|
||||
fun filePickerResult(invoke: Invoke, result: ActivityResult) {
|
||||
try {
|
||||
val readData = invoke.getBoolean("readData", false)
|
||||
when (result.resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
val callResult = createPickFilesResult(result.data, readData)
|
||||
invoke.resolve(callResult)
|
||||
}
|
||||
Activity.RESULT_CANCELED -> invoke.reject("File picker cancelled")
|
||||
else -> invoke.reject("Failed to pick files")
|
||||
}
|
||||
} catch (ex: java.lang.Exception) {
|
||||
val message = ex.message ?: "Failed to read file pick result"
|
||||
Logger.error(message)
|
||||
invoke.reject(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPickFilesResult(data: Intent?, readData: Boolean): JSObject {
|
||||
val callResult = JSObject()
|
||||
val filesResultList: MutableList<JSObject> = ArrayList()
|
||||
if (data == null) {
|
||||
callResult.put("files", JSArray.from(filesResultList))
|
||||
return callResult
|
||||
}
|
||||
val uris: MutableList<Uri?> = ArrayList()
|
||||
if (data.clipData == null) {
|
||||
val uri: Uri? = data.data
|
||||
uris.add(uri)
|
||||
} else {
|
||||
for (i in 0 until data.clipData!!.itemCount) {
|
||||
val uri: Uri = data.clipData!!.getItemAt(i).uri
|
||||
uris.add(uri)
|
||||
}
|
||||
}
|
||||
for (i in uris.indices) {
|
||||
val uri = uris[i] ?: continue
|
||||
val fileResult = JSObject()
|
||||
if (readData) {
|
||||
fileResult.put("base64Data", FilePickerUtils.getDataFromUri(activity, uri))
|
||||
}
|
||||
val duration = FilePickerUtils.getDurationFromUri(activity, uri)
|
||||
if (duration != null) {
|
||||
fileResult.put("duration", duration)
|
||||
}
|
||||
val resolution = FilePickerUtils.getHeightAndWidthFromUri(activity, uri)
|
||||
if (resolution != null) {
|
||||
fileResult.put("height", resolution.height)
|
||||
fileResult.put("width", resolution.width)
|
||||
}
|
||||
fileResult.put("mimeType", FilePickerUtils.getMimeTypeFromUri(activity, uri))
|
||||
val modifiedAt = FilePickerUtils.getModifiedAtFromUri(activity, uri)
|
||||
if (modifiedAt != null) {
|
||||
fileResult.put("modifiedAt", modifiedAt)
|
||||
}
|
||||
fileResult.put("name", FilePickerUtils.getNameFromUri(activity, uri))
|
||||
fileResult.put("path", FilePickerUtils.getPathFromUri(uri))
|
||||
fileResult.put("size", FilePickerUtils.getSizeFromUri(activity, uri))
|
||||
filesResultList.add(fileResult)
|
||||
}
|
||||
callResult.put("files", JSArray.from(filesResultList.toTypedArray()))
|
||||
return callResult
|
||||
}
|
||||
|
||||
private fun parseFiltersOption(filters: JSArray): Array<String>? {
|
||||
return try {
|
||||
val filtersList: List<JSObject> = filters.toList()
|
||||
val mimeTypes = mutableListOf<String>()
|
||||
for (filter in filtersList) {
|
||||
val extensionsList = filter.getJSONArray("extensions")
|
||||
for (i in 0 until extensionsList.length()) {
|
||||
val mime = extensionsList.getString(i)
|
||||
mimeTypes.add(if (mime == "text/csv") "text/comma-separated-values" else mime)
|
||||
}
|
||||
}
|
||||
|
||||
mimeTypes.toTypedArray()
|
||||
} catch (exception: JSONException) {
|
||||
Logger.error("parseTypesOption failed.", exception)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun showMessageDialog(invoke: Invoke) {
|
||||
val title = invoke.getString("title")
|
||||
val message = invoke.getString("message")
|
||||
val okButtonLabel = invoke.getString("okButtonLabel", "OK")
|
||||
val cancelButtonLabel = invoke.getString("cancelButtonLabel", "Cancel")
|
||||
|
||||
if (message == null) {
|
||||
invoke.reject("The `message` argument is required")
|
||||
return
|
||||
}
|
||||
|
||||
if (activity.isFinishing) {
|
||||
invoke.reject("App is finishing")
|
||||
return
|
||||
}
|
||||
|
||||
val handler = { cancelled: Boolean, value: Boolean ->
|
||||
val ret = JSObject()
|
||||
ret.put("cancelled", cancelled)
|
||||
ret.put("value", value)
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper())
|
||||
.post {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
if (title != null) {
|
||||
builder.setTitle(title)
|
||||
}
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
okButtonLabel
|
||||
) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
handler(false, true)
|
||||
}
|
||||
.setNegativeButton(
|
||||
cancelButtonLabel
|
||||
) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
handler(false, false)
|
||||
}
|
||||
.setOnCancelListener { dialog ->
|
||||
dialog.dismiss()
|
||||
handler(true, false)
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Base64
|
||||
import app.tauri.Logger
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
class FilePickerUtils {
|
||||
class FileResolution(var height: Int, var width: Int)
|
||||
|
||||
companion object {
|
||||
fun getPathFromUri(uri: Uri): String {
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
fun getNameFromUri(context: Context, uri: Uri): String? {
|
||||
var displayName: String? = ""
|
||||
val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||
val cursor =
|
||||
context.contentResolver.query(uri, projection, null, null, null)
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst()
|
||||
val columnIdx = cursor.getColumnIndex(projection[0])
|
||||
displayName = cursor.getString(columnIdx)
|
||||
cursor.close()
|
||||
}
|
||||
if (displayName == null || displayName.isEmpty()) {
|
||||
displayName = uri.lastPathSegment
|
||||
}
|
||||
return displayName
|
||||
}
|
||||
|
||||
fun getDataFromUri(context: Context, uri: Uri): String {
|
||||
try {
|
||||
val stream = context.contentResolver.openInputStream(uri) ?: return ""
|
||||
val bytes = getBytesFromInputStream(stream)
|
||||
return Base64.encodeToString(bytes, Base64.NO_WRAP)
|
||||
} catch (e: FileNotFoundException) {
|
||||
Logger.error("openInputStream failed.", e)
|
||||
} catch (e: IOException) {
|
||||
Logger.error("getBytesFromInputStream failed.", e)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fun getMimeTypeFromUri(context: Context, uri: Uri): String? {
|
||||
return context.contentResolver.getType(uri)
|
||||
}
|
||||
|
||||
fun getModifiedAtFromUri(context: Context, uri: Uri): Long? {
|
||||
return try {
|
||||
var modifiedAt: Long = 0
|
||||
val cursor =
|
||||
context.contentResolver.query(uri, null, null, null, null)
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst()
|
||||
val columnIdx =
|
||||
cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
modifiedAt = cursor.getLong(columnIdx)
|
||||
cursor.close()
|
||||
}
|
||||
modifiedAt
|
||||
} catch (e: Exception) {
|
||||
Logger.error("getModifiedAtFromUri failed.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getSizeFromUri(context: Context, uri: Uri): Long {
|
||||
var size: Long = 0
|
||||
val projection = arrayOf(OpenableColumns.SIZE)
|
||||
val cursor =
|
||||
context.contentResolver.query(uri, projection, null, null, null)
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst()
|
||||
val columnIdx = cursor.getColumnIndex(projection[0])
|
||||
size = cursor.getLong(columnIdx)
|
||||
cursor.close()
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
fun getDurationFromUri(context: Context, uri: Uri): Long? {
|
||||
if (isVideoUri(context, uri)) {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(context, uri)
|
||||
val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||
val durationMs = time?.toLong() ?: 0
|
||||
try {
|
||||
retriever.release()
|
||||
} catch (e: Exception) {
|
||||
Logger.error("MediaMetadataRetriever.release() failed.", e)
|
||||
}
|
||||
return durationMs / 1000L
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getHeightAndWidthFromUri(context: Context, uri: Uri): FileResolution? {
|
||||
if (isImageUri(context, uri)) {
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
return try {
|
||||
BitmapFactory.decodeStream(
|
||||
context.contentResolver.openInputStream(uri),
|
||||
null,
|
||||
options
|
||||
)
|
||||
FileResolution(options.outHeight, options.outWidth)
|
||||
} catch (exception: FileNotFoundException) {
|
||||
exception.printStackTrace()
|
||||
null
|
||||
}
|
||||
} else if (isVideoUri(context, uri)) {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(context, uri)
|
||||
val width =
|
||||
Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) ?: "0")
|
||||
val height =
|
||||
Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) ?: "0")
|
||||
try {
|
||||
retriever.release()
|
||||
} catch (e: Exception) {
|
||||
Logger.error("MediaMetadataRetriever.release() failed.", e)
|
||||
}
|
||||
return FileResolution(height, width)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isImageUri(context: Context, uri: Uri): Boolean {
|
||||
val mimeType = getMimeTypeFromUri(context, uri) ?: return false
|
||||
return mimeType.startsWith("image")
|
||||
}
|
||||
|
||||
private fun isVideoUri(context: Context, uri: Uri): Boolean {
|
||||
val mimeType = getMimeTypeFromUri(context, uri) ?: return false
|
||||
return mimeType.startsWith("video")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun getBytesFromInputStream(`is`: InputStream): ByteArray {
|
||||
val os = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(0xFFFF)
|
||||
var len = `is`.read(buffer)
|
||||
while (len != -1) {
|
||||
os.write(buffer, 0, len)
|
||||
len = `is`.read(buffer)
|
||||
}
|
||||
return os.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package app.tauri.dialog
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
|
||||
interface FileResponse {
|
||||
base64Data?: string;
|
||||
duration?: number;
|
||||
height?: number;
|
||||
width?: number;
|
||||
mimeType?: string;
|
||||
modifiedAt?: number;
|
||||
name?: string;
|
||||
path: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension filters for the file dialog.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface DialogFilter {
|
||||
/** Filter name. */
|
||||
name: string;
|
||||
/**
|
||||
* Extensions to filter, without a `.` prefix.
|
||||
* @example
|
||||
* ```typescript
|
||||
* extensions: ['svg', 'png']
|
||||
* ```
|
||||
*/
|
||||
extensions: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the open dialog.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface OpenDialogOptions {
|
||||
/** The title of the dialog window. */
|
||||
title?: string;
|
||||
/** The filters of the dialog. */
|
||||
filters?: DialogFilter[];
|
||||
/** Initial directory or file path. */
|
||||
defaultPath?: string;
|
||||
/** Whether the dialog allows multiple selection or not. */
|
||||
multiple?: boolean;
|
||||
/** Whether the dialog is a directory selection or not. */
|
||||
directory?: boolean;
|
||||
/**
|
||||
* If `directory` is true, indicates that it will be read recursively later.
|
||||
* Defines whether subdirectories will be allowed on the scope or not.
|
||||
*/
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the save dialog.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface SaveDialogOptions {
|
||||
/** The title of the dialog window. */
|
||||
title?: string;
|
||||
/** The filters of the dialog. */
|
||||
filters?: DialogFilter[];
|
||||
/**
|
||||
* Initial directory or file path.
|
||||
* If it's a directory path, the dialog interface will change to that folder.
|
||||
* If it's not an existing directory, the file name will be set to the dialog's file name input and the dialog will be set to the parent folder.
|
||||
*/
|
||||
defaultPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface MessageDialogOptions {
|
||||
/** The title of the dialog. Defaults to the app name. */
|
||||
title?: string;
|
||||
/** The type of the dialog. Defaults to `info`. */
|
||||
type?: "info" | "warning" | "error";
|
||||
/** The label of the confirm button. */
|
||||
okLabel?: string;
|
||||
}
|
||||
|
||||
interface ConfirmDialogOptions {
|
||||
/** The title of the dialog. Defaults to the app name. */
|
||||
title?: string;
|
||||
/** The type of the dialog. Defaults to `info`. */
|
||||
type?: "info" | "warning" | "error";
|
||||
/** The label of the confirm button. */
|
||||
okLabel?: string;
|
||||
/** The label of the cancel button. */
|
||||
cancelLabel?: string;
|
||||
}
|
||||
|
||||
async function open(
|
||||
options?: OpenDialogOptions & { multiple?: false; directory?: false }
|
||||
): Promise<null | FileResponse>;
|
||||
async function open(
|
||||
options?: OpenDialogOptions & { multiple?: true; directory?: false }
|
||||
): Promise<null | FileResponse[]>;
|
||||
async function open(
|
||||
options?: OpenDialogOptions & { multiple?: false; directory?: true }
|
||||
): Promise<null | string>;
|
||||
async function open(
|
||||
options?: OpenDialogOptions & { multiple?: true; directory?: true }
|
||||
): Promise<null | string[]>;
|
||||
/**
|
||||
* Open a file/directory selection dialog.
|
||||
*
|
||||
* The selected paths are added to the filesystem and asset protocol allowlist scopes.
|
||||
* When security is more important than the easy of use of this API,
|
||||
* prefer writing a dedicated command instead.
|
||||
*
|
||||
* Note that the allowlist scope change is not persisted, so the values are cleared when the application is restarted.
|
||||
* You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope).
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { open } from '@tauri-apps/api/dialog';
|
||||
* // Open a selection dialog for image files
|
||||
* const selected = await open({
|
||||
* multiple: true,
|
||||
* filters: [{
|
||||
* name: 'Image',
|
||||
* extensions: ['png', 'jpeg']
|
||||
* }]
|
||||
* });
|
||||
* if (Array.isArray(selected)) {
|
||||
* // user selected multiple files
|
||||
* } else if (selected === null) {
|
||||
* // user cancelled the selection
|
||||
* } else {
|
||||
* // user selected a single file
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { open } from '@tauri-apps/api/dialog';
|
||||
* import { appDir } from '@tauri-apps/api/path';
|
||||
* // Open a selection dialog for directories
|
||||
* const selected = await open({
|
||||
* directory: true,
|
||||
* multiple: true,
|
||||
* defaultPath: await appDir(),
|
||||
* });
|
||||
* if (Array.isArray(selected)) {
|
||||
* // user selected multiple directories
|
||||
* } else if (selected === null) {
|
||||
* // user cancelled the selection
|
||||
* } else {
|
||||
* // user selected a single directory
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the selected path(s)
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function open(
|
||||
options: OpenDialogOptions = {}
|
||||
): Promise<null | string | string[] | FileResponse | FileResponse[]> {
|
||||
if (typeof options === "object") {
|
||||
Object.freeze(options);
|
||||
}
|
||||
|
||||
return invoke("plugin:dialog|open", { options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a file/directory save dialog.
|
||||
*
|
||||
* The selected path is added to the filesystem and asset protocol allowlist scopes.
|
||||
* When security is more important than the easy of use of this API,
|
||||
* prefer writing a dedicated command instead.
|
||||
*
|
||||
* Note that the allowlist scope change is not persisted, so the values are cleared when the application is restarted.
|
||||
* You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope).
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { save } from '@tauri-apps/api/dialog';
|
||||
* const filePath = await save({
|
||||
* filters: [{
|
||||
* name: 'Image',
|
||||
* extensions: ['png', 'jpeg']
|
||||
* }]
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @returns A promise resolving to the selected path.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function save(options: SaveDialogOptions = {}): Promise<string | null> {
|
||||
if (typeof options === "object") {
|
||||
Object.freeze(options);
|
||||
}
|
||||
|
||||
return invoke("plugin:dialog|save", { options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message dialog with an `Ok` button.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { message } from '@tauri-apps/api/dialog';
|
||||
* await message('Tauri is awesome', 'Tauri');
|
||||
* await message('File not found', { title: 'Tauri', type: 'error' });
|
||||
* ```
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param options The dialog's options. If a string, it represents the dialog title.
|
||||
*
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
*/
|
||||
async function message(
|
||||
message: string,
|
||||
options?: string | MessageDialogOptions
|
||||
): Promise<void> {
|
||||
const opts = typeof options === "string" ? { title: options } : options;
|
||||
return invoke("plugin:dialog|message", {
|
||||
message: message.toString(),
|
||||
title: opts?.title?.toString(),
|
||||
type_: opts?.type,
|
||||
okButtonLabel: opts?.okLabel?.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a question dialog with `Yes` and `No` buttons.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { ask } from '@tauri-apps/api/dialog';
|
||||
* const yes = await ask('Are you sure?', 'Tauri');
|
||||
* const yes2 = await ask('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' });
|
||||
* ```
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param options The dialog's options. If a string, it represents the dialog title.
|
||||
*
|
||||
* @returns A promise resolving to a boolean indicating whether `Yes` was clicked or not.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function ask(
|
||||
message: string,
|
||||
options?: string | ConfirmDialogOptions
|
||||
): Promise<boolean> {
|
||||
const opts = typeof options === "string" ? { title: options } : options;
|
||||
return invoke("plugin:dialog|ask", {
|
||||
message: message.toString(),
|
||||
title: opts?.title?.toString(),
|
||||
type_: opts?.type,
|
||||
okButtonLabel: opts?.okLabel?.toString() ?? "Yes",
|
||||
cancelButtonLabel: opts?.cancelLabel?.toString() ?? "No",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a question dialog with `Ok` and `Cancel` buttons.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { confirm } from '@tauri-apps/api/dialog';
|
||||
* const confirmed = await confirm('Are you sure?', 'Tauri');
|
||||
* const confirmed2 = await confirm('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' });
|
||||
* ```
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param options The dialog's options. If a string, it represents the dialog title.
|
||||
*
|
||||
* @returns A promise resolving to a boolean indicating whether `Ok` was clicked or not.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function confirm(
|
||||
message: string,
|
||||
options?: string | ConfirmDialogOptions
|
||||
): Promise<boolean> {
|
||||
const opts = typeof options === "string" ? { title: options } : options;
|
||||
return invoke("plugin:dialog|confirm", {
|
||||
message: message.toString(),
|
||||
title: opts?.title?.toString(),
|
||||
type_: opts?.type,
|
||||
okButtonLabel: opts?.okLabel?.toString() ?? "Ok",
|
||||
cancelButtonLabel: opts?.cancelLabel?.toString() ?? "Cancel",
|
||||
});
|
||||
}
|
||||
|
||||
export type {
|
||||
DialogFilter,
|
||||
OpenDialogOptions,
|
||||
SaveDialogOptions,
|
||||
MessageDialogOptions,
|
||||
ConfirmDialogOptions,
|
||||
};
|
||||
|
||||
export { open, save, message, ask, confirm };
|
||||
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
||||
@@ -0,0 +1,31 @@
|
||||
// swift-tools-version:5.3
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-dialog",
|
||||
platforms: [
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-dialog",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-dialog"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-dialog",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tauri Plugin Dialog
|
||||
|
||||
A description of this package.
|
||||
@@ -0,0 +1,207 @@
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import PhotosUI
|
||||
import Photos
|
||||
import WebKit
|
||||
import Tauri
|
||||
import SwiftRs
|
||||
|
||||
enum FilePickerEvent {
|
||||
case selected([URL])
|
||||
case cancelled
|
||||
case error(String)
|
||||
}
|
||||
|
||||
class DialogPlugin: Plugin {
|
||||
|
||||
var filePickerController: FilePickerController!
|
||||
var pendingInvoke: Invoke? = nil
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
filePickerController = FilePickerController(self)
|
||||
}
|
||||
|
||||
@objc public func showFilePicker(_ invoke: Invoke) {
|
||||
let multiple = invoke.getBool("multiple", false)
|
||||
let filters = invoke.getArray("filters") ?? []
|
||||
let parsedTypes = parseFiltersOption(filters)
|
||||
|
||||
var isMedia = true
|
||||
var uniqueMimeType: Bool? = nil
|
||||
var mimeKind: String? = nil
|
||||
if !parsedTypes.isEmpty {
|
||||
uniqueMimeType = true
|
||||
for mime in parsedTypes {
|
||||
let kind = mime.components(separatedBy: "/")[0]
|
||||
if kind != "image" && kind != "video" {
|
||||
isMedia = false
|
||||
}
|
||||
if (mimeKind == nil) {
|
||||
mimeKind = kind
|
||||
} else if (mimeKind != kind) {
|
||||
uniqueMimeType = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pendingInvoke = invoke
|
||||
|
||||
if uniqueMimeType == true || isMedia {
|
||||
DispatchQueue.main.async {
|
||||
if #available(iOS 14, *) {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||
configuration.selectionLimit = multiple ? 0 : 1
|
||||
|
||||
if uniqueMimeType == true {
|
||||
if mimeKind == "image" {
|
||||
configuration.filter = .images
|
||||
} else if mimeKind == "video" {
|
||||
configuration.filter = .videos
|
||||
}
|
||||
}
|
||||
|
||||
let picker = PHPickerViewController(configuration: configuration)
|
||||
picker.delegate = self.filePickerController
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
} else {
|
||||
let picker = UIImagePickerController()
|
||||
picker.delegate = self.filePickerController
|
||||
|
||||
if uniqueMimeType == true && mimeKind == "image" {
|
||||
picker.sourceType = .photoLibrary
|
||||
}
|
||||
|
||||
picker.sourceType = .photoLibrary
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes
|
||||
DispatchQueue.main.async {
|
||||
let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
|
||||
picker.delegate = self.filePickerController
|
||||
picker.allowsMultipleSelection = multiple
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentViewController(_ viewControllerToPresent: UIViewController) {
|
||||
self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func parseFiltersOption(_ filters: JSArray) -> [String] {
|
||||
var parsedTypes: [String] = []
|
||||
for (_, filter) in filters.enumerated() {
|
||||
let filterObj = filter as? JSObject
|
||||
if let filterObj = filterObj {
|
||||
let extensions = filterObj["extensions"] as? JSArray
|
||||
if let extensions = extensions {
|
||||
for e in extensions {
|
||||
let ext = e as? String ?? ""
|
||||
guard let utType: String = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, ext as CFString, nil)?.takeRetainedValue() as String? else {
|
||||
continue
|
||||
}
|
||||
parsedTypes.append(utType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsedTypes
|
||||
}
|
||||
|
||||
public func onFilePickerEvent(_ event: FilePickerEvent) {
|
||||
switch event {
|
||||
case .selected(let urls):
|
||||
let readData = pendingInvoke?.getBool("readData", false) ?? false
|
||||
do {
|
||||
let filesResult = try urls.map {(url: URL) -> JSObject in
|
||||
var file = JSObject()
|
||||
|
||||
let mimeType = filePickerController.getMimeTypeFromUrl(url)
|
||||
let isVideo = mimeType.hasPrefix("video")
|
||||
let isImage = mimeType.hasPrefix("image")
|
||||
|
||||
if readData {
|
||||
file["data"] = try Data(contentsOf: url).base64EncodedString()
|
||||
}
|
||||
|
||||
if isVideo {
|
||||
file["duration"] = filePickerController.getVideoDuration(url)
|
||||
let (height, width) = filePickerController.getVideoDimensions(url)
|
||||
if let height = height {
|
||||
file["height"] = height
|
||||
}
|
||||
if let width = width {
|
||||
file["width"] = width
|
||||
}
|
||||
} else if isImage {
|
||||
let (height, width) = filePickerController.getImageDimensions(url)
|
||||
if let height = height {
|
||||
file["height"] = height
|
||||
}
|
||||
if let width = width {
|
||||
file["width"] = width
|
||||
}
|
||||
}
|
||||
|
||||
file["modifiedAt"] = filePickerController.getModifiedAtFromUrl(url)
|
||||
file["mimeType"] = mimeType
|
||||
file["name"] = url.lastPathComponent
|
||||
file["path"] = url.absoluteString
|
||||
file["size"] = try filePickerController.getSizeFromUrl(url)
|
||||
return file
|
||||
}
|
||||
pendingInvoke?.resolve(["files": filesResult])
|
||||
} catch let error as NSError {
|
||||
pendingInvoke?.reject(error.localizedDescription, nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
pendingInvoke?.resolve(["files": urls])
|
||||
case .cancelled:
|
||||
let files: JSArray = []
|
||||
pendingInvoke?.resolve(["files": files])
|
||||
case .error(let error):
|
||||
pendingInvoke?.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func showMessageDialog(_ invoke: Invoke) {
|
||||
let manager = self.manager
|
||||
let title = invoke.getString("title")
|
||||
guard let message = invoke.getString("message") else {
|
||||
invoke.reject("The `message` argument is required")
|
||||
return
|
||||
}
|
||||
let okButtonLabel = invoke.getString("okButtonLabel") ?? "OK"
|
||||
let cancelButtonLabel = invoke.getString("cancelButtonLabel") ?? "Cancel"
|
||||
|
||||
DispatchQueue.main.async { [] in
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
|
||||
alert.addAction(UIAlertAction(title: cancelButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in
|
||||
invoke.resolve([
|
||||
"value": false,
|
||||
"cancelled": false
|
||||
])
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: okButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in
|
||||
invoke.resolve([
|
||||
"value": true,
|
||||
"cancelled": false
|
||||
])
|
||||
}))
|
||||
|
||||
manager.viewController?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_dialog")
|
||||
func initPlugin(name: SRString, webview: WKWebView?) {
|
||||
Tauri.registerPlugin(webview: webview, name: name.toString(), plugin: DialogPlugin())
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import PhotosUI
|
||||
import Photos
|
||||
import Tauri
|
||||
|
||||
public class FilePickerController: NSObject {
|
||||
var plugin: DialogPlugin
|
||||
|
||||
init(_ dialogPlugin: DialogPlugin) {
|
||||
plugin = dialogPlugin
|
||||
}
|
||||
|
||||
private func dismissViewController(_ viewControllerToPresent: UIViewController, completion: (() -> Void)? = nil) {
|
||||
viewControllerToPresent.dismiss(animated: true, completion: completion)
|
||||
}
|
||||
|
||||
public func getModifiedAtFromUrl(_ url: URL) -> Int? {
|
||||
do {
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
||||
if let modifiedDateInSec = (attributes[.modificationDate] as? Date)?.timeIntervalSince1970 {
|
||||
return Int(modifiedDateInSec * 1000.0)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} catch let error as NSError {
|
||||
Logger.error("getModifiedAtFromUrl failed", error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func getMimeTypeFromUrl(_ url: URL) -> String {
|
||||
let fileExtension = url.pathExtension as CFString
|
||||
guard let extUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, nil)?.takeUnretainedValue() else {
|
||||
return ""
|
||||
}
|
||||
guard let mimeUTI = UTTypeCopyPreferredTagWithClass(extUTI, kUTTagClassMIMEType) else {
|
||||
return ""
|
||||
}
|
||||
return mimeUTI.takeRetainedValue() as String
|
||||
}
|
||||
|
||||
public func getSizeFromUrl(_ url: URL) throws -> Int {
|
||||
let values = try url.resourceValues(forKeys: [.fileSizeKey])
|
||||
return values.fileSize ?? 0
|
||||
}
|
||||
|
||||
public func getVideoDuration(_ url: URL) -> Int {
|
||||
let asset = AVAsset(url: url)
|
||||
let duration = asset.duration
|
||||
let durationTime = CMTimeGetSeconds(duration)
|
||||
return Int(round(durationTime))
|
||||
}
|
||||
|
||||
public func getImageDimensions(_ url: URL) -> (Int?, Int?) {
|
||||
if let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) {
|
||||
if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? {
|
||||
return getHeightAndWidthFromImageProperties(imageProperties)
|
||||
}
|
||||
}
|
||||
return (nil, nil)
|
||||
}
|
||||
|
||||
public func getVideoDimensions(_ url: URL) -> (Int?, Int?) {
|
||||
guard let track = AVURLAsset(url: url).tracks(withMediaType: AVMediaType.video).first else { return (nil, nil) }
|
||||
let size = track.naturalSize.applying(track.preferredTransform)
|
||||
let height = abs(Int(size.height))
|
||||
let width = abs(Int(size.width))
|
||||
return (height, width)
|
||||
}
|
||||
|
||||
private func getHeightAndWidthFromImageProperties(_ properties: [NSObject: AnyObject]) -> (Int?, Int?) {
|
||||
let width = properties[kCGImagePropertyPixelWidth] as? Int
|
||||
let height = properties[kCGImagePropertyPixelHeight] as? Int
|
||||
let orientation = properties[kCGImagePropertyOrientation] as? Int ?? UIImage.Orientation.up.rawValue
|
||||
switch orientation {
|
||||
case UIImage.Orientation.left.rawValue, UIImage.Orientation.right.rawValue, UIImage.Orientation.leftMirrored.rawValue, UIImage.Orientation.rightMirrored.rawValue:
|
||||
return (width, height)
|
||||
default:
|
||||
return (height, width)
|
||||
}
|
||||
}
|
||||
|
||||
private func getFileUrlByPath(_ path: String) -> URL? {
|
||||
guard let url = URL.init(string: path) else {
|
||||
return nil
|
||||
}
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
return url
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL {
|
||||
var directory = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
|
||||
directory = cachesDirectory
|
||||
}
|
||||
let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent)
|
||||
do {
|
||||
try deleteFile(targetUrl)
|
||||
}
|
||||
try FileManager.default.copyItem(at: sourceUrl, to: targetUrl)
|
||||
return targetUrl
|
||||
}
|
||||
|
||||
private func deleteFile(_ url: URL) throws {
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
try FileManager.default.removeItem(atPath: url.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FilePickerController: UIDocumentPickerDelegate {
|
||||
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
do {
|
||||
let temporaryUrls = try urls.map { try saveTemporaryFile($0) }
|
||||
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
|
||||
} catch {
|
||||
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
|
||||
}
|
||||
}
|
||||
|
||||
public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
}
|
||||
}
|
||||
|
||||
extension FilePickerController: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {
|
||||
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
dismissViewController(picker)
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
}
|
||||
|
||||
public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
}
|
||||
|
||||
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
}
|
||||
|
||||
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
dismissViewController(picker) {
|
||||
if let url = info[.mediaURL] as? URL {
|
||||
do {
|
||||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||||
self.plugin.onFilePickerEvent(.selected([temporaryUrl]))
|
||||
} catch {
|
||||
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
|
||||
}
|
||||
} else {
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
extension FilePickerController: PHPickerViewControllerDelegate {
|
||||
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
dismissViewController(picker)
|
||||
if results.first == nil {
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
return
|
||||
}
|
||||
var temporaryUrls: [URL] = []
|
||||
var errorMessage: String?
|
||||
let dispatchGroup = DispatchGroup()
|
||||
for result in results {
|
||||
if errorMessage != nil {
|
||||
break
|
||||
}
|
||||
if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||||
dispatchGroup.enter()
|
||||
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier, completionHandler: { url, error in
|
||||
defer {
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
if let error = error {
|
||||
errorMessage = error.localizedDescription
|
||||
return
|
||||
}
|
||||
guard let url = url else {
|
||||
errorMessage = "Unknown error"
|
||||
return
|
||||
}
|
||||
do {
|
||||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||||
temporaryUrls.append(temporaryUrl)
|
||||
} catch {
|
||||
errorMessage = "Failed to create a temporary copy of the file"
|
||||
}
|
||||
})
|
||||
} else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||||
dispatchGroup.enter()
|
||||
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier, completionHandler: { url, error in
|
||||
defer {
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
if let error = error {
|
||||
errorMessage = error.localizedDescription
|
||||
return
|
||||
}
|
||||
guard let url = url else {
|
||||
errorMessage = "Unknown error"
|
||||
return
|
||||
}
|
||||
do {
|
||||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||||
temporaryUrls.append(temporaryUrl)
|
||||
} catch {
|
||||
errorMessage = "Failed to create a temporary copy of the file"
|
||||
}
|
||||
})
|
||||
} else {
|
||||
errorMessage = "Unsupported file type identifier"
|
||||
}
|
||||
}
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if let errorMessage = errorMessage {
|
||||
self.plugin.onFilePickerEvent(.error(errorMessage))
|
||||
return
|
||||
}
|
||||
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import XCTest
|
||||
@testable import ExamplePlugin
|
||||
|
||||
final class ExamplePluginTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
let plugin = ExamplePlugin()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "tauri-plugin-dialog-api",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"authors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"type": "module",
|
||||
"browser": "dist-js/index.min.js",
|
||||
"module": "dist-js/index.mjs",
|
||||
"types": "dist-js/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist-js/index.mjs",
|
||||
"types": "./dist-js/index.d.ts",
|
||||
"browser": "./dist-js/index.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c"
|
||||
},
|
||||
"files": [
|
||||
"dist-js",
|
||||
"!dist-js/**/*.map",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
}
|
||||
}
|
||||
@@ -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/],
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{command, Manager, Runtime, State, Window};
|
||||
|
||||
use crate::{Dialog, FileDialogBuilder, FileResponse, MessageDialogKind, Result};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OpenResponse {
|
||||
#[cfg(desktop)]
|
||||
Folders(Option<Vec<PathBuf>>),
|
||||
#[cfg(desktop)]
|
||||
Folder(Option<PathBuf>),
|
||||
Files(Option<Vec<FileResponse>>),
|
||||
File(Option<FileResponse>),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DialogFilter {
|
||||
name: String,
|
||||
extensions: Vec<String>,
|
||||
}
|
||||
|
||||
/// The options for the open dialog API.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenDialogOptions {
|
||||
/// The title of the dialog window.
|
||||
title: Option<String>,
|
||||
/// The filters of the dialog.
|
||||
#[serde(default)]
|
||||
filters: Vec<DialogFilter>,
|
||||
/// Whether the dialog allows multiple selection or not.
|
||||
#[serde(default)]
|
||||
multiple: bool,
|
||||
/// Whether the dialog is a directory selection (`true` value) or file selection (`false` value).
|
||||
#[serde(default)]
|
||||
directory: bool,
|
||||
/// The initial path of the dialog.
|
||||
default_path: Option<PathBuf>,
|
||||
/// If [`Self::directory`] is true, indicates that it will be read recursively later.
|
||||
/// Defines whether subdirectories will be allowed on the scope or not.
|
||||
#[serde(default)]
|
||||
#[cfg_attr(mobile, allow(dead_code))]
|
||||
recursive: bool,
|
||||
}
|
||||
|
||||
/// The options for the save dialog API.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(mobile, allow(dead_code))]
|
||||
pub struct SaveDialogOptions {
|
||||
/// The title of the dialog window.
|
||||
title: Option<String>,
|
||||
/// The filters of the dialog.
|
||||
#[serde(default)]
|
||||
filters: Vec<DialogFilter>,
|
||||
/// The initial path of the dialog.
|
||||
default_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn set_default_path<R: Runtime>(
|
||||
mut dialog_builder: FileDialogBuilder<R>,
|
||||
default_path: PathBuf,
|
||||
) -> FileDialogBuilder<R> {
|
||||
if default_path.is_file() || !default_path.exists() {
|
||||
if let (Some(parent), Some(file_name)) = (default_path.parent(), default_path.file_name()) {
|
||||
if parent.components().count() > 0 {
|
||||
dialog_builder = dialog_builder.set_directory(parent);
|
||||
}
|
||||
dialog_builder = dialog_builder.set_file_name(file_name.to_string_lossy());
|
||||
} else {
|
||||
dialog_builder = dialog_builder.set_directory(default_path);
|
||||
}
|
||||
dialog_builder
|
||||
} else {
|
||||
dialog_builder.set_directory(default_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn open<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
options: OpenDialogOptions,
|
||||
) -> Result<OpenResponse> {
|
||||
let mut dialog_builder = dialog.file();
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
dialog_builder = dialog_builder.set_parent(&window);
|
||||
}
|
||||
if let Some(title) = options.title {
|
||||
dialog_builder = dialog_builder.set_title(&title);
|
||||
}
|
||||
if let Some(default_path) = options.default_path {
|
||||
dialog_builder = set_default_path(dialog_builder, default_path);
|
||||
}
|
||||
for filter in options.filters {
|
||||
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
|
||||
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
|
||||
}
|
||||
|
||||
let res = if options.directory {
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
if options.multiple {
|
||||
let folders = dialog_builder.blocking_pick_folders();
|
||||
if let Some(folders) = &folders {
|
||||
for folder in folders {
|
||||
window
|
||||
.fs_scope()
|
||||
.allow_directory(folder, options.recursive)?;
|
||||
}
|
||||
}
|
||||
OpenResponse::Folders(folders)
|
||||
} else {
|
||||
let folder = dialog_builder.blocking_pick_folder();
|
||||
if let Some(path) = &folder {
|
||||
window.fs_scope().allow_directory(path, options.recursive)?;
|
||||
}
|
||||
OpenResponse::Folder(folder)
|
||||
}
|
||||
}
|
||||
#[cfg(mobile)]
|
||||
return Err(crate::Error::FolderPickerNotImplemented);
|
||||
} else if options.multiple {
|
||||
let files = dialog_builder.blocking_pick_files();
|
||||
if let Some(files) = &files {
|
||||
for file in files {
|
||||
window.fs_scope().allow_file(&file.path)?;
|
||||
}
|
||||
}
|
||||
OpenResponse::Files(files)
|
||||
} else {
|
||||
let file = dialog_builder.blocking_pick_file();
|
||||
if let Some(file) = &file {
|
||||
window.fs_scope().allow_file(&file.path)?;
|
||||
}
|
||||
OpenResponse::File(file)
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[command]
|
||||
pub(crate) async fn save<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
options: SaveDialogOptions,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
#[cfg(mobile)]
|
||||
return Err(crate::Error::FileSaveDialogNotImplemented);
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
let mut dialog_builder = dialog.file();
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
dialog_builder = dialog_builder.set_parent(&window);
|
||||
}
|
||||
if let Some(title) = options.title {
|
||||
dialog_builder = dialog_builder.set_title(&title);
|
||||
}
|
||||
if let Some(default_path) = options.default_path {
|
||||
dialog_builder = set_default_path(dialog_builder, default_path);
|
||||
}
|
||||
for filter in options.filters {
|
||||
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
|
||||
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
|
||||
}
|
||||
|
||||
let path = dialog_builder.blocking_save_file();
|
||||
if let Some(p) = &path {
|
||||
window.fs_scope().allow_file(p)?;
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn message_dialog<R: Runtime>(
|
||||
#[allow(unused_variables)] window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
type_: Option<MessageDialogKind>,
|
||||
ok_button_label: Option<String>,
|
||||
cancel_button_label: Option<String>,
|
||||
) -> bool {
|
||||
let mut builder = dialog.message(message);
|
||||
|
||||
if let Some(title) = title {
|
||||
builder = builder.title(title);
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
builder = builder.parent(&window);
|
||||
}
|
||||
|
||||
if let Some(type_) = type_ {
|
||||
builder = builder.kind(type_);
|
||||
}
|
||||
|
||||
if let Some(ok) = ok_button_label {
|
||||
builder = builder.ok_button_label(ok);
|
||||
}
|
||||
|
||||
if let Some(cancel) = cancel_button_label {
|
||||
builder = builder.cancel_button_label(cancel);
|
||||
}
|
||||
|
||||
builder.blocking_show()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn message<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
type_: Option<MessageDialogKind>,
|
||||
ok_button_label: Option<String>,
|
||||
) -> Result<bool> {
|
||||
Ok(message_dialog(
|
||||
window,
|
||||
dialog,
|
||||
title,
|
||||
message,
|
||||
type_,
|
||||
ok_button_label,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn ask<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
type_: Option<MessageDialogKind>,
|
||||
ok_button_label: Option<String>,
|
||||
cancel_button_label: Option<String>,
|
||||
) -> Result<bool> {
|
||||
Ok(message_dialog(
|
||||
window,
|
||||
dialog,
|
||||
title,
|
||||
message,
|
||||
type_,
|
||||
ok_button_label,
|
||||
cancel_button_label,
|
||||
))
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn confirm<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
type_: Option<MessageDialogKind>,
|
||||
ok_button_label: Option<String>,
|
||||
cancel_button_label: Option<String>,
|
||||
) -> Result<bool> {
|
||||
Ok(message_dialog(
|
||||
window,
|
||||
dialog,
|
||||
title,
|
||||
message,
|
||||
type_,
|
||||
ok_button_label,
|
||||
cancel_button_label,
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Use native message and file open/save dialogs.
|
||||
//!
|
||||
//! This module exposes non-blocking APIs on its root, relying on callback closures
|
||||
//! to give results back. This is particularly useful when running dialogs from the main thread.
|
||||
//! When using on asynchronous contexts such as async commands, the [`blocking`] APIs are recommended.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{plugin::PluginApi, AppHandle, Runtime};
|
||||
|
||||
use crate::{models::*, FileDialogBuilder, MessageDialogBuilder};
|
||||
|
||||
const OK: &str = "Ok";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
type FileDialog = rfd::FileDialog;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
type FileDialog = rfd::AsyncFileDialog;
|
||||
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
app: &AppHandle<R>,
|
||||
_api: PluginApi<R, C>,
|
||||
) -> crate::Result<Dialog<R>> {
|
||||
Ok(Dialog(app.clone()))
|
||||
}
|
||||
|
||||
/// Access to the dialog APIs.
|
||||
#[derive(Debug)]
|
||||
pub struct Dialog<R: Runtime>(AppHandle<R>);
|
||||
|
||||
impl<R: Runtime> Clone for Dialog<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Dialog<R> {
|
||||
pub(crate) fn app_handle(&self) -> &AppHandle<R> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
macro_rules! run_dialog {
|
||||
($e:expr, $h: ident) => {{
|
||||
std::thread::spawn(move || {
|
||||
let response = $e;
|
||||
$h(response);
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
macro_rules! run_dialog {
|
||||
($e:expr, $h: ident) => {{
|
||||
std::thread::spawn(move || {
|
||||
let context = glib::MainContext::default();
|
||||
context.invoke_with_priority(glib::PRIORITY_HIGH, move || {
|
||||
let response = $e;
|
||||
$h(response);
|
||||
});
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
macro_rules! run_file_dialog {
|
||||
($e:expr, $h: ident) => {{
|
||||
std::thread::spawn(move || {
|
||||
let response = tauri::async_runtime::block_on($e);
|
||||
$h(response);
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
macro_rules! run_file_dialog {
|
||||
($e:expr, $h: ident) => {{
|
||||
std::thread::spawn(move || {
|
||||
let context = glib::MainContext::default();
|
||||
context.invoke_with_priority(glib::PRIORITY_HIGH, move || {
|
||||
let response = $e;
|
||||
$h(response);
|
||||
});
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
impl From<MessageDialogKind> for rfd::MessageLevel {
|
||||
fn from(kind: MessageDialogKind) -> Self {
|
||||
match kind {
|
||||
MessageDialogKind::Info => Self::Info,
|
||||
MessageDialogKind::Warning => Self::Warning,
|
||||
MessageDialogKind::Error => Self::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> From<FileDialogBuilder<R>> for FileDialog {
|
||||
fn from(d: FileDialogBuilder<R>) -> Self {
|
||||
let mut builder = FileDialog::new();
|
||||
|
||||
if let Some(title) = d.title {
|
||||
builder = builder.set_title(&title);
|
||||
}
|
||||
if let Some(starting_directory) = d.starting_directory {
|
||||
builder = builder.set_directory(starting_directory);
|
||||
}
|
||||
if let Some(file_name) = d.file_name {
|
||||
builder = builder.set_file_name(&file_name);
|
||||
}
|
||||
for filter in d.filters {
|
||||
let v: Vec<&str> = filter.extensions.iter().map(|x| &**x).collect();
|
||||
builder = builder.add_filter(&filter.name, &v);
|
||||
}
|
||||
#[cfg(desktop)]
|
||||
if let Some(_parent) = d.parent {
|
||||
// TODO builder = builder.set_parent(&parent);
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> From<MessageDialogBuilder<R>> for rfd::MessageDialog {
|
||||
fn from(d: MessageDialogBuilder<R>) -> Self {
|
||||
let mut dialog = rfd::MessageDialog::new()
|
||||
.set_title(&d.title)
|
||||
.set_description(&d.message)
|
||||
.set_level(d.kind.into());
|
||||
|
||||
let buttons = match (d.ok_button_label, d.cancel_button_label) {
|
||||
(Some(ok), Some(cancel)) => Some(rfd::MessageButtons::OkCancelCustom(ok, cancel)),
|
||||
(Some(ok), None) => Some(rfd::MessageButtons::OkCustom(ok)),
|
||||
(None, Some(cancel)) => Some(rfd::MessageButtons::OkCancelCustom(OK.into(), cancel)),
|
||||
(None, None) => None,
|
||||
};
|
||||
if let Some(buttons) = buttons {
|
||||
dialog = dialog.set_buttons(buttons);
|
||||
}
|
||||
|
||||
if let Some(_parent) = d.parent {
|
||||
// TODO dialog.set_parent(parent);
|
||||
}
|
||||
|
||||
dialog
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pick_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf()));
|
||||
run_file_dialog!(FileDialog::from(dialog).pick_file(), f)
|
||||
}
|
||||
|
||||
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let f = |paths: Option<Vec<rfd::FileHandle>>| {
|
||||
f(paths.map(|list| list.into_iter().map(|p| p.path().to_path_buf()).collect()))
|
||||
};
|
||||
run_file_dialog!(FileDialog::from(dialog).pick_files(), f)
|
||||
}
|
||||
|
||||
pub fn pick_folder<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf()));
|
||||
run_file_dialog!(FileDialog::from(dialog).pick_folder(), f)
|
||||
}
|
||||
|
||||
pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let f = |paths: Option<Vec<rfd::FileHandle>>| {
|
||||
f(paths.map(|list| list.into_iter().map(|p| p.path().to_path_buf()).collect()))
|
||||
};
|
||||
run_file_dialog!(FileDialog::from(dialog).pick_folders(), f)
|
||||
}
|
||||
|
||||
pub fn save_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf()));
|
||||
run_file_dialog!(FileDialog::from(dialog).save_file(), f)
|
||||
}
|
||||
|
||||
/// Shows a message dialog
|
||||
pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
|
||||
dialog: MessageDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
run_dialog!(rfd::MessageDialog::from(dialog).show(), f);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Tauri(#[from] tauri::Error),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[cfg(mobile)]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
#[cfg(mobile)]
|
||||
#[error("Folder picker is not implemented on mobile")]
|
||||
FolderPickerNotImplemented,
|
||||
#[cfg(mobile)]
|
||||
#[error("File save dialog is not implemented on mobile")]
|
||||
FileSaveDialogNotImplemented,
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime,
|
||||
};
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::mpsc::sync_channel,
|
||||
};
|
||||
|
||||
pub use models::*;
|
||||
|
||||
#[cfg(desktop)]
|
||||
mod desktop;
|
||||
#[cfg(mobile)]
|
||||
mod mobile;
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
mod models;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
#[cfg(desktop)]
|
||||
use desktop::*;
|
||||
#[cfg(mobile)]
|
||||
use mobile::*;
|
||||
|
||||
macro_rules! blocking_fn {
|
||||
($self:ident, $fn:ident) => {{
|
||||
let (tx, rx) = sync_channel(0);
|
||||
let cb = move |response| {
|
||||
tx.send(response).unwrap();
|
||||
};
|
||||
$self.$fn(cb);
|
||||
rx.recv().unwrap()
|
||||
}};
|
||||
}
|
||||
|
||||
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the dialog APIs.
|
||||
pub trait DialogExt<R: Runtime> {
|
||||
fn dialog(&self) -> &Dialog<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> crate::DialogExt<R> for T {
|
||||
fn dialog(&self) -> &Dialog<R> {
|
||||
self.state::<Dialog<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Dialog<R> {
|
||||
pub fn message(&self, message: impl Into<String>) -> MessageDialogBuilder<R> {
|
||||
MessageDialogBuilder::new(
|
||||
self.clone(),
|
||||
self.app_handle().package_info().name.clone(),
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn file(&self) -> FileDialogBuilder<R> {
|
||||
FileDialogBuilder::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("dialog")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::open,
|
||||
commands::save,
|
||||
commands::message,
|
||||
commands::ask,
|
||||
commands::confirm
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let dialog = mobile::init(app, api)?;
|
||||
#[cfg(desktop)]
|
||||
let dialog = desktop::init(app, api)?;
|
||||
app.manage(dialog);
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
/// A builder for message dialogs.
|
||||
pub struct MessageDialogBuilder<R: Runtime> {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) dialog: Dialog<R>,
|
||||
pub(crate) title: String,
|
||||
pub(crate) message: String,
|
||||
pub(crate) kind: MessageDialogKind,
|
||||
pub(crate) ok_button_label: Option<String>,
|
||||
pub(crate) cancel_button_label: Option<String>,
|
||||
#[cfg(desktop)]
|
||||
pub(crate) parent: Option<raw_window_handle::RawWindowHandle>,
|
||||
}
|
||||
|
||||
/// Payload for the message dialog mobile API.
|
||||
#[cfg(mobile)]
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageDialogPayload<'a> {
|
||||
title: &'a String,
|
||||
message: &'a String,
|
||||
kind: &'a MessageDialogKind,
|
||||
ok_button_label: &'a Option<String>,
|
||||
cancel_button_label: &'a Option<String>,
|
||||
}
|
||||
|
||||
// raw window handle :(
|
||||
unsafe impl<R: Runtime> Send for MessageDialogBuilder<R> {}
|
||||
|
||||
impl<R: Runtime> MessageDialogBuilder<R> {
|
||||
/// Creates a new message dialog builder.
|
||||
pub fn new(dialog: Dialog<R>, title: impl Into<String>, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
dialog,
|
||||
title: title.into(),
|
||||
message: message.into(),
|
||||
kind: Default::default(),
|
||||
ok_button_label: None,
|
||||
cancel_button_label: None,
|
||||
#[cfg(desktop)]
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
pub(crate) fn payload(&self) -> MessageDialogPayload<'_> {
|
||||
MessageDialogPayload {
|
||||
title: &self.title,
|
||||
message: &self.message,
|
||||
kind: &self.kind,
|
||||
ok_button_label: &self.ok_button_label,
|
||||
cancel_button_label: &self.cancel_button_label,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the dialog title.
|
||||
pub fn title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = title.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set parent windows explicitly (optional)
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Unsupported.
|
||||
#[cfg(desktop)]
|
||||
pub fn parent<W: raw_window_handle::HasRawWindowHandle>(mut self, parent: &W) -> Self {
|
||||
self.parent.replace(parent.raw_window_handle());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the label for the OK button.
|
||||
pub fn ok_button_label(mut self, label: impl Into<String>) -> Self {
|
||||
self.ok_button_label.replace(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the label for the Cancel button.
|
||||
pub fn cancel_button_label(mut self, label: impl Into<String>) -> Self {
|
||||
self.cancel_button_label.replace(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set type of a dialog.
|
||||
///
|
||||
/// Depending on the system it can result in type specific icon to show up,
|
||||
/// the will inform user it message is a error, warning or just information.
|
||||
pub fn kind(mut self, kind: MessageDialogKind) -> Self {
|
||||
self.kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows a message dialog
|
||||
pub fn show<F: FnOnce(bool) + Send + 'static>(self, f: F) {
|
||||
show_message_dialog(self, f)
|
||||
}
|
||||
|
||||
//// Shows a message dialog.
|
||||
pub fn blocking_show(self) -> bool {
|
||||
blocking_fn!(self, show)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileResponse {
|
||||
pub base64_data: Option<String>,
|
||||
pub duration: Option<u64>,
|
||||
pub height: Option<usize>,
|
||||
pub width: Option<usize>,
|
||||
pub mime_type: Option<String>,
|
||||
pub modified_at: Option<u64>,
|
||||
pub name: Option<String>,
|
||||
pub path: PathBuf,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
impl FileResponse {
|
||||
#[cfg(desktop)]
|
||||
fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
base64_data: None,
|
||||
duration: None,
|
||||
height: None,
|
||||
width: None,
|
||||
mime_type: None,
|
||||
modified_at: None,
|
||||
name: path.file_name().map(|f| f.to_string_lossy().into_owned()),
|
||||
path,
|
||||
size: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct Filter {
|
||||
pub name: String,
|
||||
pub extensions: Vec<String>,
|
||||
}
|
||||
|
||||
/// The file dialog builder.
|
||||
///
|
||||
/// Constructs file picker dialogs that can select single/multiple files or directories.
|
||||
#[derive(Debug)]
|
||||
pub struct FileDialogBuilder<R: Runtime> {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) dialog: Dialog<R>,
|
||||
pub(crate) filters: Vec<Filter>,
|
||||
pub(crate) starting_directory: Option<PathBuf>,
|
||||
pub(crate) file_name: Option<String>,
|
||||
pub(crate) title: Option<String>,
|
||||
#[cfg(desktop)]
|
||||
pub(crate) parent: Option<raw_window_handle::RawWindowHandle>,
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileDialogPayload<'a> {
|
||||
filters: &'a Vec<Filter>,
|
||||
multiple: bool,
|
||||
}
|
||||
|
||||
// raw window handle :(
|
||||
unsafe impl<R: Runtime> Send for FileDialogBuilder<R> {}
|
||||
|
||||
impl<R: Runtime> FileDialogBuilder<R> {
|
||||
/// Gets the default file dialog builder.
|
||||
pub fn new(dialog: Dialog<R>) -> Self {
|
||||
Self {
|
||||
dialog,
|
||||
filters: Vec::new(),
|
||||
starting_directory: None,
|
||||
file_name: None,
|
||||
title: None,
|
||||
#[cfg(desktop)]
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> {
|
||||
FileDialogPayload {
|
||||
filters: &self.filters,
|
||||
multiple,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add file extension filter. Takes in the name of the filter, and list of extensions
|
||||
#[must_use]
|
||||
pub fn add_filter(mut self, name: impl Into<String>, extensions: &[&str]) -> Self {
|
||||
self.filters.push(Filter {
|
||||
name: name.into(),
|
||||
extensions: extensions.iter().map(|e| e.to_string()).collect(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Set starting directory of the dialog.
|
||||
#[must_use]
|
||||
pub fn set_directory<P: AsRef<Path>>(mut self, directory: P) -> Self {
|
||||
self.starting_directory.replace(directory.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set starting file name of the dialog.
|
||||
#[must_use]
|
||||
pub fn set_file_name(mut self, file_name: impl Into<String>) -> Self {
|
||||
self.file_name.replace(file_name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the parent window of the dialog.
|
||||
#[cfg(desktop)]
|
||||
#[must_use]
|
||||
pub fn set_parent<W: raw_window_handle::HasRawWindowHandle>(mut self, parent: &W) -> Self {
|
||||
self.parent.replace(parent.raw_window_handle());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the title of the dialog.
|
||||
#[must_use]
|
||||
pub fn set_title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title.replace(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows the dialog to select a single file.
|
||||
/// This is not a blocking operation,
|
||||
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
|
||||
///
|
||||
/// For usage in other contexts such as commands, prefer [`Self::pick_file`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::FileDialogBuilder;
|
||||
/// tauri::Builder::default()
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("failed to build tauri app")
|
||||
/// .run(|_app, _event| {
|
||||
/// FileDialogBuilder::new().pick_file(|file_path| {
|
||||
/// // do something with the optional file path here
|
||||
/// // the file path is `None` if the user closed the dialog
|
||||
/// })
|
||||
/// })
|
||||
/// ```
|
||||
pub fn pick_file<F: FnOnce(Option<FileResponse>) + Send + 'static>(self, f: F) {
|
||||
#[cfg(desktop)]
|
||||
let f = |path: Option<PathBuf>| f(path.map(FileResponse::new));
|
||||
pick_file(self, f)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select multiple files.
|
||||
/// This is not a blocking operation,
|
||||
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::FileDialogBuilder;
|
||||
/// tauri::Builder::default()
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("failed to build tauri app")
|
||||
/// .run(|_app, _event| {
|
||||
/// FileDialogBuilder::new().pick_files(|file_paths| {
|
||||
/// // do something with the optional file paths here
|
||||
/// // the file paths value is `None` if the user closed the dialog
|
||||
/// })
|
||||
/// })
|
||||
/// ```
|
||||
pub fn pick_files<F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>(self, f: F) {
|
||||
#[cfg(desktop)]
|
||||
let f = |paths: Option<Vec<PathBuf>>| {
|
||||
f(paths.map(|p| {
|
||||
p.into_iter()
|
||||
.map(FileResponse::new)
|
||||
.collect::<Vec<FileResponse>>()
|
||||
}))
|
||||
};
|
||||
pick_files(self, f)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select a single folder.
|
||||
/// This is not a blocking operation,
|
||||
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::FileDialogBuilder;
|
||||
/// tauri::Builder::default()
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("failed to build tauri app")
|
||||
/// .run(|_app, _event| {
|
||||
/// FileDialogBuilder::new().pick_folder(|folder_path| {
|
||||
/// // do something with the optional folder path here
|
||||
/// // the folder path is `None` if the user closed the dialog
|
||||
/// })
|
||||
/// })
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn pick_folder<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
|
||||
pick_folder(self, f)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select multiple folders.
|
||||
/// This is not a blocking operation,
|
||||
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::FileDialogBuilder;
|
||||
/// tauri::Builder::default()
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("failed to build tauri app")
|
||||
/// .run(|_app, _event| {
|
||||
/// FileDialogBuilder::new().pick_folders(|file_paths| {
|
||||
/// // do something with the optional folder paths here
|
||||
/// // the folder paths value is `None` if the user closed the dialog
|
||||
/// })
|
||||
/// })
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn pick_folders<F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(self, f: F) {
|
||||
pick_folders(self, f)
|
||||
}
|
||||
|
||||
/// Shows the dialog to save a file.
|
||||
///
|
||||
/// This is not a blocking operation,
|
||||
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::FileDialogBuilder;
|
||||
/// tauri::Builder::default()
|
||||
/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
|
||||
/// .expect("failed to build tauri app")
|
||||
/// .run(|_app, _event| {
|
||||
/// FileDialogBuilder::new().save_file(|file_path| {
|
||||
/// // do something with the optional file path here
|
||||
/// // the file path is `None` if the user closed the dialog
|
||||
/// })
|
||||
/// })
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn save_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
|
||||
save_file(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Blocking APIs.
|
||||
impl<R: Runtime> FileDialogBuilder<R> {
|
||||
/// Shows the dialog to select a single file.
|
||||
/// This is a blocking operation,
|
||||
/// and should *NOT* be used when running on the main thread context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
/// #[tauri::command]
|
||||
/// async fn my_command() {
|
||||
/// let file_path = FileDialogBuilder::new().pick_file();
|
||||
/// // do something with the optional file path here
|
||||
/// // the file path is `None` if the user closed the dialog
|
||||
/// }
|
||||
/// ```
|
||||
pub fn blocking_pick_file(self) -> Option<FileResponse> {
|
||||
blocking_fn!(self, pick_file)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select multiple files.
|
||||
/// This is a blocking operation,
|
||||
/// and should *NOT* be used when running on the main thread context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
/// #[tauri::command]
|
||||
/// async fn my_command() {
|
||||
/// let file_path = FileDialogBuilder::new().pick_files();
|
||||
/// // do something with the optional file paths here
|
||||
/// // the file paths value is `None` if the user closed the dialog
|
||||
/// }
|
||||
/// ```
|
||||
pub fn blocking_pick_files(self) -> Option<Vec<FileResponse>> {
|
||||
blocking_fn!(self, pick_files)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select a single folder.
|
||||
/// This is a blocking operation,
|
||||
/// and should *NOT* be used when running on the main thread context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
/// #[tauri::command]
|
||||
/// async fn my_command() {
|
||||
/// let folder_path = FileDialogBuilder::new().pick_folder();
|
||||
/// // do something with the optional folder path here
|
||||
/// // the folder path is `None` if the user closed the dialog
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn blocking_pick_folder(self) -> Option<PathBuf> {
|
||||
blocking_fn!(self, pick_folder)
|
||||
}
|
||||
|
||||
/// Shows the dialog to select multiple folders.
|
||||
/// This is a blocking operation,
|
||||
/// and should *NOT* be used when running on the main thread context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
/// #[tauri::command]
|
||||
/// async fn my_command() {
|
||||
/// let folder_paths = FileDialogBuilder::new().pick_folders();
|
||||
/// // do something with the optional folder paths here
|
||||
/// // the folder paths value is `None` if the user closed the dialog
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn blocking_pick_folders(self) -> Option<Vec<PathBuf>> {
|
||||
blocking_fn!(self, pick_folders)
|
||||
}
|
||||
|
||||
/// Shows the dialog to save a file.
|
||||
/// This is a blocking operation,
|
||||
/// and should *NOT* be used when running on the main thread context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
/// #[tauri::command]
|
||||
/// async fn my_command() {
|
||||
/// let file_path = FileDialogBuilder::new().save_file();
|
||||
/// // do something with the optional file path here
|
||||
/// // the file path is `None` if the user closed the dialog
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(desktop)]
|
||||
pub fn blocking_save_file(self) -> Option<PathBuf> {
|
||||
blocking_fn!(self, save_file)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use tauri::{
|
||||
plugin::{PluginApi, PluginHandle},
|
||||
AppHandle, Runtime,
|
||||
};
|
||||
|
||||
use crate::{FileDialogBuilder, FileResponse, MessageDialogBuilder};
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog";
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
tauri::ios_plugin_binding!(init_plugin_dialog);
|
||||
|
||||
// initializes the Kotlin or Swift plugin classes
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
_app: &AppHandle<R>,
|
||||
api: PluginApi<R, C>,
|
||||
) -> crate::Result<Dialog<R>> {
|
||||
#[cfg(target_os = "android")]
|
||||
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "DialogPlugin")?;
|
||||
#[cfg(target_os = "ios")]
|
||||
let handle = api.register_ios_plugin(init_plugin_dialog)?;
|
||||
Ok(Dialog(handle))
|
||||
}
|
||||
|
||||
/// Access to the dialog APIs.
|
||||
#[derive(Debug)]
|
||||
pub struct Dialog<R: Runtime>(PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> Clone for Dialog<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Dialog<R> {
|
||||
pub(crate) fn app_handle(&self) -> &AppHandle<R> {
|
||||
self.0.app()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FilePickerResponse {
|
||||
files: Vec<FileResponse>,
|
||||
}
|
||||
|
||||
pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
std::thread::spawn(move || {
|
||||
let res = dialog
|
||||
.dialog
|
||||
.0
|
||||
.run_mobile_plugin::<FilePickerResponse>("showFilePicker", dialog.payload(false));
|
||||
if let Ok(response) = res {
|
||||
f(Some(response.files.into_iter().next().unwrap()))
|
||||
} else {
|
||||
f(None)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>(
|
||||
dialog: FileDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
std::thread::spawn(move || {
|
||||
let res = dialog
|
||||
.dialog
|
||||
.0
|
||||
.run_mobile_plugin::<FilePickerResponse>("showFilePicker", dialog.payload(true));
|
||||
if let Ok(response) = res {
|
||||
f(Some(response.files))
|
||||
} else {
|
||||
f(None)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ShowMessageDialogResponse {
|
||||
#[allow(dead_code)]
|
||||
cancelled: bool,
|
||||
value: bool,
|
||||
}
|
||||
|
||||
/// Shows a message dialog
|
||||
pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
|
||||
dialog: MessageDialogBuilder<R>,
|
||||
f: F,
|
||||
) {
|
||||
std::thread::spawn(move || {
|
||||
let res = dialog
|
||||
.dialog
|
||||
.0
|
||||
.run_mobile_plugin::<ShowMessageDialogResponse>("showMessageDialog", dialog.payload());
|
||||
f(res.map(|r| r.value).unwrap_or_default())
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
/// Types of message, ask and confirm dialogs.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum MessageDialogKind {
|
||||
/// Information dialog.
|
||||
Info,
|
||||
/// Warning dialog.
|
||||
Warning,
|
||||
/// Error dialog.
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Default for MessageDialogKind {
|
||||
fn default() -> Self {
|
||||
Self::Info
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MessageDialogKind {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(match s.to_lowercase().as_str() {
|
||||
"info" => MessageDialogKind::Info,
|
||||
"warning" => MessageDialogKind::Warning,
|
||||
"error" => MessageDialogKind::Error,
|
||||
_ => MessageDialogKind::Info,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for MessageDialogKind {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Info => serializer.serialize_str("info"),
|
||||
Self::Warning => serializer.serialize_str("warning"),
|
||||
Self::Error => serializer.serialize_str("error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["guest-js/*.ts"]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "tauri-plugin-fs-extra"
|
||||
version = "0.1.0"
|
||||
description = "Additional file system methods not included in the core API."
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
|
||||
export interface Permissions {
|
||||
/**
|
||||
* `true` if these permissions describe a readonly (unwritable) file.
|
||||
*/
|
||||
readonly: boolean;
|
||||
/**
|
||||
* The underlying raw `st_mode` bits that contain the standard Unix permissions for this file.
|
||||
*/
|
||||
mode: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata information about a file.
|
||||
* This structure is returned from the `metadata` function or method
|
||||
* and represents known metadata about a file such as its permissions, size, modification times, etc.
|
||||
*/
|
||||
export interface Metadata {
|
||||
/**
|
||||
* The last access time of this metadata.
|
||||
*/
|
||||
accessedAt: Date;
|
||||
/**
|
||||
* The creation time listed in this metadata.
|
||||
*/
|
||||
createdAt: Date;
|
||||
/**
|
||||
* The last modification time listed in this metadata.
|
||||
*/
|
||||
modifiedAt: Date;
|
||||
/**
|
||||
* `true` if this metadata is for a directory.
|
||||
*/
|
||||
isDir: boolean;
|
||||
/**
|
||||
* `true` if this metadata is for a regular file.
|
||||
*/
|
||||
isFile: boolean;
|
||||
/**
|
||||
* `true` if this metadata is for a symbolic link.
|
||||
*/
|
||||
isSymlink: boolean;
|
||||
/**
|
||||
* The size of the file, in bytes, this metadata is for.
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* The permissions of the file this metadata is for.
|
||||
*/
|
||||
permissions: Permissions;
|
||||
/**
|
||||
* The ID of the device containing the file. Only available on Unix.
|
||||
*/
|
||||
dev: number | undefined;
|
||||
/**
|
||||
* The inode number. Only available on Unix.
|
||||
*/
|
||||
ino: number | undefined;
|
||||
/**
|
||||
* The rights applied to this file. Only available on Unix.
|
||||
*/
|
||||
mode: number | undefined;
|
||||
/**
|
||||
* The number of hard links pointing to this file. Only available on Unix.
|
||||
*/
|
||||
nlink: number | undefined;
|
||||
/**
|
||||
* The user ID of the owner of this file. Only available on Unix.
|
||||
*/
|
||||
uid: number | undefined;
|
||||
/**
|
||||
* The group ID of the owner of this file. Only available on Unix.
|
||||
*/
|
||||
gid: number | undefined;
|
||||
/**
|
||||
* The device ID of this file (if it is a special one). Only available on Unix.
|
||||
*/
|
||||
rdev: number | undefined;
|
||||
/**
|
||||
* The block size for filesystem I/O. Only available on Unix.
|
||||
*/
|
||||
blksize: number | undefined;
|
||||
/**
|
||||
* The number of blocks allocated to the file, in 512-byte units. Only available on Unix.
|
||||
*/
|
||||
blocks: number | undefined;
|
||||
}
|
||||
|
||||
interface BackendMetadata {
|
||||
accessedAtMs: number;
|
||||
createdAtMs: number;
|
||||
modifiedAtMs: number;
|
||||
isDir: boolean;
|
||||
isFile: boolean;
|
||||
isSymlink: boolean;
|
||||
size: number;
|
||||
permissions: Permissions;
|
||||
dev: number | undefined;
|
||||
ino: number | undefined;
|
||||
mode: number | undefined;
|
||||
nlink: number | undefined;
|
||||
uid: number | undefined;
|
||||
gid: number | undefined;
|
||||
rdev: number | undefined;
|
||||
blksize: number | undefined;
|
||||
blocks: number | undefined;
|
||||
}
|
||||
|
||||
export async function metadata(path: string): Promise<Metadata> {
|
||||
return await invoke<BackendMetadata>("plugin:fs-extra|metadata", {
|
||||
path,
|
||||
}).then((metadata) => {
|
||||
const { accessedAtMs, createdAtMs, modifiedAtMs, ...data } = metadata;
|
||||
return {
|
||||
accessedAt: new Date(accessedAtMs),
|
||||
createdAt: new Date(createdAtMs),
|
||||
modifiedAt: new Date(modifiedAtMs),
|
||||
...data,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function exists(path: string): Promise<boolean> {
|
||||
return await invoke("plugin:fs-extra|exists", { path });
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
use tauri::{
|
||||
command,
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
Runtime,
|
||||
};
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Permissions {
|
||||
readonly: bool,
|
||||
#[cfg(unix)]
|
||||
mode: u32,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UnixMetadata {
|
||||
dev: u64,
|
||||
ino: u64,
|
||||
mode: u32,
|
||||
nlink: u64,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
rdev: u64,
|
||||
blksize: u64,
|
||||
blocks: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Metadata {
|
||||
accessed_at_ms: u64,
|
||||
created_at_ms: u64,
|
||||
modified_at_ms: u64,
|
||||
is_dir: bool,
|
||||
is_file: bool,
|
||||
is_symlink: bool,
|
||||
size: u64,
|
||||
permissions: Permissions,
|
||||
#[cfg(unix)]
|
||||
#[serde(flatten)]
|
||||
unix: UnixMetadata,
|
||||
#[cfg(windows)]
|
||||
file_attributes: u32,
|
||||
}
|
||||
|
||||
fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
|
||||
time.map(|t| {
|
||||
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
|
||||
duration_since_epoch.as_millis() as u64
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn metadata(path: PathBuf) -> Result<Metadata> {
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
let file_type = metadata.file_type();
|
||||
let permissions = metadata.permissions();
|
||||
Ok(Metadata {
|
||||
accessed_at_ms: system_time_to_ms(metadata.accessed()),
|
||||
created_at_ms: system_time_to_ms(metadata.created()),
|
||||
modified_at_ms: system_time_to_ms(metadata.modified()),
|
||||
is_dir: file_type.is_dir(),
|
||||
is_file: file_type.is_file(),
|
||||
is_symlink: file_type.is_symlink(),
|
||||
size: metadata.len(),
|
||||
permissions: Permissions {
|
||||
readonly: permissions.readonly(),
|
||||
#[cfg(unix)]
|
||||
mode: permissions.mode(),
|
||||
},
|
||||
#[cfg(unix)]
|
||||
unix: UnixMetadata {
|
||||
dev: metadata.dev(),
|
||||
ino: metadata.ino(),
|
||||
mode: metadata.mode(),
|
||||
nlink: metadata.nlink(),
|
||||
uid: metadata.uid(),
|
||||
gid: metadata.gid(),
|
||||
rdev: metadata.rdev(),
|
||||
blksize: metadata.blksize(),
|
||||
blocks: metadata.blocks(),
|
||||
},
|
||||
#[cfg(windows)]
|
||||
file_attributes: metadata.file_attributes(),
|
||||
})
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn exists(path: PathBuf) -> bool {
|
||||
path.exists()
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
PluginBuilder::new("fs-extra")
|
||||
.invoke_handler(tauri::generate_handler![exists, metadata])
|
||||
.build()
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-plugin-fs-watch"
|
||||
version = "0.1.0"
|
||||
version = "0.0.0"
|
||||
description = "Watch files and directories for changes."
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -18,7 +18,7 @@ Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri-plugin-fs-watch = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
||||
tauri-plugin-fs-watch = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
```
|
||||
|
||||
You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
|
||||
@@ -26,11 +26,11 @@ You can install the JavaScript Guest bindings using your preferred JavaScript pa
|
||||
> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
|
||||
|
||||
```sh
|
||||
pnpm add https://github.com/tauri-apps/tauri-plugin-fs-watch
|
||||
pnpm add https://github.com/tauri-apps/tauri-plugin-fs-watch#v2
|
||||
# or
|
||||
npm add https://github.com/tauri-apps/tauri-plugin-fs-watch
|
||||
npm add https://github.com/tauri-apps/tauri-plugin-fs-watch#v2
|
||||
# or
|
||||
yarn add https://github.com/tauri-apps/tauri-plugin-fs-watch
|
||||
yarn add https://github.com/tauri-apps/tauri-plugin-fs-watch#v2
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -56,18 +56,18 @@ import { watch, watchImmediate } from "tauri-plugin-fs-watch-api";
|
||||
// can also watch an array of paths
|
||||
const stopWatching = await watch(
|
||||
"/path/to/something",
|
||||
{ recursive: true },
|
||||
(event) => {
|
||||
const { type, payload } = event;
|
||||
}
|
||||
},
|
||||
{ recursive: true }
|
||||
);
|
||||
|
||||
const stopRawWatcher = await watchImmediate(
|
||||
["/path/a", "/path/b"],
|
||||
{},
|
||||
(event) => {
|
||||
const { path, operation, cookie } = event;
|
||||
}
|
||||
},
|
||||
{}
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ async function unwatch(id: number): Promise<void> {
|
||||
|
||||
export async function watch(
|
||||
paths: string | string[],
|
||||
options: DebouncedWatchOptions,
|
||||
cb: (event: DebouncedEvent) => void
|
||||
cb: (event: DebouncedEvent) => void,
|
||||
options: DebouncedWatchOptions = {}
|
||||
): Promise<UnlistenFn> {
|
||||
const opts = {
|
||||
recursive: false,
|
||||
@@ -82,8 +82,8 @@ export async function watch(
|
||||
|
||||
export async function watchImmediate(
|
||||
paths: string | string[],
|
||||
options: WatchOptions,
|
||||
cb: (event: RawEvent) => void
|
||||
cb: (event: RawEvent) => void,
|
||||
options: WatchOptions = {}
|
||||
): Promise<UnlistenFn> {
|
||||
const opts = {
|
||||
recursive: false,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"devDependencies": {
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0"
|
||||
|
||||
Generated
+3591
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "0.0.0"
|
||||
description = "Access the file system."
|
||||
edition = "2021"
|
||||
#authors.workspace = true
|
||||
#license.workspace = true
|
||||
#edition.workspace = true
|
||||
#rust-version.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
#serde.workspace = true
|
||||
#serde_json.workspace = true
|
||||
#tauri.workspace = true
|
||||
#log.workspace = true
|
||||
#thiserror.workspace = true
|
||||
serde = "1"
|
||||
thiserror = "1"
|
||||
tauri = { git = "https://github.com/tauri-apps/tauri", branch = "next" }
|
||||
anyhow = "1"
|
||||
@@ -0,0 +1,20 @@
|
||||
SPDXVersion: SPDX-2.1
|
||||
DataLicense: CC0-1.0
|
||||
PackageName: tauri
|
||||
DataFormat: SPDXRef-1
|
||||
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
|
||||
PackageHomePage: https://tauri.app
|
||||
PackageLicenseDeclared: Apache-2.0
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy
|
||||
PackageSummary: <text>Tauri is a rust project that enables developers to make secure
|
||||
and small desktop applications using a web frontend.
|
||||
</text>
|
||||
PackageComment: <text>The package includes the following libraries; see
|
||||
Relationship information.
|
||||
</text>
|
||||
Created: 2019-05-20T09:00:00Z
|
||||
PackageDownloadLocation: git://github.com/tauri-apps/tauri
|
||||
PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git
|
||||
PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git
|
||||
Creator: Person: Daniel Thompson-Yvetot
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,67 @@
|
||||

|
||||
|
||||
Access the file system.
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
There are three general methods of installation that we can recommend.
|
||||
|
||||
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
|
||||
2. Pull sources directly from Github using git tags / revision hashes (most secure)
|
||||
3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use)
|
||||
|
||||
Install the Core plugin by adding the following to your `Cargo.toml` file:
|
||||
|
||||
`src-tauri/Cargo.toml`
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
```
|
||||
|
||||
You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
|
||||
|
||||
> Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
|
||||
|
||||
```sh
|
||||
pnpm add https://github.com/tauri-apps/tauri-plugin-fs#v2
|
||||
# or
|
||||
npm add https://github.com/tauri-apps/tauri-plugin-fs#v2
|
||||
# or
|
||||
yarn add https://github.com/tauri-apps/tauri-plugin-fs#v2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
First you need to register the core plugin with Tauri:
|
||||
|
||||
`src-tauri/src/main.rs`
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_fs::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 { metadata } from "tauri-plugin-fs-api";
|
||||
|
||||
await metadata("/path/to/file");
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
|
||||
|
||||
## License
|
||||
|
||||
Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy.
|
||||
|
||||
MIT or MIT/Apache 2.0 where applicable.
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user