mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-05-01 12:08:06 +02:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f85c405b3a | |||
| ad3212a159 | |||
| d3e07db4ad | |||
| 403f54b78c | |||
| 28ea4dbadc | |||
| 1771c6ed36 | |||
| f831b003c2 | |||
| d1edf783e1 | |||
| 9804eeef03 | |||
| 829fd23b8e | |||
| 13bbb9cd3d | |||
| 9b162b51a6 | |||
| 57f0422ae9 | |||
| 581523244f | |||
| f924ef16e4 | |||
| c9babc028a | |||
| fdcc15a5fe | |||
| 53bf0af9dd | |||
| c4665050bb | |||
| 6f65edfd72 | |||
| 63f660996f | |||
| 8cb053f47c | |||
| cac333b076 | |||
| 5ce3e45768 | |||
| 88da3d26ad | |||
| af5500caed | |||
| dc55eb51ed | |||
| a132d8f3d5 | |||
| a7af1a81b7 |
+8
-2
@@ -1,5 +1,11 @@
|
||||
[advisories]
|
||||
ignore = [
|
||||
# time crate can't be updated in the repo because of MSRV, users are unaffected
|
||||
"RUSTSEC-2026-0009",
|
||||
# time 0.1
|
||||
"RUSTSEC-2020-0071",
|
||||
# needs sqlx 0.7 (still in alpha)
|
||||
"RUSTSEC-2022-0090",
|
||||
# wry needs kuchiki on Android
|
||||
"RUSTSEC-2023-0019",
|
||||
# atty is only used when the `colored` feature is enabled on tauri-plugin-log
|
||||
"RUSTSEC-2021-0145",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"dialog-js": minor:feat
|
||||
"dialog": minor:feat
|
||||
---
|
||||
|
||||
Add `xdg-portal` as an optional feature for `rfd`
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"log": "minor"
|
||||
"log-js": "minor"
|
||||
---
|
||||
|
||||
Allow specifying a log formatter per target using the `format` method on `Target`.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"sql": minor
|
||||
"sql-js": minor
|
||||
---
|
||||
|
||||
Add support for Postgres `NUMERIC` and custom data types.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"fs": minor
|
||||
"fs-js": minor
|
||||
---
|
||||
|
||||
Add `encoding` option for `readTextFile` and `readTextFileLines`
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"http": patch
|
||||
"http-js": patch
|
||||
---
|
||||
|
||||
Correct Response header initialization to support cloning and ensure Set-Cookie visibility.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"updater": patch
|
||||
"updater-js": patch
|
||||
---
|
||||
|
||||
fix: preserve file extension of updater package, otherwise users may get confused when presented with a sudo dialog suggesting to install a file with the extension `.rpm` using `dpkg -i`
|
||||
@@ -78,6 +78,7 @@
|
||||
"notification",
|
||||
"os",
|
||||
"process",
|
||||
"secure-storage",
|
||||
"shell",
|
||||
"store",
|
||||
"updater",
|
||||
@@ -104,6 +105,7 @@
|
||||
"notification-js",
|
||||
"os-js",
|
||||
"process-js",
|
||||
"secure-storage-js",
|
||||
"shell-js",
|
||||
"store-js",
|
||||
"updater-js"
|
||||
@@ -282,6 +284,14 @@
|
||||
"path": "./plugins/process",
|
||||
"manager": "javascript"
|
||||
},
|
||||
"secure-storage": {
|
||||
"path": "./plugins/secure-storage",
|
||||
"manager": "rust"
|
||||
},
|
||||
"secure-storage-js": {
|
||||
"path": "./plugins/secure-storage",
|
||||
"manager": "javascript"
|
||||
},
|
||||
"shell": {
|
||||
"path": "./plugins/shell",
|
||||
"manager": "rust"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
stronghold: patch
|
||||
stronghold-js: patch
|
||||
---
|
||||
|
||||
The `stronghold` plugin is now deprecated and will be removed in v3. The `secure-storage` plugin should be a replacement for most use-cases.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"dialog": minor
|
||||
"dialog-js": minor
|
||||
---
|
||||
|
||||
Add `pickerMode` option to file picker (currently only used on iOS)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"localhost": patch
|
||||
---
|
||||
|
||||
Disable caching on responses.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"barcode-scanner": patch
|
||||
"barcode-scanner-js": patch
|
||||
---
|
||||
|
||||
Fix the `cameraView` is not removed after scanning in iOS.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"log": patch
|
||||
"log-js": patch
|
||||
---
|
||||
|
||||
Fix log file rotation when exceeding `max_file_size`.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"nfc": "patch"
|
||||
"nfc-js": "patch"
|
||||
---
|
||||
|
||||
Update return value of `isAvailable` to match TypeScript function signature
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"dialog": minor
|
||||
"dialog-js": minor
|
||||
---
|
||||
|
||||
Re-use `message` command in Rust side for `ask` and `confirm` commands, `allow-ask` and `allow-confirm` permissions are now aliases to `allow-message`
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"fs": minor
|
||||
"fs-js": minor
|
||||
---
|
||||
|
||||
Enable access for security-scoped resources on iOS by automatically calling `NSURL::startAccessingSecurityScopedResource` on resource access and adding the `stopAccessingSecurityScopedResource` API.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"updater": minor
|
||||
"updater-js": minor
|
||||
---
|
||||
|
||||
Updater plugin now supports all bundle types: Deb, Rpm and AppImage for Linux; NSiS, MSI for Windows. This was added in https://github.com/tauri-apps/plugins-workspace/pull/2624
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"upload": minor
|
||||
"upload-js": minor
|
||||
---
|
||||
|
||||
Upload plugin now supports specifying an HTTP method i.e. POST, PUT etc.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"deep-link": patch
|
||||
"deep-link-js": patch
|
||||
---
|
||||
|
||||
Validate Android new intent is actually a deep link before triggering the onOpenUrl event.
|
||||
@@ -1,63 +0,0 @@
|
||||
# Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: 🐞 Bug Report
|
||||
description: Report a bug
|
||||
labels: ['type: bug']
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## First of all
|
||||
1. Please search for [existing issues](https://github.com/tauri-apps/plugins-workspace/issues?q=is%3Aissue) about this problem first.
|
||||
2. Make sure `rustc` and all relevant Tauri packages are up to date.
|
||||
3. Make sure it's an issue with a tauri plugin and not something else you are using.
|
||||
4. Remember to follow our community guidelines and be friendly.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear description of what the bug is. Include screenshots if applicable.
|
||||
placeholder: Bug description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction
|
||||
description: A link to a reproduction repo or steps to reproduce the behaviour.
|
||||
placeholder: |
|
||||
Please provide a minimal reproduction or steps to reproduce, see this guide https://stackoverflow.com/help/minimal-reproducible-example
|
||||
Why reproduction is required? see this article https://antfu.me/posts/why-reproductions-are-required
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear description of what you expected to happen.
|
||||
|
||||
- type: textarea
|
||||
id: info
|
||||
attributes:
|
||||
label: Full `tauri info` output
|
||||
description: 'Output of `npm run tauri info` or `cargo tauri info`. Issues without this will be closed!'
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Stack trace
|
||||
description: A stack trace or ANY error messages you may see.
|
||||
render: text
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here. For example a link to a Discord discussion.
|
||||
@@ -1,12 +0,0 @@
|
||||
# Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
#blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💡 Request a new plugin
|
||||
url: https://github.com/orgs/tauri-apps/discussions/new?category=plugin-requests
|
||||
about: Propose a new Plugin to the community or the Tauri org.
|
||||
- name: 💬 Discord Chat
|
||||
url: https://discord.com/invite/tauri
|
||||
about: Ask questions and talk to other Tauri users and the maintainers
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
name: 📚 Docs Report
|
||||
about: Create a report to help us improve the docs
|
||||
labels: 'type: documentation'
|
||||
---
|
||||
@@ -1,46 +0,0 @@
|
||||
# Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: 💡 Feature Request
|
||||
description: Request a feature or enhancement for an existing plugin
|
||||
labels: ['type: feature request']
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## First of all
|
||||
- Please search for [existing issues](https://github.com/tauri-apps/plugins-workspace/issues?q=is%3Aissue) for this request first.
|
||||
- Only requests for plugins that exist in this repo are allowed.
|
||||
- You can request new plugins [here](https://github.com/orgs/tauri-apps/discussions/new?category=plugin-requests)
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Describe the problem
|
||||
description: A clear description of the problem this feature would solve
|
||||
placeholder: "I'm always frustrated when..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: "Describe the solution you'd like"
|
||||
description: A clear description of what change you would like
|
||||
placeholder: 'I would like to...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: "Any alternative solutions you've considered"
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: rustsec/audit-check@v2
|
||||
- uses: rustsec/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# https://github.com/tauri-apps/plugins-workspace/issues/774
|
||||
|
||||
@@ -101,6 +101,11 @@ jobs:
|
||||
- pnpm-lock.yaml
|
||||
- plugins/process/guest-js/**
|
||||
- plugins/process/src/api-iife.js
|
||||
secure-storage:
|
||||
- .github/workflows/check-generated-files.yml
|
||||
- pnpm-lock.yaml
|
||||
- plugins/secure-storage/guest-js/**
|
||||
- plugins/secure-storage/src/api-iife.js
|
||||
shell:
|
||||
- .github/workflows/check-generated-files.yml
|
||||
- pnpm-lock.yaml
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
name: integration tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- v1
|
||||
|
||||
@@ -98,6 +98,9 @@ jobs:
|
||||
tauri-plugin-process:
|
||||
- .github/workflows/lint-rust.yml
|
||||
- plugins/process/**
|
||||
tauri-plugin-secure-storage:
|
||||
- .github/workflows/lint-rust.yml
|
||||
- plugins/secure-storage/**
|
||||
tauri-plugin-shell:
|
||||
- .github/workflows/lint-rust.yml
|
||||
- plugins/shell/**
|
||||
|
||||
@@ -137,6 +137,11 @@ jobs:
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- plugins/process/**
|
||||
tauri-plugin-secure-storage:
|
||||
- .github/workflows/test-rust.yml
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- plugins/secure-storage/**
|
||||
tauri-plugin-shell:
|
||||
- .github/workflows/test-rust.yml
|
||||
- Cargo.toml
|
||||
@@ -219,7 +224,7 @@ jobs:
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-latest,
|
||||
runner: 'cross',
|
||||
command: 'build'
|
||||
command: 'build --verbose'
|
||||
}
|
||||
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
@@ -233,7 +238,7 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.77.2
|
||||
- uses: dtolnay/rust-toolchain@1.85
|
||||
with:
|
||||
targets: ${{ matrix.platform.target }}
|
||||
|
||||
@@ -246,7 +251,7 @@ jobs:
|
||||
run: cargo +stable install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: test ${{ matrix.package }}
|
||||
if: ${{ matrix.package != 'tauri-plugin-http' && matrix.package != 'tauri-plugin-dialog' }}
|
||||
if: matrix.package != 'tauri-plugin-http' && matrix.package != 'tauri-plugin-secure-storage' && matrix.package != 'tauri-plugin-dialog'
|
||||
run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets --all-features
|
||||
|
||||
- name: test ${{ matrix.package }}
|
||||
|
||||
@@ -21,7 +21,6 @@ target/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
bun.lockb
|
||||
bun.lock
|
||||
|
||||
# rust compiled folders
|
||||
target/
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
plugins/*/permissions/autogenerated/
|
||||
plugins/*/android/.tauri/tauri-api/build/
|
||||
plugins/*/android/build/intermediates/
|
||||
|
||||
Generated
+380
-690
File diff suppressed because it is too large
Load Diff
+5
-13
@@ -12,16 +12,14 @@ resolver = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
log = "0.4"
|
||||
tauri = { version = "2.10", default-features = false }
|
||||
tauri-runtime-wry = "2"
|
||||
tauri-runtime-cef = { git = "https://github.com/tauri-apps/tauri.git", branch = "refactor/runtime-feature-flag" }
|
||||
tauri-build = "2.5"
|
||||
tauri-plugin = "2.5"
|
||||
tauri-utils = "2.8"
|
||||
tauri = { version = "2.8.2", default-features = false }
|
||||
tauri-build = "2.4"
|
||||
tauri-plugin = "2.4"
|
||||
tauri-utils = "2.7"
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
url = "2"
|
||||
schemars = "1"
|
||||
schemars = "0.8"
|
||||
dunce = "1"
|
||||
specta = "^2.0.0-rc.16"
|
||||
glob = "0.3"
|
||||
@@ -41,9 +39,3 @@ codegen-units = 1
|
||||
lto = true
|
||||
incremental = false
|
||||
opt-level = "s"
|
||||
|
||||
[patch.crates-io]
|
||||
tauri = { git = "https://github.com/tauri-apps/tauri.git", branch = "refactor/runtime-feature-flag" }
|
||||
tauri-utils = { git = "https://github.com/tauri-apps/tauri.git", branch = "refactor/runtime-feature-flag" }
|
||||
tauri-plugin = { git = "https://github.com/tauri-apps/tauri.git", branch = "refactor/runtime-feature-flag" }
|
||||
tauri-runtime-wry = { git = "https://github.com/tauri-apps/tauri.git", branch = "refactor/runtime-feature-flag" }
|
||||
|
||||
@@ -1,35 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.0.37]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `updater-js@2.10.0`
|
||||
- Upgraded to `barcode-scanner-js@2.4.4`
|
||||
- Upgraded to `http-js@2.5.7`
|
||||
- Upgraded to `shell-js@2.3.5`
|
||||
|
||||
## \[2.0.36]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `http-js@2.5.6`
|
||||
- Upgraded to `dialog-js@2.6.0`
|
||||
|
||||
## \[2.0.35]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `dialog-js@2.5.0`
|
||||
- Upgraded to `log-js@2.8.0`
|
||||
- Upgraded to `shell-js@2.3.4`
|
||||
- Upgraded to `barcode-scanner-js@2.4.3`
|
||||
- Upgraded to `fs-js@2.4.5`
|
||||
- Upgraded to `http-js@2.5.5`
|
||||
- Upgraded to `nfc-js@2.3.4`
|
||||
- Upgraded to `opener-js@2.5.3`
|
||||
- Upgraded to `store-js@2.4.2`
|
||||
|
||||
## \[2.0.34]
|
||||
|
||||
### Dependencies
|
||||
|
||||
+13
-12
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "api",
|
||||
"private": true,
|
||||
"version": "2.0.37",
|
||||
"version": "2.0.34",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --clearScreen false",
|
||||
@@ -10,25 +10,26 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-barcode-scanner": "^2.4.4",
|
||||
"@tauri-apps/api": "2.9.0",
|
||||
"@tauri-apps/plugin-barcode-scanner": "^2.4.2",
|
||||
"@tauri-apps/plugin-biometric": "^2.3.2",
|
||||
"@tauri-apps/plugin-cli": "^2.4.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-geolocation": "^2.2.0",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.3.1",
|
||||
"@tauri-apps/plugin-haptics": "^2.2.0",
|
||||
"@tauri-apps/plugin-http": "^2.5.7",
|
||||
"@tauri-apps/plugin-nfc": "^2.3.4",
|
||||
"@tauri-apps/plugin-http": "^2.5.4",
|
||||
"@tauri-apps/plugin-nfc": "^2.3.3",
|
||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@tauri-apps/plugin-opener": "^2.5.2",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||
"@tauri-apps/plugin-store": "^2.4.2",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@tauri-apps/plugin-secure-storage": "file:../../plugins/secure-storage",
|
||||
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||
"@tauri-apps/plugin-store": "^2.4.1",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@tauri-apps/plugin-upload": "^2.3.0",
|
||||
"@zerodevx/svelte-json-view": "1.0.11"
|
||||
},
|
||||
@@ -36,7 +37,7 @@
|
||||
"@iconify-json/codicon": "^1.2.12",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"@tauri-apps/cli": "2.10.1",
|
||||
"@tauri-apps/cli": "2.9.4",
|
||||
"@unocss/extractor-svelte": "^66.3.3",
|
||||
"svelte": "^5.20.4",
|
||||
"unocss": "^66.3.3",
|
||||
|
||||
@@ -1,35 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.0.41]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `updater@2.10.0`
|
||||
- Upgraded to `barcode-scanner@2.4.4`
|
||||
- Upgraded to `http@2.5.7`
|
||||
- Upgraded to `shell@2.3.5`
|
||||
|
||||
## \[2.0.40]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `http@2.5.6`
|
||||
- Upgraded to `dialog@2.6.0`
|
||||
|
||||
## \[2.0.39]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `dialog@2.5.0`
|
||||
- Upgraded to `log@2.8.0`
|
||||
- Upgraded to `shell@2.3.4`
|
||||
- Upgraded to `barcode-scanner@2.4.3`
|
||||
- Upgraded to `fs@2.4.5`
|
||||
- Upgraded to `http@2.5.5`
|
||||
- Upgraded to `nfc@2.3.4`
|
||||
- Upgraded to `opener@2.5.3`
|
||||
- Upgraded to `store@2.4.2`
|
||||
|
||||
## \[2.0.38]
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "api"
|
||||
publish = false
|
||||
version = "2.0.41"
|
||||
version = "2.0.38"
|
||||
description = "An example Tauri Application showcasing the api"
|
||||
edition = "2021"
|
||||
rust-version = { workspace = true }
|
||||
@@ -20,33 +20,33 @@ serde = { workspace = true }
|
||||
tiny_http = "0.12"
|
||||
time = "0.3"
|
||||
log = { workspace = true }
|
||||
tauri-plugin-log = { path = "../../../plugins/log", version = "2.8.0" }
|
||||
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.4.5", features = [
|
||||
tauri-plugin-log = { path = "../../../plugins/log", version = "2.7.1" }
|
||||
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.4.4", features = [
|
||||
"watch",
|
||||
] }
|
||||
tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.3.2" }
|
||||
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.6.0" }
|
||||
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.4.2" }
|
||||
tauri-plugin-http = { path = "../../../plugins/http", features = [
|
||||
"multipart",
|
||||
"cookies",
|
||||
], version = "2.5.7" }
|
||||
], version = "2.5.4" }
|
||||
tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.3.3", features = [
|
||||
"windows7-compat",
|
||||
] }
|
||||
tauri-plugin-os = { path = "../../../plugins/os", version = "2.3.2" }
|
||||
tauri-plugin-process = { path = "../../../plugins/process", version = "2.3.1" }
|
||||
tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.5.3" }
|
||||
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.5" }
|
||||
tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.2" }
|
||||
tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.5.2" }
|
||||
tauri-plugin-secure-storage = { path = "../../../plugins/secure-storage" }
|
||||
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.3" }
|
||||
tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.1" }
|
||||
tauri-plugin-upload = { path = "../../../plugins/upload", version = "2.3.0" }
|
||||
tauri-runtime-wry = { workspace = true, optional = true, features = [
|
||||
"macos-private-api",
|
||||
] }
|
||||
tauri-runtime-cef = { workspace = true, optional = true }
|
||||
|
||||
[dependencies.tauri]
|
||||
workspace = true
|
||||
features = [
|
||||
"wry",
|
||||
"common-controls-v6",
|
||||
"x11",
|
||||
"image-ico",
|
||||
"image-png",
|
||||
"isolation",
|
||||
@@ -58,18 +58,15 @@ features = [
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
tauri-plugin-cli = { path = "../../../plugins/cli", version = "2.4.1" }
|
||||
tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", version = "2.3.1" }
|
||||
tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.10.0" }
|
||||
tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.9.0" }
|
||||
tauri-plugin-window-state = { path = "../../../plugins/window-state", version = "2.2.0" }
|
||||
|
||||
[target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies]
|
||||
tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.4.4" }
|
||||
tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.3.4" }
|
||||
tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.4.2" }
|
||||
tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.3.3" }
|
||||
tauri-plugin-biometric = { path = "../../../plugins/biometric/", version = "2.3.2" }
|
||||
tauri-plugin-geolocation = { path = "../../../plugins/geolocation/", version = "2.3.2" }
|
||||
tauri-plugin-haptics = { path = "../../../plugins/haptics/", version = "2.3.2" }
|
||||
|
||||
[features]
|
||||
default = ["wry"]
|
||||
wry = ["dep:tauri-runtime-wry"]
|
||||
cef = ["dep:tauri-runtime-cef"]
|
||||
prod = ["tauri/custom-protocol"]
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
"core:window:allow-start-dragging",
|
||||
"notification:default",
|
||||
"os:allow-platform",
|
||||
"dialog:default",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-save",
|
||||
"dialog:allow-confirm",
|
||||
"dialog:allow-message",
|
||||
{
|
||||
"identifier": "shell:allow-spawn",
|
||||
"allow": [
|
||||
@@ -98,6 +102,10 @@
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }]
|
||||
},
|
||||
"upload:default"
|
||||
"upload:default",
|
||||
"secure-storage:allow-get-string",
|
||||
"secure-storage:allow-set-string",
|
||||
"secure-storage:allow-get-binary",
|
||||
"secure-storage:allow-set-binary"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ pub struct RequestBody {
|
||||
|
||||
#[command]
|
||||
pub fn log_operation(event: String, payload: Option<String>) {
|
||||
log::info!("{} {:?}", event, payload);
|
||||
log::info!("{event} {payload:?}");
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn perform_request(endpoint: String, body: RequestBody) -> String {
|
||||
println!("{} {:?}", endpoint, body);
|
||||
println!("{endpoint} {body:?}");
|
||||
"message response".into()
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@ mod cmd;
|
||||
#[cfg(desktop)]
|
||||
mod tray;
|
||||
|
||||
#[cfg(feature = "cef")]
|
||||
type TauriRuntime = tauri_runtime_cef::CefRuntime<tauri::EventLoopMessage>;
|
||||
#[cfg(feature = "wry")]
|
||||
type TauriRuntime = tauri_runtime_wry::Wry<tauri::EventLoopMessage>;
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::{
|
||||
webview::{PageLoadEvent, WebviewWindowBuilder},
|
||||
@@ -22,13 +17,13 @@ struct Reply {
|
||||
data: String,
|
||||
}
|
||||
|
||||
pub type SetupHook<R> = Box<dyn FnOnce(&mut App<R>) -> Result<(), Box<dyn std::error::Error>> + Send>;
|
||||
pub type OnEvent<R> = Box<dyn FnMut(&AppHandle<R>, RunEvent)>;
|
||||
pub type SetupHook = Box<dyn FnOnce(&mut App) -> Result<(), Box<dyn std::error::Error>> + Send>;
|
||||
pub type OnEvent = Box<dyn FnMut(&AppHandle, RunEvent)>;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::<TauriRuntime>::new()
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
@@ -43,6 +38,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_secure_storage::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(tauri_plugin_upload::init())
|
||||
.setup(move |app| {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import Biometric from './views/Biometric.svelte'
|
||||
import Geolocation from './views/Geolocation.svelte'
|
||||
import Haptics from './views/Haptics.svelte'
|
||||
import SecureStorage from './views/SecureStorage.svelte'
|
||||
|
||||
import { onMount, tick } from 'svelte'
|
||||
import { ask } from '@tauri-apps/plugin-dialog'
|
||||
@@ -103,6 +104,11 @@
|
||||
component: Store,
|
||||
icon: 'i-codicon-file-code'
|
||||
},
|
||||
{
|
||||
label: 'SecureStorage',
|
||||
component: SecureStorage,
|
||||
icon: 'i-codicon-file-code'
|
||||
},
|
||||
!isMobile && {
|
||||
label: 'Updater',
|
||||
component: Updater,
|
||||
@@ -213,9 +219,9 @@
|
||||
...r,
|
||||
{
|
||||
html:
|
||||
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
|
||||
(typeof value === 'string' ? value : JSON.stringify(value, null, 1)) +
|
||||
'</pre>'
|
||||
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> `
|
||||
+ (typeof value === 'string' ? value : JSON.stringify(value, null, 1))
|
||||
+ '</pre>'
|
||||
}
|
||||
])
|
||||
await tick()
|
||||
@@ -229,9 +235,9 @@
|
||||
...r,
|
||||
{
|
||||
html:
|
||||
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
|
||||
html +
|
||||
'</pre>'
|
||||
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> `
|
||||
+ html
|
||||
+ '</pre>'
|
||||
}
|
||||
])
|
||||
await tick()
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
let filter = null;
|
||||
let multiple = false;
|
||||
let directory = false;
|
||||
let pickerMode = "document";
|
||||
let fileAccessMode = "scoped";
|
||||
let pickerMode = "";
|
||||
|
||||
function arrayBufferToBase64(buffer, callback) {
|
||||
var blob = new Blob([buffer], {
|
||||
@@ -43,7 +42,7 @@
|
||||
}
|
||||
|
||||
async function msg() {
|
||||
await message("Tauri is awesome!").then((res) => onMessage(res));
|
||||
await message("Tauri is awesome!");
|
||||
}
|
||||
|
||||
async function msgCustom(result) {
|
||||
@@ -53,60 +52,54 @@
|
||||
.catch(onMessage);
|
||||
}
|
||||
|
||||
async function openDialog() {
|
||||
try {
|
||||
var result = await open({
|
||||
title: "My wonderful open dialog",
|
||||
defaultPath,
|
||||
filters: filter
|
||||
? [
|
||||
{
|
||||
name: "Tauri Example",
|
||||
extensions: filter.split(",").map((f) => f.trim()),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
multiple,
|
||||
directory,
|
||||
pickerMode,
|
||||
fileAccessMode,
|
||||
})
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
onMessage(result);
|
||||
} else {
|
||||
var pathToRead = result;
|
||||
var isFile = pathToRead.match(/\S+\.\S+$/g);
|
||||
|
||||
await readFile(pathToRead)
|
||||
.then(function (res) {
|
||||
if (isFile) {
|
||||
if (
|
||||
pathToRead.includes(".png") ||
|
||||
pathToRead.includes(".jpg") ||
|
||||
pathToRead.includes(".jpeg")
|
||||
) {
|
||||
arrayBufferToBase64(
|
||||
new Uint8Array(res),
|
||||
function (base64) {
|
||||
var src = "data:image/png;base64," + base64;
|
||||
insecureRenderHtml('<img src="' + src + '"></img>');
|
||||
}
|
||||
);
|
||||
function openDialog() {
|
||||
open({
|
||||
title: "My wonderful open dialog",
|
||||
defaultPath,
|
||||
filters: filter
|
||||
? [
|
||||
{
|
||||
name: "Tauri Example",
|
||||
extensions: filter.split(",").map((f) => f.trim()),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
multiple,
|
||||
directory,
|
||||
pickerMode: pickerMode === "" ? undefined : pickerMode,
|
||||
})
|
||||
.then(function (res) {
|
||||
if (Array.isArray(res)) {
|
||||
onMessage(res);
|
||||
} else {
|
||||
var pathToRead = res;
|
||||
var isFile = pathToRead.match(/\S+\.\S+$/g);
|
||||
readFile(pathToRead)
|
||||
.then(function (response) {
|
||||
if (isFile) {
|
||||
if (
|
||||
pathToRead.includes(".png") ||
|
||||
pathToRead.includes(".jpg") ||
|
||||
pathToRead.includes(".jpeg")
|
||||
) {
|
||||
arrayBufferToBase64(
|
||||
new Uint8Array(response),
|
||||
function (base64) {
|
||||
var src = "data:image/png;base64," + base64;
|
||||
insecureRenderHtml('<img src="' + src + '"></img>');
|
||||
}
|
||||
);
|
||||
} else {
|
||||
onMessage(res);
|
||||
}
|
||||
} else {
|
||||
// Convert byte array to UTF-8 string
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const text = decoder.decode(new Uint8Array(res));
|
||||
onMessage(text);
|
||||
onMessage(res);
|
||||
}
|
||||
} else {
|
||||
onMessage(res);
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch(exception) {
|
||||
onMessage(exception)
|
||||
}
|
||||
})
|
||||
.catch(onMessage);
|
||||
}
|
||||
})
|
||||
.catch(onMessage);
|
||||
}
|
||||
|
||||
function saveDialog() {
|
||||
@@ -161,13 +154,6 @@
|
||||
<option value="document">Document</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="dialog-file-access-mode">File Access Mode:</label>
|
||||
<select id="dialog-file-access-mode" bind:value={fileAccessMode}>
|
||||
<option value="copy">Copy</option>
|
||||
<option value="scoped">Scoped</option>
|
||||
</select>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div class="flex flex-wrap flex-col md:flex-row gap-2 children:flex-shrink-0">
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import { getString, setString } from '@tauri-apps/plugin-secure-storage'
|
||||
|
||||
export let onMessage
|
||||
|
||||
let key
|
||||
let value
|
||||
let output
|
||||
|
||||
async function read(key) {
|
||||
try {
|
||||
output = await getString(key)
|
||||
} catch (error) {
|
||||
onMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function write(key, value) {
|
||||
try {
|
||||
await setString(key, value)
|
||||
} catch (error) {
|
||||
onMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
output = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col childre:grow gap-1">
|
||||
<div class="flex flex-col flex-row-md gap-4">
|
||||
<div class="flex items-center gap-1">
|
||||
Key:
|
||||
<input class="grow input" bind:value={key} />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
Value:
|
||||
<input class="grow input" bind:value />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn" on:click={() => write(key, value)}>Write</button>
|
||||
<button class="btn" on:click={() => read(key)}>Read</button>
|
||||
<button class="btn" on:click={() => reset()}>Reset Output</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Output: {output}
|
||||
</div>
|
||||
</div>
|
||||
+8
-7
@@ -11,18 +11,19 @@
|
||||
"example:api:dev": "pnpm run --filter \"api\" tauri dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "10.0.1",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@rollup/plugin-node-resolve": "16.0.3",
|
||||
"@rollup/plugin-terser": "1.0.0",
|
||||
"@rollup/plugin-terser": "0.4.4",
|
||||
"@rollup/plugin-typescript": "12.3.0",
|
||||
"eslint": "10.0.2",
|
||||
"covector": "^0.12.4",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-security": "4.0.0",
|
||||
"prettier": "3.8.1",
|
||||
"rollup": "4.59.0",
|
||||
"eslint-plugin-security": "3.0.1",
|
||||
"prettier": "3.6.2",
|
||||
"rollup": "4.53.2",
|
||||
"tslib": "2.8.1",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.56.1"
|
||||
"typescript-eslint": "8.47.0"
|
||||
},
|
||||
"minimumReleaseAge": 4320,
|
||||
"pnpm": {
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,20 @@
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -43,10 +46,10 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -59,14 +62,16 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
@@ -80,19 +85,17 @@
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier",
|
||||
"description",
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -101,14 +104,14 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri internal convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -144,10 +147,7 @@
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
@@ -155,24 +155,24 @@
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.\nThe configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json\n{\n \"allow\": [{ \"path\": \"$HOME/**\" }],\n \"deny\": [{ \"path\": \"$HOME/secret.txt\" }]\n}\n```",
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
@@ -257,27 +257,84 @@
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"const": "macOS"
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"const": "windows"
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"const": "linux"
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"const": "android"
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"const": "iOS"
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the disable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-disable",
|
||||
"markdownDescription": "Enables the disable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the disable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-disable",
|
||||
"markdownDescription": "Denies the disable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the enable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-enable",
|
||||
"markdownDescription": "Enables the enable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the enable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-enable",
|
||||
"markdownDescription": "Denies the enable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_enabled command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-is-enabled",
|
||||
"markdownDescription": "Enables the is_enabled command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_enabled command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-is-enabled",
|
||||
"markdownDescription": "Denies the is_enabled command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures if your\r\napplication can enable or disable auto\r\nstarting the application on boot.\r\n\r\n#### Granted Permissions\r\n\r\nIt allows all to check, enable and\r\ndisable the automatic start on boot.\r\n\r\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "This permission set configures if your\r\napplication can enable or disable auto\r\nstarting the application on boot.\r\n\r\n#### Granted Permissions\r\n\r\nIt allows all to check, enable and\r\ndisable the automatic start on boot.\r\n\r\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.4.4]
|
||||
|
||||
- [`82fbb0c7`](https://github.com/tauri-apps/plugins-workspace/commit/82fbb0c790288eca72af9ade13828ded7700ff90) ([#3221](https://github.com/tauri-apps/plugins-workspace/pull/3221)) On iOS, fixed an application crash happening when the scanner was started when user denied permission before.
|
||||
- [`b60dd887`](https://github.com/tauri-apps/plugins-workspace/commit/b60dd88702a2be2af942a3d104d728970a4c42d6) ([#3223](https://github.com/tauri-apps/plugins-workspace/pull/3223)) On iOS, start the scanning session on a separate thread to fix performance issues.
|
||||
- [`c27af912`](https://github.com/tauri-apps/plugins-workspace/commit/c27af9128d6cc7a2424ec49e98005e68b2d0eb1a) ([#3222](https://github.com/tauri-apps/plugins-workspace/pull/3222)) On iOS, fixed an application crash happening when the scanner was started on the iOS Simulator (no camera available).
|
||||
|
||||
## \[2.4.3]
|
||||
|
||||
- [`d8bfe61f`](https://github.com/tauri-apps/plugins-workspace/commit/d8bfe61f20f235314bad93a9c50d8b7f3eade734) ([#3121](https://github.com/tauri-apps/plugins-workspace/pull/3121) by [@NKIPSC](https://github.com/tauri-apps/plugins-workspace/../../NKIPSC)) Remove unnecessary checks on Android when requesting camera permission.
|
||||
- [`631d0e25`](https://github.com/tauri-apps/plugins-workspace/commit/631d0e256a37946b6a9102ca35511abfbebb92c5) ([#2440](https://github.com/tauri-apps/plugins-workspace/pull/2440) by [@kingsword09](https://github.com/tauri-apps/plugins-workspace/../../kingsword09)) Fix the `cameraView` is not removed after scanning in iOS.
|
||||
|
||||
## \[2.4.2]
|
||||
|
||||
- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-plugin-barcode-scanner"
|
||||
version = "2.4.4"
|
||||
version = "2.4.2"
|
||||
description = "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS"
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
@@ -31,4 +31,4 @@ log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
tauri = { workspace = true }
|
||||
tauri = { workspace = true, features = ["wry"] }
|
||||
|
||||
@@ -54,6 +54,7 @@ import java.util.concurrent.ExecutionException
|
||||
|
||||
private const val PERMISSION_ALIAS_CAMERA = "camera"
|
||||
private const val PERMISSION_NAME = Manifest.permission.CAMERA
|
||||
private const val PREFS_PERMISSION_FIRST_TIME_ASKING = "PREFS_PERMISSION_FIRST_TIME_ASKING"
|
||||
|
||||
@InvokeArg
|
||||
class ScanOptions {
|
||||
@@ -353,6 +354,17 @@ class BarcodeScannerPlugin(private val activity: Activity) : Plugin(activity),
|
||||
}
|
||||
}
|
||||
|
||||
private fun markFirstPermissionRequest() {
|
||||
val sharedPreference: SharedPreferences =
|
||||
activity.getSharedPreferences(PREFS_PERMISSION_FIRST_TIME_ASKING, MODE_PRIVATE)
|
||||
sharedPreference.edit().putBoolean(PERMISSION_NAME, false).apply()
|
||||
}
|
||||
|
||||
private fun firstPermissionRequest(): Boolean {
|
||||
return activity.getSharedPreferences(PREFS_PERMISSION_FIRST_TIME_ASKING, MODE_PRIVATE)
|
||||
.getBoolean(PERMISSION_NAME, true)
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@PermissionCallback
|
||||
fun cameraPermissionCallback(invoke: Invoke) {
|
||||
@@ -368,7 +380,9 @@ class BarcodeScannerPlugin(private val activity: Activity) : Plugin(activity),
|
||||
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.GRANTED)
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.DENIED)
|
||||
if (!activity.shouldShowRequestPermissionRationale(PERMISSION_NAME)) {
|
||||
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.DENIED)
|
||||
}
|
||||
} else {
|
||||
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.GRANTED)
|
||||
}
|
||||
@@ -387,12 +401,20 @@ class BarcodeScannerPlugin(private val activity: Activity) : Plugin(activity),
|
||||
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.GRANTED)
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
requestPermissionForAlias(
|
||||
PERMISSION_ALIAS_CAMERA,
|
||||
invoke,
|
||||
"cameraPermissionCallback"
|
||||
)
|
||||
return
|
||||
if (firstPermissionRequest() || activity.shouldShowRequestPermissionRationale(
|
||||
PERMISSION_NAME
|
||||
)
|
||||
) {
|
||||
markFirstPermissionRequest()
|
||||
requestPermissionForAlias(
|
||||
PERMISSION_ALIAS_CAMERA,
|
||||
invoke,
|
||||
"cameraPermissionCallback"
|
||||
)
|
||||
return
|
||||
} else {
|
||||
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.DENIED)
|
||||
}
|
||||
} else {
|
||||
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.GRANTED)
|
||||
}
|
||||
|
||||
@@ -261,34 +261,27 @@ class BarcodeScannerPlugin: Plugin, AVCaptureMetadataOutputObjectsDelegate {
|
||||
}
|
||||
|
||||
private func runScanner(_ invoke: Invoke, args: ScanOptions) {
|
||||
if getPermissionState() != "granted" {
|
||||
invoke.reject("Camera permission denied or not yet requested")
|
||||
return
|
||||
}
|
||||
|
||||
scanFormats = [AVMetadataObject.ObjectType]()
|
||||
|
||||
(args.formats ?? []).forEach { format in
|
||||
if let formatValue = format.value {
|
||||
scanFormats.append(formatValue)
|
||||
} else {
|
||||
invoke.reject("Unsupported barcode format on this iOS version: \(format)")
|
||||
return
|
||||
}
|
||||
if let formatValue = format.value {
|
||||
scanFormats.append(formatValue)
|
||||
} else {
|
||||
invoke.reject("Unsupported barcode format on this iOS version: \(format)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if scanFormats.isEmpty {
|
||||
for supportedFormat in SupportedFormat.allCases {
|
||||
if let formatValue = supportedFormat.value {
|
||||
scanFormats.append(formatValue)
|
||||
for supportedFormat in SupportedFormat.allCases {
|
||||
if let formatValue = supportedFormat.value {
|
||||
scanFormats.append(formatValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.metaOutput!.metadataObjectTypes = self.scanFormats
|
||||
DispatchQueue.main.async {
|
||||
self.captureSession!.startRunning()
|
||||
}
|
||||
self.captureSession!.startRunning()
|
||||
|
||||
self.isScanning = true
|
||||
}
|
||||
@@ -305,13 +298,6 @@ class BarcodeScannerPlugin: Plugin, AVCaptureMetadataOutputObjectsDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if camera is available on this platform (iOS simulator doesn't have cameras)
|
||||
let availableVideoDevices = discoverCaptureDevices()
|
||||
if availableVideoDevices.isEmpty {
|
||||
invoke.reject("No camera available on this device (e.g., iOS Simulator)")
|
||||
return
|
||||
}
|
||||
|
||||
var iOS14min: Bool = false
|
||||
if #available(iOS 14.0, *) { iOS14min = true }
|
||||
if !iOS14min && self.getPermissionState() != "granted" {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tauri-apps/plugin-barcode-scanner",
|
||||
"version": "2.4.4",
|
||||
"version": "2.4.2",
|
||||
"description": "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"authors": [
|
||||
@@ -25,6 +25,6 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,20 @@
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -43,10 +46,10 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -59,14 +62,16 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
@@ -80,19 +85,17 @@
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier",
|
||||
"description",
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -101,14 +104,14 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri internal convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -144,10 +147,7 @@
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
@@ -155,24 +155,24 @@
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.\nThe configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json\n{\n \"allow\": [{ \"path\": \"$HOME/**\" }],\n \"deny\": [{ \"path\": \"$HOME/secret.txt\" }]\n}\n```",
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
@@ -257,27 +257,120 @@
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"const": "macOS"
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"const": "windows"
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"const": "linux"
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"const": "android"
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"const": "iOS"
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-cancel",
|
||||
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-cancel",
|
||||
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-check-permissions",
|
||||
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-check-permissions",
|
||||
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the open_app_settings command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-open-app-settings",
|
||||
"markdownDescription": "Enables the open_app_settings command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the open_app_settings command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-open-app-settings",
|
||||
"markdownDescription": "Denies the open_app_settings command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-request-permissions",
|
||||
"markdownDescription": "Enables the request_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-request-permissions",
|
||||
"markdownDescription": "Denies the request_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the scan command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-scan",
|
||||
"markdownDescription": "Enables the scan command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the scan command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-scan",
|
||||
"markdownDescription": "Denies the scan command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the vibrate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-vibrate",
|
||||
"markdownDescription": "Enables the vibrate command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the vibrate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-vibrate",
|
||||
"markdownDescription": "Denies the vibrate command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nbarcode scanning features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all barcode related features.\n\n\n#### This default permission set includes:\n\n- `allow-cancel`\n- `allow-check-permissions`\n- `allow-open-app-settings`\n- `allow-request-permissions`\n- `allow-scan`\n- `allow-vibrate`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "This permission set configures which\nbarcode scanning features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all barcode related features.\n\n\n#### This default permission set includes:\n\n- `allow-cancel`\n- `allow-check-permissions`\n- `allow-open-app-settings`\n- `allow-request-permissions`\n- `allow-scan`\n- `allow-vibrate`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,20 @@
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -43,10 +46,10 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -59,14 +62,16 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
@@ -80,19 +85,17 @@
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier",
|
||||
"description",
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -101,14 +104,14 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri internal convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -144,10 +147,7 @@
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
@@ -155,24 +155,24 @@
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.\nThe configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json\n{\n \"allow\": [{ \"path\": \"$HOME/**\" }],\n \"deny\": [{ \"path\": \"$HOME/secret.txt\" }]\n}\n```",
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
@@ -257,27 +257,72 @@
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"const": "macOS"
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"const": "windows"
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"const": "linux"
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"const": "android"
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"const": "iOS"
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the authenticate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-authenticate",
|
||||
"markdownDescription": "Enables the authenticate command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the authenticate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-authenticate",
|
||||
"markdownDescription": "Denies the authenticate command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-status",
|
||||
"markdownDescription": "Enables the status command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-status",
|
||||
"markdownDescription": "Denies the status command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\r\nbiometric features are by default exposed.\r\n\r\n#### Granted Permissions\r\n\r\nIt allows acccess to all biometric commands.\r\n\r\n\n#### This default permission set includes:\n\n- `allow-authenticate`\n- `allow-status`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "This permission set configures which\r\nbiometric features are by default exposed.\r\n\r\n#### Granted Permissions\r\n\r\nIt allows acccess to all biometric commands.\r\n\r\n\n#### This default permission set includes:\n\n- `allow-authenticate`\n- `allow-status`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,20 @@
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -43,10 +46,10 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -59,14 +62,16 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
@@ -80,19 +85,17 @@
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier",
|
||||
"description",
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -101,14 +104,14 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri internal convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -144,10 +147,7 @@
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
@@ -155,24 +155,24 @@
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.\nThe configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json\n{\n \"allow\": [{ \"path\": \"$HOME/**\" }],\n \"deny\": [{ \"path\": \"$HOME/secret.txt\" }]\n}\n```",
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
@@ -257,27 +257,60 @@
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"const": "macOS"
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"const": "windows"
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"const": "linux"
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"const": "android"
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"const": "iOS"
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the cli_matches command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-cli-matches",
|
||||
"markdownDescription": "Enables the cli_matches command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the cli_matches command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-cli-matches",
|
||||
"markdownDescription": "Denies the cli_matches command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Allows reading the CLI matches\n#### This default permission set includes:\n\n- `allow-cli-matches`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Allows reading the CLI matches\n#### This default permission set includes:\n\n- `allow-cli-matches`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
tauri = { workspace = true }
|
||||
tauri = { workspace = true, features = ["wry"] }
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
arboard = { version = "3", features = ["wayland-data-control"] }
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## Default Permission
|
||||
|
||||
No features are enabled by default, as we believe
|
||||
the clipboard can be inherently dangerous and it is
|
||||
the clipboard can be inherently dangerous and it is
|
||||
application specific if read and/or write access is needed.
|
||||
|
||||
Clipboard interaction needs to be explicitly enabled.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[default]
|
||||
description = """
|
||||
No features are enabled by default, as we believe
|
||||
the clipboard can be inherently dangerous and it is
|
||||
the clipboard can be inherently dangerous and it is
|
||||
application specific if read and/or write access is needed.
|
||||
|
||||
Clipboard interaction needs to be explicitly enabled.
|
||||
|
||||
@@ -24,17 +24,20 @@
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -43,10 +46,10 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -59,14 +62,16 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
@@ -80,19 +85,17 @@
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier",
|
||||
"description",
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -101,14 +104,14 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri internal convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -144,10 +147,7 @@
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
@@ -155,24 +155,24 @@
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.\nThe configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json\n{\n \"allow\": [{ \"path\": \"$HOME/**\" }],\n \"deny\": [{ \"path\": \"$HOME/secret.txt\" }]\n}\n```",
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
@@ -257,27 +257,120 @@
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"const": "macOS"
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"const": "windows"
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"const": "linux"
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"const": "android"
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"const": "iOS"
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the clear command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-clear",
|
||||
"markdownDescription": "Enables the clear command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the clear command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-clear",
|
||||
"markdownDescription": "Denies the clear command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the read_image command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-read-image",
|
||||
"markdownDescription": "Enables the read_image command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the read_image command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-read-image",
|
||||
"markdownDescription": "Denies the read_image command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the read_text command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-read-text",
|
||||
"markdownDescription": "Enables the read_text command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the read_text command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-read-text",
|
||||
"markdownDescription": "Denies the read_text command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the write_html command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-write-html",
|
||||
"markdownDescription": "Enables the write_html command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the write_html command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-write-html",
|
||||
"markdownDescription": "Denies the write_html command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the write_image command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-write-image",
|
||||
"markdownDescription": "Enables the write_image command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the write_image command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-write-image",
|
||||
"markdownDescription": "Denies the write_image command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the write_text command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-write-text",
|
||||
"markdownDescription": "Enables the write_text command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the write_text command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-write-text",
|
||||
"markdownDescription": "Denies the write_text command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "No features are enabled by default, as we believe\r\nthe clipboard can be inherently dangerous and it is\r\napplication specific if read and/or write access is needed.\r\n\r\nClipboard interaction needs to be explicitly enabled.\r\n",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "No features are enabled by default, as we believe\r\nthe clipboard can be inherently dangerous and it is\r\napplication specific if read and/or write access is needed.\r\n\r\nClipboard interaction needs to be explicitly enabled.\r\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.4.7]
|
||||
|
||||
- [`8374e997`](https://github.com/tauri-apps/plugins-workspace/commit/8374e997b82c95516fc0c1f6d665d9fc3b52edf8) ([#3258](https://github.com/tauri-apps/plugins-workspace/pull/3258) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Fix runtime deep link registration failing on Linux when the app path has spaces.
|
||||
|
||||
## \[2.4.6]
|
||||
|
||||
- [`28048039`](https://github.com/tauri-apps/plugins-workspace/commit/28048039496e84b46847c008416d341f1349e30e) ([#3143](https://github.com/tauri-apps/plugins-workspace/pull/3143) by [@Tunglies](https://github.com/tauri-apps/plugins-workspace/../../Tunglies)) Fix clippy warnings. No user facing changes.
|
||||
|
||||
## \[2.4.5]
|
||||
|
||||
- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.4.7"
|
||||
version = "2.4.5"
|
||||
description = "Set your Tauri application as the default handler for an URL"
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
@@ -6,8 +6,9 @@ package app.tauri.deep_link
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.PatternMatcher
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import app.tauri.Logger
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
@@ -15,35 +16,18 @@ import app.tauri.plugin.Channel
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@InvokeArg
|
||||
class SetEventHandlerArgs {
|
||||
lateinit var handler: Channel
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class AssociatedDomain {
|
||||
var scheme: List<String> = listOf("https", "http")
|
||||
var host: String? = null
|
||||
var path: List<String> = listOf()
|
||||
var pathPattern: List<String> = listOf()
|
||||
var pathPrefix: List<String> = listOf()
|
||||
var pathSuffix: List<String> = listOf()
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class PluginConfig {
|
||||
var mobile: List<AssociatedDomain> = listOf()
|
||||
}
|
||||
|
||||
@TauriPlugin
|
||||
class DeepLinkPlugin(private val activity: Activity): Plugin(activity) {
|
||||
//private val implementation = Example()
|
||||
private var webView: WebView? = null
|
||||
private var currentUrl: String? = null
|
||||
private var channel: Channel? = null
|
||||
private var config: PluginConfig? = null
|
||||
|
||||
companion object {
|
||||
var instance: DeepLinkPlugin? = null
|
||||
@@ -67,105 +51,27 @@ class DeepLinkPlugin(private val activity: Activity): Plugin(activity) {
|
||||
|
||||
override fun load(webView: WebView) {
|
||||
instance = this
|
||||
config = getConfig(PluginConfig::class.java)
|
||||
|
||||
super.load(webView)
|
||||
this.webView = webView
|
||||
|
||||
val intent = activity.intent
|
||||
|
||||
if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
|
||||
val url = intent.data.toString()
|
||||
if (isDeepLink(url)) {
|
||||
// TODO: check if it makes sense to split up init url and last url
|
||||
this.currentUrl = url
|
||||
val event = JSObject()
|
||||
event.put("url", this.currentUrl)
|
||||
this.channel?.send(event)
|
||||
}
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
// TODO: check if it makes sense to split up init url and last url
|
||||
this.currentUrl = intent.data.toString()
|
||||
val event = JSObject()
|
||||
event.put("url", this.currentUrl)
|
||||
this.channel?.send(event)
|
||||
}
|
||||
|
||||
super.load(webView)
|
||||
this.webView = webView
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
|
||||
val url = intent.data.toString()
|
||||
if (isDeepLink(url)) {
|
||||
this.currentUrl = url
|
||||
val event = JSObject()
|
||||
event.put("url", this.currentUrl)
|
||||
this.channel?.send(event)
|
||||
}
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
this.currentUrl = intent.data.toString()
|
||||
val event = JSObject()
|
||||
event.put("url", this.currentUrl)
|
||||
this.channel?.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDeepLink(url: String): Boolean {
|
||||
val config = this.config ?: return false
|
||||
|
||||
if (config.mobile.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val uri = try {
|
||||
url.toUri()
|
||||
} catch (_: Exception) {
|
||||
// not a URL
|
||||
return false
|
||||
}
|
||||
|
||||
val scheme = uri.scheme ?: return false
|
||||
val host = uri.host
|
||||
val path = uri.path ?: ""
|
||||
|
||||
// Check if URL matches any configured mobile deep link
|
||||
for (domain in config.mobile) {
|
||||
// Check scheme
|
||||
if (!domain.scheme.any { it.equals(scheme, ignoreCase = true) }) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check host (if configured)
|
||||
if (domain.host != null) {
|
||||
if (!host.equals(domain.host, ignoreCase = true)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check path constraints
|
||||
// According to Android docs:
|
||||
// - path: exact match, must begin with /
|
||||
// - pathPrefix: matches initial part of path
|
||||
// - pathSuffix: matches ending part, doesn't need to begin with /
|
||||
// - pathPattern: simple glob pattern (., *, .*)
|
||||
val pathMatches = when {
|
||||
// Exact path match (must begin with /)
|
||||
domain.path.isNotEmpty() && domain.path.any { it == path } -> true
|
||||
// Path pattern match (simple glob: ., *, .*)
|
||||
domain.pathPattern.isNotEmpty() && domain.pathPattern.any { pattern ->
|
||||
try {
|
||||
PatternMatcher(pattern, PatternMatcher.PATTERN_SIMPLE_GLOB).match(path)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} -> true
|
||||
// Path prefix match
|
||||
domain.pathPrefix.isNotEmpty() && domain.pathPrefix.any { prefix ->
|
||||
path.startsWith(prefix)
|
||||
} -> true
|
||||
// Path suffix match
|
||||
domain.pathSuffix.isNotEmpty() && domain.pathSuffix.any { suffix ->
|
||||
path.endsWith(suffix)
|
||||
} -> true
|
||||
// If no path constraints, any path is allowed
|
||||
domain.path.isEmpty() && domain.pathPattern.isEmpty() &&
|
||||
domain.pathPrefix.isEmpty() && domain.pathSuffix.isEmpty() -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
if (pathMatches) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,13 @@ fn main() {
|
||||
let deep_link_domains = config
|
||||
.mobile
|
||||
.iter()
|
||||
.filter(|domain| domain.is_app_link())
|
||||
.filter_map(|domain| {
|
||||
if domain.is_app_link() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(domain)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if deep_link_domains.is_empty() {
|
||||
@@ -171,7 +177,7 @@ fn main() {
|
||||
);
|
||||
dict.insert(
|
||||
"CFBundleURLName".into(),
|
||||
domain.scheme[0].clone().into(),
|
||||
format!("{}", domain.scheme[0]).into(),
|
||||
);
|
||||
plist::Value::Dictionary(dict)
|
||||
})
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.2.10]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `deep-link-js@2.4.7`
|
||||
|
||||
## \[2.2.9]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `deep-link-js@2.4.6`
|
||||
|
||||
## \[2.2.8]
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "deep-link-example",
|
||||
"private": true,
|
||||
"version": "2.2.10",
|
||||
"version": "2.2.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -10,12 +10,12 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-deep-link": "2.4.7"
|
||||
"@tauri-apps/api": "2.9.0",
|
||||
"@tauri-apps/plugin-deep-link": "2.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.10.1",
|
||||
"@tauri-apps/cli": "2.9.4",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^7.3.1"
|
||||
"vite": "^7.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,7 @@ tauri-build = { workspace = true }
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
tauri-runtime-wry = { workspace = true, optional = true }
|
||||
tauri-runtime-cef = { workspace = true, optional = true }
|
||||
tauri = { workspace = true, features = ["wry", "common-controls-v6", "x11"] }
|
||||
tauri-plugin-deep-link = { path = "../../../" }
|
||||
tauri-plugin-log = { path = "../../../../log" }
|
||||
tauri-plugin-single-instance = { path = "../../../../single-instance", features = [
|
||||
@@ -30,9 +28,6 @@ tauri-plugin-single-instance = { path = "../../../../single-instance", features
|
||||
log = "0.4"
|
||||
|
||||
[features]
|
||||
default = ["wry"]
|
||||
wry = ["dep:tauri-runtime-wry"]
|
||||
cef = ["dep:tauri-runtime-cef"]
|
||||
# this feature is used for production builds or when `devUrl` points to the filesystem and the built-in dev server is disabled.
|
||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||
# DO NOT REMOVE!!
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(feature = "cef")]
|
||||
type TauriRuntime = tauri_runtime_cef::CefRuntime<tauri::EventLoopMessage>;
|
||||
#[cfg(feature = "wry")]
|
||||
type TauriRuntime = tauri_runtime_wry::Wry<tauri::EventLoopMessage>;
|
||||
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
@@ -18,7 +13,7 @@ fn greet(name: &str) -> String {
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::<TauriRuntime>::new();
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tauri-apps/plugin-deep-link",
|
||||
"version": "2.4.7",
|
||||
"version": "2.4.5",
|
||||
"description": "Set your Tauri application as the default handler for an URL",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"authors": [
|
||||
@@ -25,9 +25,9 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.10.1"
|
||||
"@tauri-apps/cli": "2.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,20 @@
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -43,10 +46,10 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -59,14 +62,16 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
@@ -80,19 +85,17 @@
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier",
|
||||
"description",
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -101,14 +104,14 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri internal convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -144,10 +147,7 @@
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
@@ -155,24 +155,24 @@
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.\nThe configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json\n{\n \"allow\": [{ \"path\": \"$HOME/**\" }],\n \"deny\": [{ \"path\": \"$HOME/secret.txt\" }]\n}\n```",
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
@@ -257,27 +257,96 @@
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"const": "macOS"
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"const": "windows"
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"const": "linux"
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"const": "android"
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"const": "iOS"
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the get_current command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-current",
|
||||
"markdownDescription": "Enables the get_current command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_current command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-current",
|
||||
"markdownDescription": "Denies the get_current command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_registered command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-is-registered",
|
||||
"markdownDescription": "Enables the is_registered command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_registered command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-is-registered",
|
||||
"markdownDescription": "Denies the is_registered command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-register",
|
||||
"markdownDescription": "Enables the register command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-register",
|
||||
"markdownDescription": "Denies the register command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the unregister command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-unregister",
|
||||
"markdownDescription": "Enables the unregister command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the unregister command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-unregister",
|
||||
"markdownDescription": "Denies the unregister command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Allows reading the opened deep link via the get_current command\n#### This default permission set includes:\n\n- `allow-get-current`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Allows reading the opened deep link via the get_current command\n#### This default permission set includes:\n\n- `allow-get-current`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ mod imp {
|
||||
.unwrap_or_else(|| bin.into_os_string())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let qualified_exec = format!("\"{}\" %u", exec);
|
||||
let qualified_exec = format!("{} %u", exec);
|
||||
|
||||
let target = self.app.path().data_dir()?.join("applications");
|
||||
|
||||
@@ -540,7 +540,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
||||
Ok(())
|
||||
})
|
||||
.on_event(|_app, _event| {
|
||||
#[cfg(desktop)]
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
if let tauri::RunEvent::Opened { urls } = _event {
|
||||
use tauri::Emitter;
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.6.0]
|
||||
|
||||
- [`d7a0bb32`](https://github.com/tauri-apps/plugins-workspace/commit/d7a0bb325dad919d6cc132eb3898c33540de77c4) ([#3136](https://github.com/tauri-apps/plugins-workspace/pull/3136) by [@onehumandev](https://github.com/tauri-apps/plugins-workspace/../../onehumandev)) Add `fileAccessMode` option to file picker.
|
||||
|
||||
## \[2.5.0]
|
||||
|
||||
- [`dff6fa98`](https://github.com/tauri-apps/plugins-workspace/commit/dff6fa986a9a05ba98b6ca660fea78ae97251fc2) ([#3034](https://github.com/tauri-apps/plugins-workspace/pull/3034) by [@onehumandev](https://github.com/tauri-apps/plugins-workspace/../../onehumandev)) Add `pickerMode` option to file picker (currently only used on iOS)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `fs-js@2.4.5`
|
||||
|
||||
### feat
|
||||
|
||||
- [`c23fa03f`](https://github.com/tauri-apps/plugins-workspace/commit/c23fa03f07d5c1c220bcf0bca482364513e3f754) ([#3098](https://github.com/tauri-apps/plugins-workspace/pull/3098) by [@Lepidopteran](https://github.com/tauri-apps/plugins-workspace/../../Lepidopteran)) Add `xdg-portal` as an optional feature for `rfd`
|
||||
|
||||
## \[2.4.2]
|
||||
|
||||
- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.6.0"
|
||||
version = "2.4.2"
|
||||
description = "Native system dialogs for opening and saving files along with message dialogs on your Tauri application."
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
@@ -11,7 +11,7 @@ links = "tauri-plugin-dialog"
|
||||
|
||||
[features]
|
||||
default = ["gtk3"]
|
||||
xdg-portal = ["rfd/xdg-portal", "rfd/tokio", "rfd/wayland"]
|
||||
xdg-portal = ["rfd/xdg-portal"]
|
||||
gtk3 = ["rfd/gtk3"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
@@ -28,7 +28,7 @@ ios = { level = "partial", notes = "Does not support folder picker" }
|
||||
tauri-plugin = { workspace = true, features = ["build"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tauri = { workspace = true }
|
||||
tauri = { workspace = true, features = ["wry"] }
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
@@ -37,13 +37,14 @@ tauri = { workspace = true }
|
||||
log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
tauri-plugin-fs = { path = "../fs", version = "2.4.5" }
|
||||
tauri-plugin-fs = { path = "../fs", version = "2.4.4" }
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
tauri = { workspace = true }
|
||||
tauri = { workspace = true, features = ["wry"] }
|
||||
|
||||
[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.16", default-features = false, features = [
|
||||
rfd = { version = "0.15", default-features = false, features = [
|
||||
"tokio",
|
||||
"common-controls-v6",
|
||||
] }
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_DIALOG__=function(n){"use strict";async function e(n,e={},t){return window.__TAURI_INTERNALS__.invoke(n,e,t)}function t(n){if(void 0!==n)return"string"==typeof n?n:"ok"in n&&"cancel"in n?{OkCancelCustom:[n.ok,n.cancel]}:"yes"in n&&"no"in n&&"cancel"in n?{YesNoCancelCustom:[n.yes,n.no,n.cancel]}:"ok"in n?{OkCustom:n.ok}:void 0}async function o(n,o){return await e("plugin:dialog|message",{message:n,title:o?.title,kind:o?.kind,buttons:t(o?.buttons)})}return"function"==typeof SuppressedError&&SuppressedError,n.ask=async function(n,e){const t="string"==typeof e?{title:e}:e,i=t?.okLabel||t?.cancelLabel,a=t?.okLabel??"Yes";return await o(n,{title:t?.title,kind:t?.kind,buttons:i?{ok:a,cancel:t.cancelLabel??"No"}:"YesNo"})===a},n.confirm=async function(n,e){const t="string"==typeof e?{title:e}:e,i=t?.okLabel||t?.cancelLabel,a=t?.okLabel??"Ok";return await o(n,{title:t?.title,kind:t?.kind,buttons:i?{ok:a,cancel:t.cancelLabel??"Cancel"}:"OkCancel"})===a},n.message=async function(n,e){const t="string"==typeof e?{title:e}:e;return t&&!t.buttons&&t.okLabel&&(t.buttons={ok:t.okLabel}),o(n,t)},n.open=async function(n={}){return"object"==typeof n&&Object.freeze(n),await e("plugin:dialog|open",{options:n})},n.save=async function(n={}){return"object"==typeof n&&Object.freeze(n),await e("plugin:dialog|save",{options:n})},n}({});Object.defineProperty(window.__TAURI__,"dialog",{value:__TAURI_PLUGIN_DIALOG__})}
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_DIALOG__=function(t){"use strict";async function n(t,n={},e){return window.__TAURI_INTERNALS__.invoke(t,n,e)}function e(t){if(void 0!==t)return"string"==typeof t?t:"ok"in t&&"cancel"in t?{OkCancelCustom:[t.ok,t.cancel]}:"yes"in t&&"no"in t&&"cancel"in t?{YesNoCancelCustom:[t.yes,t.no,t.cancel]}:"ok"in t?{OkCustom:t.ok}:void 0}return"function"==typeof SuppressedError&&SuppressedError,t.ask=async function(t,e){const o="string"==typeof e?{title:e}:e;return await n("plugin:dialog|ask",{message:t.toString(),title:o?.title?.toString(),kind:o?.kind,yesButtonLabel:o?.okLabel?.toString(),noButtonLabel:o?.cancelLabel?.toString()})},t.confirm=async function(t,e){const o="string"==typeof e?{title:e}:e;return await n("plugin:dialog|confirm",{message:t.toString(),title:o?.title?.toString(),kind:o?.kind,okButtonLabel:o?.okLabel?.toString(),cancelButtonLabel:o?.cancelLabel?.toString()})},t.message=async function(t,o){const i="string"==typeof o?{title:o}:o;return n("plugin:dialog|message",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString(),buttons:e(i?.buttons)})},t.open=async function(t={}){return"object"==typeof t&&Object.freeze(t),await n("plugin:dialog|open",{options:t})},t.save=async function(t={}){return"object"==typeof t&&Object.freeze(t),await n("plugin:dialog|save",{options:t})},t}({});Object.defineProperty(window.__TAURI__,"dialog",{value:__TAURI_PLUGIN_DIALOG__})}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const COMMANDS: &[&str] = &["open", "save", "message"];
|
||||
const COMMANDS: &[&str] = &["open", "save", "message", "ask", "confirm"];
|
||||
|
||||
fn main() {
|
||||
let result = tauri_plugin::Builder::new(COMMANDS)
|
||||
|
||||
@@ -76,31 +76,6 @@ interface OpenDialogOptions {
|
||||
* On desktop, this option is ignored.
|
||||
*/
|
||||
pickerMode?: PickerMode
|
||||
/**
|
||||
* The file access mode of the dialog.
|
||||
* If not provided, `copy` is used, which matches the behavior of the {@linkcode open} method before the introduction of this option.
|
||||
*
|
||||
* **Usage**
|
||||
* If a file is opened with {@linkcode fileAccessMode: 'copy'}, it will be copied to the app's sandbox.
|
||||
* This means the file can be read, edited, deleted, copied, or any other operation without any issues, since the file
|
||||
* now belongs to the app.
|
||||
* This also means that the caller has responsibility of deleting the file if this file is not meant to be retained
|
||||
* in the app sandbox.
|
||||
*
|
||||
* If a file is opened with {@linkcode fileAccessMode: 'scoped'}, the file will remain in its original location
|
||||
* and security-scoped access will be automatically managed by the system.
|
||||
*
|
||||
* **Note**
|
||||
* This is specifically meant for document pickers on iOS or MacOS, in conjunction with [security scoped resources](https://developer.apple.com/documentation/foundation/nsurl/startaccessingsecurityscopedresource()).
|
||||
*
|
||||
* Why only document pickers, and not image or video pickers?
|
||||
* The image and video pickers on iOS behave differently from the document pickers, and return [NSItemProvider](https://developer.apple.com/documentation/foundation/nsitemprovider) objects instead of file URLs.
|
||||
* These are meant to be ephemeral (only available within the callback of the picker), and are not accessible outside of the callback.
|
||||
* So for image and video pickers, the only way to access the file is to copy it to the app's sandbox, and this is the URL that is returned from this API.
|
||||
* This means there is no provision for using `scoped` mode with image or video pickers.
|
||||
* If an image or video picker is used, `copy` is always used.
|
||||
*/
|
||||
fileAccessMode?: FileAccessMode
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,16 +111,6 @@ interface SaveDialogOptions {
|
||||
*/
|
||||
export type PickerMode = 'document' | 'media' | 'image' | 'video'
|
||||
|
||||
/**
|
||||
* The file access mode of the dialog.
|
||||
*
|
||||
* - `copy`: copy/move the picked file to the app sandbox; no scoped access required.
|
||||
* - `scoped`: keep file in place; security-scoped access is automatically managed.
|
||||
*
|
||||
* **Note:** This option is only supported on iOS 14 and above. This parameter is ignored on iOS 13 and below.
|
||||
*/
|
||||
export type FileAccessMode = 'copy' | 'scoped'
|
||||
|
||||
/**
|
||||
* Default buttons for a message dialog.
|
||||
*
|
||||
@@ -405,18 +370,6 @@ async function save(options: SaveDialogOptions = {}): Promise<string | null> {
|
||||
*/
|
||||
export type MessageDialogResult = 'Yes' | 'No' | 'Ok' | 'Cancel' | (string & {})
|
||||
|
||||
async function messageCommand(
|
||||
message: string,
|
||||
options?: Omit<MessageDialogOptions, 'okLabel'>
|
||||
) {
|
||||
return await invoke<MessageDialogResult>('plugin:dialog|message', {
|
||||
message,
|
||||
title: options?.title,
|
||||
kind: options?.kind,
|
||||
buttons: buttonsToRust(options?.buttons)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message dialog with an `Ok` button.
|
||||
* @example
|
||||
@@ -439,17 +392,18 @@ async function message(
|
||||
options?: string | MessageDialogOptions
|
||||
): Promise<MessageDialogResult> {
|
||||
const opts = typeof options === 'string' ? { title: options } : options
|
||||
if (opts && !opts.buttons && opts.okLabel) {
|
||||
opts.buttons = { ok: opts.okLabel }
|
||||
}
|
||||
return messageCommand(message, opts)
|
||||
|
||||
return invoke<MessageDialogResult>('plugin:dialog|message', {
|
||||
message: message.toString(),
|
||||
title: opts?.title?.toString(),
|
||||
kind: opts?.kind,
|
||||
okButtonLabel: opts?.okLabel?.toString(),
|
||||
buttons: buttonsToRust(opts?.buttons)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a question dialog with `Yes` and `No` buttons.
|
||||
*
|
||||
* Convenient wrapper for `await message('msg', { buttons: 'YesNo' }) === 'Yes'`
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { ask } from '@tauri-apps/plugin-dialog';
|
||||
@@ -469,24 +423,17 @@ async function ask(
|
||||
options?: string | ConfirmDialogOptions
|
||||
): Promise<boolean> {
|
||||
const opts = typeof options === 'string' ? { title: options } : options
|
||||
const customButtons = opts?.okLabel || opts?.cancelLabel
|
||||
const okLabel = opts?.okLabel ?? 'Yes'
|
||||
return (
|
||||
(await messageCommand(message, {
|
||||
title: opts?.title,
|
||||
kind: opts?.kind,
|
||||
buttons: customButtons
|
||||
? { ok: okLabel, cancel: opts.cancelLabel ?? 'No' }
|
||||
: 'YesNo'
|
||||
})) === okLabel
|
||||
)
|
||||
return await invoke('plugin:dialog|ask', {
|
||||
message: message.toString(),
|
||||
title: opts?.title?.toString(),
|
||||
kind: opts?.kind,
|
||||
yesButtonLabel: opts?.okLabel?.toString(),
|
||||
noButtonLabel: opts?.cancelLabel?.toString()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a question dialog with `Ok` and `Cancel` buttons.
|
||||
*
|
||||
* Convenient wrapper for `await message('msg', { buttons: 'OkCancel' }) === 'Ok'`
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { confirm } from '@tauri-apps/plugin-dialog';
|
||||
@@ -506,17 +453,13 @@ async function confirm(
|
||||
options?: string | ConfirmDialogOptions
|
||||
): Promise<boolean> {
|
||||
const opts = typeof options === 'string' ? { title: options } : options
|
||||
const customButtons = opts?.okLabel || opts?.cancelLabel
|
||||
const okLabel = opts?.okLabel ?? 'Ok'
|
||||
return (
|
||||
(await messageCommand(message, {
|
||||
title: opts?.title,
|
||||
kind: opts?.kind,
|
||||
buttons: customButtons
|
||||
? { ok: okLabel, cancel: opts.cancelLabel ?? 'Cancel' }
|
||||
: 'OkCancel'
|
||||
})) === okLabel
|
||||
)
|
||||
return await invoke('plugin:dialog|confirm', {
|
||||
message: message.toString(),
|
||||
title: opts?.title?.toString(),
|
||||
kind: opts?.kind,
|
||||
okButtonLabel: opts?.okLabel?.toString(),
|
||||
cancelButtonLabel: opts?.cancelLabel?.toString()
|
||||
})
|
||||
}
|
||||
|
||||
export type {
|
||||
|
||||
@@ -34,7 +34,6 @@ struct FilePickerOptions: Decodable {
|
||||
var filters: [Filter]?
|
||||
var defaultPath: String?
|
||||
var pickerMode: PickerMode?
|
||||
var fileAccessMode: FileAccessMode?
|
||||
}
|
||||
|
||||
struct SaveFileDialogOptions: Decodable {
|
||||
@@ -42,11 +41,6 @@ struct SaveFileDialogOptions: Decodable {
|
||||
var defaultPath: String?
|
||||
}
|
||||
|
||||
enum FileAccessMode: String, Decodable {
|
||||
case copy
|
||||
case scoped
|
||||
}
|
||||
|
||||
enum PickerMode: String, Decodable {
|
||||
case document
|
||||
case media
|
||||
@@ -62,7 +56,6 @@ class DialogPlugin: Plugin {
|
||||
override init() {
|
||||
super.init()
|
||||
filePickerController = FilePickerController(self)
|
||||
|
||||
}
|
||||
|
||||
@objc public func showFilePicker(_ invoke: Invoke) throws {
|
||||
@@ -77,13 +70,12 @@ class DialogPlugin: Plugin {
|
||||
case .error(let error):
|
||||
invoke.reject(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 14, *) {
|
||||
let parsedTypes = parseFiltersOption(args.filters ?? [])
|
||||
|
||||
let mimeKinds = Set(
|
||||
parsedTypes.compactMap { $0.preferredMIMEType?.components(separatedBy: "/")[0] })
|
||||
let mimeKinds = Set(parsedTypes.compactMap { $0.preferredMIMEType?.components(separatedBy: "/")[0] })
|
||||
let filtersIncludeImage = mimeKinds.contains("image")
|
||||
let filtersIncludeVideo = mimeKinds.contains("video")
|
||||
let filtersIncludeNonMedia = mimeKinds.contains(where: { $0 != "image" && $0 != "video" })
|
||||
@@ -93,8 +85,7 @@ class DialogPlugin: Plugin {
|
||||
if args.pickerMode == .media
|
||||
|| args.pickerMode == .image
|
||||
|| args.pickerMode == .video
|
||||
|| (!filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo))
|
||||
{
|
||||
|| (!filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo)) {
|
||||
DispatchQueue.main.async {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||
configuration.selectionLimit = (args.multiple ?? false) ? 0 : 1
|
||||
@@ -116,10 +107,8 @@ class DialogPlugin: Plugin {
|
||||
DispatchQueue.main.async {
|
||||
// The UTType.item is the catch-all, allowing for any file type to be selected.
|
||||
let contentTypes = parsedTypes.isEmpty ? [UTType.item] : parsedTypes
|
||||
let picker: UIDocumentPickerViewController = UIDocumentPickerViewController(
|
||||
forOpeningContentTypes: contentTypes,
|
||||
asCopy: args.fileAccessMode == .scoped ? false : true)
|
||||
|
||||
let picker: UIDocumentPickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: contentTypes, asCopy: true)
|
||||
|
||||
if let defaultPath = args.defaultPath {
|
||||
picker.directoryURL = URL(string: defaultPath)
|
||||
}
|
||||
@@ -192,7 +181,7 @@ class DialogPlugin: Plugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return parsedTypes
|
||||
}
|
||||
|
||||
@@ -214,14 +203,14 @@ class DialogPlugin: Plugin {
|
||||
if !filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo) {
|
||||
DispatchQueue.main.async {
|
||||
let picker = UIImagePickerController()
|
||||
picker.delegate = self.filePickerController
|
||||
picker.delegate = self.filePickerController
|
||||
|
||||
if filtersIncludeImage && !filtersIncludeVideo {
|
||||
picker.sourceType = .photoLibrary
|
||||
}
|
||||
if filtersIncludeImage && !filtersIncludeVideo {
|
||||
picker.sourceType = .photoLibrary
|
||||
}
|
||||
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
} else {
|
||||
let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes
|
||||
@@ -245,8 +234,7 @@ class DialogPlugin: Plugin {
|
||||
for filter in filters {
|
||||
for ext in filter.extensions ?? [] {
|
||||
guard
|
||||
let utType: String = UTTypeCreatePreferredIdentifierForTag(
|
||||
kUTTagClassMIMEType, ext as CFString, nil)?.takeRetainedValue() as String?
|
||||
let utType: String = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, ext as CFString, nil)?.takeRetainedValue() as String?
|
||||
else {
|
||||
continue
|
||||
}
|
||||
@@ -304,7 +292,6 @@ class DialogPlugin: Plugin {
|
||||
manager.viewController?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_dialog")
|
||||
|
||||
@@ -95,33 +95,16 @@ public class FilePickerController: NSObject {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// ## In which cases do we need to save a copy of a file selected by a user to the app sandbox?
|
||||
/// In short, only when the file is **not** selected using UIDocumentPickerDelegate.
|
||||
/// For the rest of the cases, we need to write a copy of the file to the app sandbox.
|
||||
///
|
||||
/// For PHPicker (used for photos and videos), `NSItemProvider.loadFileRepresentation` returns a temporary file URL that is deleted after the completion handler.
|
||||
/// The recommendation is to [Persist](https://developer.apple.com/documentation/foundation/nsitemprovider/2888338-loadfilerepresentation) the file by moving/copying
|
||||
/// it to your app’s directory within the completion handler.
|
||||
///
|
||||
/// If available, `loadInPlaceFileRepresentation` can open a file in place; Photos assets typically do not support true in-place access,
|
||||
/// so fall back to persisting a local file.
|
||||
/// Ref: https://developer.apple.com/documentation/foundation/nsitemprovider/2888335-loadinplacefilerepresentation
|
||||
///
|
||||
/// For UIDocumentPicker, prefer "open in place" and avoid copying when possible.
|
||||
/// Ref: https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller
|
||||
private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -136,7 +119,8 @@ public class FilePickerController: NSObject {
|
||||
extension FilePickerController: UIDocumentPickerDelegate {
|
||||
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
do {
|
||||
self.plugin.onFilePickerEvent(.selected(urls))
|
||||
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"))
|
||||
}
|
||||
@@ -207,8 +191,6 @@ extension FilePickerController: PHPickerViewControllerDelegate {
|
||||
return
|
||||
}
|
||||
do {
|
||||
// We have to make a copy of the file to the app sandbox here, as PHPicker returns an NSItemProvider with either an ephemeral file URL or content that is deleted after the completion handler.
|
||||
// This is a different behavior from UIDocumentPicker, where the file can either be copied to the app sandbox or opened in place, and then accessed with `startAccessingSecurityScopedResource`.
|
||||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||||
temporaryUrls.append(temporaryUrl)
|
||||
} catch {
|
||||
@@ -230,8 +212,6 @@ extension FilePickerController: PHPickerViewControllerDelegate {
|
||||
return
|
||||
}
|
||||
do {
|
||||
// We have to make a copy of the file to the app sandbox here, as PHPicker returns an NSItemProvider with either an ephemeral file URL or content that is deleted after the completion handler.
|
||||
// This is a different behavior from UIDocumentPicker, where the file can either be copied to the app sandbox or opened in place, and then accessed with `startAccessingSecurityScopedResource`.
|
||||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||||
temporaryUrls.append(temporaryUrl)
|
||||
} catch {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tauri-apps/plugin-dialog",
|
||||
"version": "2.6.0",
|
||||
"version": "2.4.2",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"authors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
@@ -24,6 +24,6 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"$schema" = "schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-ask"
|
||||
description = "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||
commands.allow = ["message"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-ask"
|
||||
description = "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||
commands.deny = ["message"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-ask"
|
||||
description = "Enables the ask command without any pre-configured scope."
|
||||
commands.allow = ["ask"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-ask"
|
||||
description = "Denies the ask command without any pre-configured scope."
|
||||
commands.deny = ["ask"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-confirm"
|
||||
description = "Enables the confirm command without any pre-configured scope."
|
||||
commands.allow = ["confirm"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-confirm"
|
||||
description = "Denies the confirm command without any pre-configured scope."
|
||||
commands.deny = ["confirm"]
|
||||
@@ -9,6 +9,8 @@ All dialog types are enabled.
|
||||
|
||||
#### This default permission set includes the following:
|
||||
|
||||
- `allow-ask`
|
||||
- `allow-confirm`
|
||||
- `allow-message`
|
||||
- `allow-save`
|
||||
- `allow-open`
|
||||
@@ -30,7 +32,7 @@ All dialog types are enabled.
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)
|
||||
Enables the ask command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -43,7 +45,33 @@ Enables the ask command without any pre-configured scope. (**DEPRECATED**: This
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)
|
||||
Denies the ask command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`dialog:allow-confirm`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the confirm command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`dialog:deny-confirm`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the confirm command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -123,32 +151,6 @@ Enables the save command without any pre-configured scope.
|
||||
|
||||
Denies the save command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`dialog:allow-confirm`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`dialog:deny-confirm`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"$schema" = "schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-confirm"
|
||||
description = "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||
commands.allow = ["message"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-confirm"
|
||||
description = "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||
commands.deny = ["message"]
|
||||
@@ -11,4 +11,10 @@ All dialog types are enabled.
|
||||
|
||||
|
||||
"""
|
||||
permissions = ["allow-message", "allow-save", "allow-open"]
|
||||
permissions = [
|
||||
"allow-ask",
|
||||
"allow-confirm",
|
||||
"allow-message",
|
||||
"allow-save",
|
||||
"allow-open",
|
||||
]
|
||||
|
||||
@@ -24,17 +24,20 @@
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -43,10 +46,10 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -59,14 +62,16 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
@@ -80,19 +85,17 @@
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier",
|
||||
"description",
|
||||
"permissions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
@@ -101,14 +104,14 @@
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.\nTauri internal convention is to use `<h4>` headings in markdown content\nfor Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -144,10 +147,7 @@
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identifier"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
@@ -155,24 +155,24 @@
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.\nThe configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json\n{\n \"allow\": [{ \"path\": \"$HOME/**\" }],\n \"deny\": [{ \"path\": \"$HOME/secret.txt\" }]\n}\n```",
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
@@ -257,27 +257,108 @@
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"const": "macOS"
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"const": "windows"
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"const": "linux"
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"const": "android"
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"const": "iOS"
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the ask command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-ask",
|
||||
"markdownDescription": "Enables the ask command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the ask command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-ask",
|
||||
"markdownDescription": "Denies the ask command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the confirm command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-confirm",
|
||||
"markdownDescription": "Enables the confirm command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the confirm command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-confirm",
|
||||
"markdownDescription": "Denies the confirm command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the message command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-message",
|
||||
"markdownDescription": "Enables the message command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the message command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-message",
|
||||
"markdownDescription": "Denies the message command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the open command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-open",
|
||||
"markdownDescription": "Enables the open command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the open command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-open",
|
||||
"markdownDescription": "Denies the open command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the save command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-save",
|
||||
"markdownDescription": "Enables the save command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the save command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-save",
|
||||
"markdownDescription": "Denies the save command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ use tauri::{command, Manager, Runtime, State, Window};
|
||||
use tauri_plugin_fs::FsExt;
|
||||
|
||||
use crate::{
|
||||
Dialog, FileAccessMode, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind,
|
||||
MessageDialogResult, PickerMode, Result,
|
||||
Dialog, FileDialogBuilder, FilePath, MessageDialogBuilder, MessageDialogButtons,
|
||||
MessageDialogKind, MessageDialogResult, PickerMode, Result, CANCEL, NO, OK, YES,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -63,10 +63,6 @@ pub struct OpenDialogOptions {
|
||||
#[serde(default)]
|
||||
#[cfg_attr(mobile, allow(dead_code))]
|
||||
picker_mode: Option<PickerMode>,
|
||||
/// The file access mode of the dialog.
|
||||
#[serde(default)]
|
||||
#[cfg_attr(mobile, allow(dead_code))]
|
||||
file_access_mode: Option<FileAccessMode>,
|
||||
}
|
||||
|
||||
/// The options for the save dialog API.
|
||||
@@ -145,9 +141,6 @@ pub(crate) async fn open<R: Runtime>(
|
||||
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
|
||||
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
|
||||
}
|
||||
if let Some(file_access_mode) = options.file_access_mode {
|
||||
dialog_builder = dialog_builder.set_file_access_mode(file_access_mode);
|
||||
}
|
||||
|
||||
let res = if options.directory {
|
||||
#[cfg(desktop)]
|
||||
@@ -258,20 +251,17 @@ pub(crate) async fn save<R: Runtime>(
|
||||
Ok(path.map(|p| p.simplified()))
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn message<R: Runtime>(
|
||||
#[allow(unused)] window: Window<R>,
|
||||
fn message_dialog<R: Runtime>(
|
||||
#[allow(unused_variables)] window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
kind: Option<MessageDialogKind>,
|
||||
buttons: Option<MessageDialogButtons>,
|
||||
) -> Result<MessageDialogResult> {
|
||||
buttons: MessageDialogButtons,
|
||||
) -> MessageDialogBuilder<R> {
|
||||
let mut builder = dialog.message(message);
|
||||
|
||||
if let Some(buttons) = buttons {
|
||||
builder = builder.buttons(buttons);
|
||||
}
|
||||
builder = builder.buttons(buttons);
|
||||
|
||||
if let Some(title) = title {
|
||||
builder = builder.title(title);
|
||||
@@ -286,5 +276,86 @@ pub(crate) async fn message<R: Runtime>(
|
||||
builder = builder.kind(kind);
|
||||
}
|
||||
|
||||
Ok(builder.blocking_show_with_result())
|
||||
builder
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn message<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
kind: Option<MessageDialogKind>,
|
||||
ok_button_label: Option<String>,
|
||||
buttons: Option<MessageDialogButtons>,
|
||||
) -> Result<MessageDialogResult> {
|
||||
let buttons = buttons.unwrap_or(if let Some(ok_button_label) = ok_button_label {
|
||||
MessageDialogButtons::OkCustom(ok_button_label)
|
||||
} else {
|
||||
MessageDialogButtons::Ok
|
||||
});
|
||||
|
||||
Ok(message_dialog(window, dialog, title, message, kind, buttons).blocking_show_with_result())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn ask<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
kind: Option<MessageDialogKind>,
|
||||
yes_button_label: Option<String>,
|
||||
no_button_label: Option<String>,
|
||||
) -> Result<bool> {
|
||||
let dialog = message_dialog(
|
||||
window,
|
||||
dialog,
|
||||
title,
|
||||
message,
|
||||
kind,
|
||||
if let Some(yes_button_label) = yes_button_label {
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
yes_button_label,
|
||||
no_button_label.unwrap_or(NO.to_string()),
|
||||
)
|
||||
} else if let Some(no_button_label) = no_button_label {
|
||||
MessageDialogButtons::OkCancelCustom(YES.to_string(), no_button_label)
|
||||
} else {
|
||||
MessageDialogButtons::YesNo
|
||||
},
|
||||
);
|
||||
|
||||
Ok(dialog.blocking_show())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn confirm<R: Runtime>(
|
||||
window: Window<R>,
|
||||
dialog: State<'_, Dialog<R>>,
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
kind: Option<MessageDialogKind>,
|
||||
ok_button_label: Option<String>,
|
||||
cancel_button_label: Option<String>,
|
||||
) -> Result<bool> {
|
||||
let dialog = message_dialog(
|
||||
window,
|
||||
dialog,
|
||||
title,
|
||||
message,
|
||||
kind,
|
||||
if let Some(ok_button_label) = ok_button_label {
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
ok_button_label,
|
||||
cancel_button_label.unwrap_or(CANCEL.to_string()),
|
||||
)
|
||||
} else if let Some(cancel_button_label) = cancel_button_label {
|
||||
MessageDialogButtons::OkCancelCustom(OK.to_string(), cancel_button_label)
|
||||
} else {
|
||||
MessageDialogButtons::OkCancel
|
||||
},
|
||||
);
|
||||
|
||||
Ok(dialog.blocking_show())
|
||||
}
|
||||
|
||||
@@ -53,19 +53,9 @@ pub enum PickerMode {
|
||||
Video,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FileAccessMode {
|
||||
Copy,
|
||||
Scoped,
|
||||
}
|
||||
|
||||
pub(crate) const OK: &str = "Ok";
|
||||
#[cfg(mobile)]
|
||||
pub(crate) const CANCEL: &str = "Cancel";
|
||||
#[cfg(mobile)]
|
||||
pub(crate) const YES: &str = "Yes";
|
||||
#[cfg(mobile)]
|
||||
pub(crate) const NO: &str = "No";
|
||||
|
||||
macro_rules! blocking_fn {
|
||||
@@ -200,6 +190,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::open,
|
||||
commands::save,
|
||||
commands::message,
|
||||
commands::ask,
|
||||
commands::confirm
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
@@ -247,8 +239,8 @@ impl<R: Runtime> MessageDialogBuilder<R> {
|
||||
dialog,
|
||||
title: title.into(),
|
||||
message: message.into(),
|
||||
kind: MessageDialogKind::default(),
|
||||
buttons: MessageDialogButtons::default(),
|
||||
kind: Default::default(),
|
||||
buttons: Default::default(),
|
||||
#[cfg(desktop)]
|
||||
parent: None,
|
||||
}
|
||||
@@ -387,7 +379,6 @@ pub struct FileDialogBuilder<R: Runtime> {
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) can_create_directories: Option<bool>,
|
||||
pub(crate) picker_mode: Option<PickerMode>,
|
||||
pub(crate) file_access_mode: Option<FileAccessMode>,
|
||||
#[cfg(desktop)]
|
||||
pub(crate) parent: Option<crate::desktop::WindowHandle>,
|
||||
}
|
||||
@@ -400,7 +391,6 @@ pub(crate) struct FileDialogPayload<'a> {
|
||||
filters: &'a Vec<Filter>,
|
||||
multiple: bool,
|
||||
picker_mode: &'a Option<PickerMode>,
|
||||
file_access_mode: &'a Option<FileAccessMode>,
|
||||
}
|
||||
|
||||
// raw window handle :(
|
||||
@@ -417,7 +407,6 @@ impl<R: Runtime> FileDialogBuilder<R> {
|
||||
title: None,
|
||||
can_create_directories: None,
|
||||
picker_mode: None,
|
||||
file_access_mode: None,
|
||||
#[cfg(desktop)]
|
||||
parent: None,
|
||||
}
|
||||
@@ -430,7 +419,6 @@ impl<R: Runtime> FileDialogBuilder<R> {
|
||||
filters: &self.filters,
|
||||
multiple,
|
||||
picker_mode: &self.picker_mode,
|
||||
file_access_mode: &self.file_access_mode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,14 +488,6 @@ impl<R: Runtime> FileDialogBuilder<R> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the file access mode of the dialog.
|
||||
/// This is only used on iOS.
|
||||
/// On desktop and Android, this option is ignored.
|
||||
pub fn set_file_access_mode(mut self, mode: FileAccessMode) -> Self {
|
||||
self.file_access_mode.replace(mode);
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows the dialog to select a single file.
|
||||
///
|
||||
/// This is not a blocking operation,
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.4.5]
|
||||
|
||||
- [`521cd8b3`](https://github.com/tauri-apps/plugins-workspace/commit/521cd8b372c862d96b1637775710e4d7cf2443e2) ([#3155](https://github.com/tauri-apps/plugins-workspace/pull/3155) by [@EliasStar](https://github.com/tauri-apps/plugins-workspace/../../EliasStar)) Fix off by one error in the implementation of readTextFileLines causing all lines to end with an (additional) null byte.
|
||||
Issue: [#3154](https://github.com/tauri-apps/plugins-workspace/issues/3154)
|
||||
PR: [#3155](https://github.com/tauri-apps/plugins-workspace/pull/3155)
|
||||
|
||||
## \[2.4.4]
|
||||
|
||||
- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.4.5"
|
||||
version = "2.4.4"
|
||||
description = "Access the file system."
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
@@ -30,7 +30,6 @@ serde_repr = "0.1"
|
||||
tauri = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
log = { workspace = true }
|
||||
anyhow = "1"
|
||||
glob = { workspace = true }
|
||||
# TODO: Remove `serialization-compat-6` in v3
|
||||
@@ -42,8 +41,5 @@ notify-debouncer-full = { version = "0.6", optional = true }
|
||||
dunce = { workspace = true }
|
||||
percent-encoding = "2"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc2-foundation = { version = "0.3", features = ["NSURL", "NSString"] }
|
||||
|
||||
[features]
|
||||
watch = ["notify", "notify-debouncer-full"]
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-6
@@ -104,8 +104,6 @@ const COMMANDS: &[(&str, &[&str])] = &[
|
||||
// TODO: Remove this in v3
|
||||
("unwatch", &[]),
|
||||
("size", &[]),
|
||||
("start_accessing_security_scoped_resource", &[]),
|
||||
("stop_accessing_security_scoped_resource", &[]),
|
||||
];
|
||||
|
||||
fn main() {
|
||||
@@ -220,10 +218,7 @@ permissions = [
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.global_api_script_path("./api-iife.js")
|
||||
.global_scope_schema(
|
||||
schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::draft07())
|
||||
.into_root_schema_for::<FsScopeEntry>(),
|
||||
)
|
||||
.global_scope_schema(schemars::schema_for!(FsScopeEntry))
|
||||
.android_path("android")
|
||||
.build();
|
||||
|
||||
|
||||
@@ -5,19 +5,6 @@
|
||||
/**
|
||||
* Access the file system.
|
||||
*
|
||||
* ## iOS security-scoped resources
|
||||
*
|
||||
* On iOS, the `fs` plugin automatically manages access to security-scoped resources when a file URL is accessed.
|
||||
* This is required for files outside the app's sandbox (e.g., from file picker).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { open } from '@tauri-apps/plugin-fs';
|
||||
*
|
||||
* const file = await open('file:///path/to/file.txt');
|
||||
* await file.close();
|
||||
* ```
|
||||
*
|
||||
* ## Security
|
||||
*
|
||||
* This module prevents path traversal, not allowing parent directory accessors to be used
|
||||
@@ -736,8 +723,6 @@ async function readDir(
|
||||
interface ReadFileOptions {
|
||||
/** Base directory for `path` */
|
||||
baseDir?: BaseDirectory
|
||||
/** Text encoding to use when reading a text file. Defaults to 'utf-8'. */
|
||||
encoding?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -768,7 +753,7 @@ async function readFile(
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and returns the entire contents of a file as a string using the specified encoding (default: UTF-8).
|
||||
* Reads and returns the entire contents of a file as UTF-8 string.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { readTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
|
||||
@@ -792,11 +777,11 @@ async function readTextFile(
|
||||
|
||||
const bytes = arr instanceof ArrayBuffer ? arr : Uint8Array.from(arr)
|
||||
|
||||
return new TextDecoder(options?.encoding ?? 'utf-8').decode(bytes)
|
||||
return new TextDecoder().decode(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an async {@linkcode AsyncIterableIterator} over the lines of a file, decoded using the specified encoding (default: UTF-8).
|
||||
* Returns an async {@linkcode AsyncIterableIterator} over the lines of a file as UTF-8 string.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { readTextFileLines, BaseDirectory } from '@tauri-apps/plugin-fs';
|
||||
@@ -825,15 +810,10 @@ async function readTextFileLines(
|
||||
rid: null as number | null,
|
||||
|
||||
async next(): Promise<IteratorResult<string>> {
|
||||
const decoder = new TextDecoder(options?.encoding ?? 'utf-8')
|
||||
|
||||
if (this.rid === null) {
|
||||
// Use the normalized encoding label for options.
|
||||
const encoding = decoder.encoding
|
||||
|
||||
this.rid = await invoke<number>('plugin:fs|read_text_file_lines', {
|
||||
path: pathStr,
|
||||
options: options != null ? { ...options, encoding } : undefined
|
||||
options
|
||||
})
|
||||
}
|
||||
|
||||
@@ -858,7 +838,7 @@ async function readTextFileLines(
|
||||
return { value: null, done }
|
||||
}
|
||||
|
||||
const line = decoder.decode(bytes.slice(0, bytes.byteLength - 1))
|
||||
const line = new TextDecoder().decode(bytes.slice(0, bytes.byteLength))
|
||||
|
||||
return {
|
||||
value: line,
|
||||
@@ -1366,79 +1346,6 @@ async function size(path: string | URL): Promise<number> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts accessing a security-scoped resource for the given file URL.
|
||||
* This should be called when you're accessing a file that was opened
|
||||
* using a security-scoped URL (e.g., from a file picker).
|
||||
*
|
||||
* Note that accessing security-scoped resources is automatically managed by the plugin on iOS, so you don't need to call this function
|
||||
* unless you want to manage the scope manually.
|
||||
*
|
||||
* You must call {@linkcode stopAccessingSecurityScopedResource} when you're done accessing the resource.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **iOS:** Starts accessing the security-scoped resource.
|
||||
* - **Other platforms:** does nothing.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { startAccessingSecurityScopedResource } from '@tauri-apps/plugin-fs';
|
||||
*
|
||||
* const filePath = 'file:///path/to/file.txt';
|
||||
* await startAccessingSecurityScopedResource(filePath);
|
||||
* // ... use the resource ...
|
||||
* ```
|
||||
*
|
||||
* @since 2.5.0
|
||||
*/
|
||||
async function startAccessingSecurityScopedResource(
|
||||
path: string | URL
|
||||
): Promise<void> {
|
||||
if (path instanceof URL && path.protocol !== 'file:') {
|
||||
throw new TypeError('Must be a file URL.')
|
||||
}
|
||||
|
||||
await invoke('plugin:fs|start_accessing_security_scoped_resource', {
|
||||
path: path instanceof URL ? path.toString() : path
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops accessing a security-scoped resource for the given file URL.
|
||||
* This should be called when you're done accessing a file that was opened
|
||||
* using a security-scoped URL (e.g., from a file picker) when using manual tracking via {@linkcode startAccessingSecurityScopedResource}.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **iOS:** Stops accessing the security-scoped resource.
|
||||
* - **Other platforms:** does nothing.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { stopAccessingSecurityScopedResource } from '@tauri-apps/plugin-fs';
|
||||
*
|
||||
* const filePath = 'file:///path/to/file.txt';
|
||||
* await startAccessingSecurityScopedResource(filePath);
|
||||
* // ... use the resource ...
|
||||
* // when you're done with the resource:
|
||||
* await stopAccessingSecurityScopedResource(filePath);
|
||||
* ```
|
||||
*
|
||||
* @since 2.5.0
|
||||
*/
|
||||
async function stopAccessingSecurityScopedResource(
|
||||
path: string | URL
|
||||
): Promise<void> {
|
||||
if (path instanceof URL && path.protocol !== 'file:') {
|
||||
throw new TypeError('Must be a file URL.')
|
||||
}
|
||||
|
||||
await invoke('plugin:fs|stop_accessing_security_scoped_resource', {
|
||||
path: path instanceof URL ? path.toString() : path
|
||||
})
|
||||
}
|
||||
|
||||
export type {
|
||||
CreateOptions,
|
||||
OpenOptions,
|
||||
@@ -1487,7 +1394,5 @@ export {
|
||||
exists,
|
||||
watch,
|
||||
watchImmediate,
|
||||
size,
|
||||
startAccessingSecurityScopedResource,
|
||||
stopAccessingSecurityScopedResource
|
||||
size
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tauri-apps/plugin-fs",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.4",
|
||||
"description": "Access the file system.",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"authors": [
|
||||
@@ -25,6 +25,6 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-start-accessing-security-scoped-resource"
|
||||
description = "Enables the start_accessing_security_scoped_resource command without any pre-configured scope."
|
||||
commands.allow = ["start_accessing_security_scoped_resource"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-start-accessing-security-scoped-resource"
|
||||
description = "Denies the start_accessing_security_scoped_resource command without any pre-configured scope."
|
||||
commands.deny = ["start_accessing_security_scoped_resource"]
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-stop-accessing-security-scoped-resource"
|
||||
description = "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope."
|
||||
commands.allow = ["stop_accessing_security_scoped_resource"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-stop-accessing-security-scoped-resource"
|
||||
description = "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope."
|
||||
commands.deny = ["stop_accessing_security_scoped_resource"]
|
||||
@@ -1,6 +1,6 @@
|
||||
## Default Permission
|
||||
|
||||
This set of permissions describes the what kind of
|
||||
This set of permissions describes what kind of
|
||||
file system access the `fs` plugin has enabled or denied by default.
|
||||
|
||||
#### Granted Permissions
|
||||
@@ -3435,32 +3435,6 @@ Denies the size command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`fs:allow-start-accessing-security-scoped-resource`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the start_accessing_security_scoped_resource command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`fs:deny-start-accessing-security-scoped-resource`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the start_accessing_security_scoped_resource command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`fs:allow-stat`
|
||||
|
||||
</td>
|
||||
@@ -3487,32 +3461,6 @@ Denies the stat command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`fs:allow-stop-accessing-security-scoped-resource`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the stop_accessing_security_scoped_resource command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`fs:deny-stop-accessing-security-scoped-resource`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the stop_accessing_security_scoped_resource command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`fs:allow-truncate`
|
||||
|
||||
</td>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[default]
|
||||
description = """
|
||||
This set of permissions describes the what kind of
|
||||
This set of permissions describes what kind of
|
||||
file system access the `fs` plugin has enabled or denied by default.
|
||||
|
||||
#### Granted Permissions
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+77
-591
@@ -16,7 +16,6 @@ use std::{
|
||||
borrow::Cow,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read, Write},
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Mutex,
|
||||
@@ -71,209 +70,6 @@ impl Serialize for CommandError {
|
||||
|
||||
pub type CommandResult<T> = std::result::Result<T, CommandError>;
|
||||
|
||||
/// Represents either a plain PathBuf or a PathHandle that manages security-scoped resources.
|
||||
pub enum PathKind<R: Runtime> {
|
||||
/// A plain path that doesn't manage security-scoped resources.
|
||||
#[allow(dead_code)] // only used on mobile
|
||||
Path(PathBuf),
|
||||
/// A path handle that manages security-scoped resources and will clean them up on drop.
|
||||
Handle(PathHandle<R>),
|
||||
}
|
||||
|
||||
impl<R: Runtime> PathKind<R> {
|
||||
/// Get a reference to the underlying path.
|
||||
pub fn as_path(&self) -> &Path {
|
||||
match self {
|
||||
PathKind::Path(p) => p.as_ref(),
|
||||
PathKind::Handle(h) => h.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the underlying PathBuf.
|
||||
pub fn as_path_buf(&self) -> &PathBuf {
|
||||
match self {
|
||||
PathKind::Path(p) => p,
|
||||
PathKind::Handle(h) => h,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> AsRef<Path> for PathKind<R> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> AsRef<PathBuf> for PathKind<R> {
|
||||
fn as_ref(&self) -> &PathBuf {
|
||||
self.as_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
/// A file handle that automatically stops accessing security-scoped resources on iOS when dropped.
|
||||
pub struct FileHandle<R: Runtime> {
|
||||
file: File,
|
||||
path: PathKind<R>,
|
||||
#[allow(dead_code)] // Used in Drop implementation
|
||||
path_: SafeFilePath,
|
||||
#[allow(dead_code)] // Used in Drop implementation
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> FileHandle<R> {
|
||||
fn new(
|
||||
file: File,
|
||||
path: PathKind<R>,
|
||||
path_: SafeFilePath,
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
) -> Self {
|
||||
Self {
|
||||
file,
|
||||
path,
|
||||
path_,
|
||||
app_handle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the resolved path.
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Deref for FileHandle<R> {
|
||||
type Target = File;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> DerefMut for FileHandle<R> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Drop for FileHandle<R> {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
// Only clean up if we have a plain PathBuf, not a PathHandle
|
||||
// PathHandle will handle its own cleanup when it's dropped
|
||||
if let PathKind::Path(_) = &self.path {
|
||||
use crate::{FilePath, FsExt};
|
||||
// Convert SafeFilePath to FilePath
|
||||
let file_path: FilePath = match &self.path_ {
|
||||
SafeFilePath::Url(url) => FilePath::Url(url.clone()),
|
||||
SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()),
|
||||
};
|
||||
|
||||
// Only clean up if we're tracking this resource
|
||||
// If start_accessing_security_scoped_resource was used, it won't be in our tracking
|
||||
// and we shouldn't interfere
|
||||
if let FilePath::Url(url) = file_path {
|
||||
if url.scheme() == "file" {
|
||||
let security_scoped_resources =
|
||||
self.app_handle.state::<crate::SecurityScopedResources>();
|
||||
|
||||
// Only clean up if it's not tracked manually
|
||||
if !security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
log::debug!("Stopping accessing security-scoped resource for URL: {url} on drop");
|
||||
let _ = self
|
||||
.app_handle
|
||||
.fs()
|
||||
.stop_accessing_security_scoped_resource(FilePath::Url(
|
||||
url.clone(),
|
||||
));
|
||||
security_scoped_resources.remove(url.as_str());
|
||||
} else {
|
||||
log::debug!("Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A path handle that automatically stops accessing security-scoped resources on iOS when dropped.
|
||||
pub struct PathHandle<R: Runtime> {
|
||||
path: PathBuf,
|
||||
#[allow(dead_code)] // Used in Drop implementation
|
||||
path_: SafeFilePath,
|
||||
#[allow(dead_code)] // Used in Drop implementation
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> PathHandle<R> {
|
||||
fn new(path: PathBuf, path_: SafeFilePath, app_handle: tauri::AppHandle<R>) -> Self {
|
||||
Self {
|
||||
path,
|
||||
path_,
|
||||
app_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Deref for PathHandle<R> {
|
||||
type Target = PathBuf;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> AsRef<Path> for PathHandle<R> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> AsRef<PathBuf> for PathHandle<R> {
|
||||
fn as_ref(&self) -> &PathBuf {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Drop for PathHandle<R> {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
use crate::{FilePath, FsExt};
|
||||
// Convert SafeFilePath to FilePath
|
||||
let file_path: FilePath = match &self.path_ {
|
||||
SafeFilePath::Url(url) => FilePath::Url(url.clone()),
|
||||
SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()),
|
||||
};
|
||||
|
||||
// Only clean up if we're tracking this resource (i.e., resolve_path started it)
|
||||
// If start_accessing_security_scoped_resource was used, it won't be in our tracking
|
||||
// and we shouldn't interfere
|
||||
if let FilePath::Url(url) = file_path {
|
||||
if url.scheme() == "file" {
|
||||
let security_scoped_resources =
|
||||
self.app_handle.state::<crate::SecurityScopedResources>();
|
||||
|
||||
// Only clean up if it's not tracked manually
|
||||
if !security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
log::debug!(
|
||||
"Stopping accessing security-scoped resource for URL: {url} on drop"
|
||||
);
|
||||
let _ = self
|
||||
.app_handle
|
||||
.fs()
|
||||
.stop_accessing_security_scoped_resource(FilePath::Url(url.clone()));
|
||||
security_scoped_resources.remove(url.as_str());
|
||||
} else {
|
||||
log::debug!("Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BaseOptions {
|
||||
@@ -288,8 +84,7 @@ pub fn create<R: Runtime>(
|
||||
path: SafeFilePath,
|
||||
options: Option<BaseOptions>,
|
||||
) -> CommandResult<ResourceId> {
|
||||
let path_ = path.clone();
|
||||
let resolved_path_handle = resolve_path(
|
||||
let resolved_path = resolve_path(
|
||||
"create",
|
||||
&webview,
|
||||
&global_scope,
|
||||
@@ -297,22 +92,13 @@ pub fn create<R: Runtime>(
|
||||
path,
|
||||
options.and_then(|o| o.base_dir),
|
||||
)?;
|
||||
let file = File::create(&*resolved_path_handle).map_err(|e| {
|
||||
let file = File::create(&resolved_path).map_err(|e| {
|
||||
format!(
|
||||
"failed to create file at path: {} with error: {e}",
|
||||
resolved_path_handle.display()
|
||||
resolved_path.display()
|
||||
)
|
||||
})?;
|
||||
let app_handle = webview.app_handle().clone();
|
||||
let file_handle = FileHandle::new(
|
||||
file,
|
||||
PathKind::Handle(resolved_path_handle),
|
||||
path_,
|
||||
app_handle,
|
||||
);
|
||||
let rid = webview
|
||||
.resources_table()
|
||||
.add(StdFileResource::new(file_handle));
|
||||
let rid = webview.resources_table().add(StdFileResource::new(file));
|
||||
Ok(rid)
|
||||
}
|
||||
|
||||
@@ -333,7 +119,7 @@ pub fn open<R: Runtime>(
|
||||
path: SafeFilePath,
|
||||
options: Option<OpenOptions>,
|
||||
) -> CommandResult<ResourceId> {
|
||||
let file_handle = resolve_file(
|
||||
let (file, _path) = resolve_file(
|
||||
"open",
|
||||
&webview,
|
||||
&global_scope,
|
||||
@@ -361,9 +147,7 @@ pub fn open<R: Runtime>(
|
||||
},
|
||||
)?;
|
||||
|
||||
let rid = webview
|
||||
.resources_table()
|
||||
.add(StdFileResource::new(file_handle));
|
||||
let rid = webview.resources_table().add(StdFileResource::new(file));
|
||||
|
||||
Ok(rid)
|
||||
}
|
||||
@@ -524,8 +308,8 @@ pub async fn read<R: Runtime>(
|
||||
len: usize,
|
||||
) -> CommandResult<tauri::ipc::Response> {
|
||||
let mut data = vec![0; len];
|
||||
let file: std::sync::Arc<StdFileResource<R>> = webview.resources_table().get(rid)?;
|
||||
let nread = StdFileResource::with_lock(&file, |file| file.read(&mut data))
|
||||
let file = webview.resources_table().get::<StdFileResource>(rid)?;
|
||||
let nread = StdFileResource::with_lock(&file, |mut file| file.read(&mut data))
|
||||
.map_err(|e| format!("faied to read bytes from file with error: {e}"))?;
|
||||
|
||||
// This is an optimization to include the number of read bytes (as bigendian bytes)
|
||||
@@ -561,7 +345,7 @@ async fn read_file_inner<R: Runtime>(
|
||||
path: SafeFilePath,
|
||||
options: Option<BaseOptions>,
|
||||
) -> CommandResult<tauri::ipc::Response> {
|
||||
let mut file_handle = resolve_file(
|
||||
let (mut file, path) = resolve_file(
|
||||
permission,
|
||||
&webview,
|
||||
&global_scope,
|
||||
@@ -580,10 +364,10 @@ async fn read_file_inner<R: Runtime>(
|
||||
|
||||
let mut contents = Vec::new();
|
||||
|
||||
file_handle.read_to_end(&mut contents).map_err(|e| {
|
||||
file.read_to_end(&mut contents).map_err(|e| {
|
||||
format!(
|
||||
"failed to read file as text at path: {} with error: {e}",
|
||||
file_handle.path().display()
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -609,14 +393,6 @@ pub async fn read_file<R: Runtime>(
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadTextFileOptions {
|
||||
#[serde(flatten)]
|
||||
base: BaseOptions,
|
||||
encoding: Option<String>,
|
||||
}
|
||||
|
||||
// TODO, remove in v3, rely on `read_file` command instead
|
||||
#[tauri::command]
|
||||
pub async fn read_text_file<R: Runtime>(
|
||||
@@ -643,7 +419,7 @@ pub fn read_text_file_lines<R: Runtime>(
|
||||
global_scope: GlobalScope<Entry>,
|
||||
command_scope: CommandScope<Entry>,
|
||||
path: SafeFilePath,
|
||||
options: Option<ReadTextFileOptions>,
|
||||
options: Option<BaseOptions>,
|
||||
) -> CommandResult<ResourceId> {
|
||||
let resolved_path = resolve_path(
|
||||
"read-text-file-lines",
|
||||
@@ -651,7 +427,7 @@ pub fn read_text_file_lines<R: Runtime>(
|
||||
&global_scope,
|
||||
&command_scope,
|
||||
path,
|
||||
options.as_ref().and_then(|o| o.base.base_dir),
|
||||
options.as_ref().and_then(|o| o.base_dir),
|
||||
)?;
|
||||
|
||||
let file = File::open(&resolved_path).map_err(|e| {
|
||||
@@ -661,43 +437,12 @@ pub fn read_text_file_lines<R: Runtime>(
|
||||
)
|
||||
})?;
|
||||
|
||||
let encoding = options.as_ref().and_then(|o| o.encoding.as_deref());
|
||||
let (lf_bytes, cr_bytes) = lf_cr_bytes_for_encoding_label(encoding);
|
||||
let lines = BufReader::new(file);
|
||||
let rid = webview
|
||||
.resources_table()
|
||||
.add(StdLinesResource::new(lines, lf_bytes, cr_bytes));
|
||||
let rid = webview.resources_table().add(StdLinesResource::new(lines));
|
||||
|
||||
Ok(rid)
|
||||
}
|
||||
|
||||
/// Returns the byte sequences for LF (`\n`) and CR (`\r`) in the encoding label.
|
||||
///
|
||||
/// The provided encoding label must be a normalized, lowercase string,
|
||||
/// such as one obtained via `(new TextDecoder(encoding)).encoding`.
|
||||
///
|
||||
/// <https://developer.mozilla.org/ja/docs/Web/API/Encoding_API/Encodings>
|
||||
fn lf_cr_bytes_for_encoding_label(label: Option<&str>) -> (Vec<u8>, Vec<u8>) {
|
||||
// Defaults to utf-8
|
||||
// https://developer.mozilla.org/ja/docs/Web/API/TextDecoder/TextDecoder#label
|
||||
let label = label.unwrap_or("utf-8");
|
||||
|
||||
// Currently, according to the Web Standard,
|
||||
// the ASCII-incompatible encodings are UTF-16LE/BE and ISO-2022-JP.
|
||||
// However, ISO-2022-JP can still detect line breaks in the same way as ASCII.
|
||||
//
|
||||
// https://encoding.spec.whatwg.org/#security-background
|
||||
if label == "utf-16le" {
|
||||
return (vec![0x0A, 0x00], vec![0x0D, 0x00]);
|
||||
}
|
||||
if label == "utf-16be" {
|
||||
return (vec![0x00, 0x0A], vec![0x00, 0x0D]);
|
||||
}
|
||||
|
||||
// ASCII-compatible
|
||||
(vec![b'\n'], vec![b'\r'])
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_text_file_lines_next<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
@@ -854,8 +599,8 @@ pub async fn seek<R: Runtime>(
|
||||
whence: SeekMode,
|
||||
) -> CommandResult<u64> {
|
||||
use std::io::{Seek, SeekFrom};
|
||||
let file: std::sync::Arc<StdFileResource<R>> = webview.resources_table().get(rid)?;
|
||||
StdFileResource::with_lock(&file, |file| {
|
||||
let file = webview.resources_table().get::<StdFileResource>(rid)?;
|
||||
StdFileResource::with_lock(&file, |mut file| {
|
||||
file.seek(match whence {
|
||||
SeekMode::Start => SeekFrom::Start(offset as u64),
|
||||
SeekMode::Current => SeekFrom::Current(offset),
|
||||
@@ -878,7 +623,7 @@ fn get_metadata<R: Runtime, F: FnOnce(&PathBuf) -> std::io::Result<std::fs::Meta
|
||||
) -> CommandResult<std::fs::Metadata> {
|
||||
match path {
|
||||
SafeFilePath::Url(url) => {
|
||||
let file_handle = resolve_file(
|
||||
let (file, path) = resolve_file(
|
||||
permission,
|
||||
webview,
|
||||
global_scope,
|
||||
@@ -892,10 +637,10 @@ fn get_metadata<R: Runtime, F: FnOnce(&PathBuf) -> std::io::Result<std::fs::Meta
|
||||
},
|
||||
},
|
||||
)?;
|
||||
file_handle.metadata().map_err(|e| {
|
||||
file.metadata().map_err(|e| {
|
||||
format!(
|
||||
"failed to get metadata of path: {} with error: {e}",
|
||||
file_handle.path().display()
|
||||
path.display()
|
||||
)
|
||||
.into()
|
||||
})
|
||||
@@ -1002,7 +747,7 @@ pub fn lstat<R: Runtime>(
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fstat<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> CommandResult<FileInfo> {
|
||||
let file: std::sync::Arc<StdFileResource<R>> = webview.resources_table().get(rid)?;
|
||||
let file = webview.resources_table().get::<StdFileResource>(rid)?;
|
||||
let metadata = StdFileResource::with_lock(&file, |file| file.metadata())
|
||||
.map_err(|e| format!("failed to get metadata of file with error: {e}"))?;
|
||||
Ok(get_stat(metadata))
|
||||
@@ -1050,7 +795,7 @@ pub async fn ftruncate<R: Runtime>(
|
||||
rid: ResourceId,
|
||||
len: Option<u64>,
|
||||
) -> CommandResult<()> {
|
||||
let file: std::sync::Arc<StdFileResource<R>> = webview.resources_table().get(rid)?;
|
||||
let file = webview.resources_table().get::<StdFileResource>(rid)?;
|
||||
StdFileResource::with_lock(&file, |file| file.set_len(len.unwrap_or(0)))
|
||||
.map_err(|e| format!("failed to truncate file with error: {e}"))
|
||||
.map_err(Into::into)
|
||||
@@ -1062,8 +807,8 @@ pub async fn write<R: Runtime>(
|
||||
rid: ResourceId,
|
||||
data: Vec<u8>,
|
||||
) -> CommandResult<usize> {
|
||||
let file: std::sync::Arc<StdFileResource<R>> = webview.resources_table().get(rid)?;
|
||||
StdFileResource::with_lock(&file, |file| file.write(&data))
|
||||
let file = webview.resources_table().get::<StdFileResource>(rid)?;
|
||||
StdFileResource::with_lock(&file, |mut file| file.write(&data))
|
||||
.map_err(|e| format!("failed to write bytes to file with error: {e}"))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
@@ -1111,7 +856,7 @@ async fn write_file_inner<R: Runtime>(
|
||||
.and_then(|p| p.to_str().ok())
|
||||
.and_then(|opts| serde_json::from_str(opts).ok());
|
||||
|
||||
let mut file_handle = resolve_file(
|
||||
let (mut file, path) = resolve_file(
|
||||
permission,
|
||||
&webview,
|
||||
&global_scope,
|
||||
@@ -1158,12 +903,11 @@ async fn write_file_inner<R: Runtime>(
|
||||
_ => return Err(anyhow::anyhow!("unexpected invoke body").into()),
|
||||
};
|
||||
|
||||
file_handle
|
||||
.write_all(&data)
|
||||
file.write_all(&data)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"failed to write bytes to file at path: {} with error: {e}",
|
||||
file_handle.path().display()
|
||||
path.display()
|
||||
)
|
||||
})
|
||||
.map_err(Into::into)
|
||||
@@ -1249,130 +993,6 @@ pub async fn size<R: Runtime>(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_accessing_security_scoped_resource<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
path: SafeFilePath,
|
||||
) -> CommandResult<()> {
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
use crate::FilePath;
|
||||
// Convert SafeFilePath to FilePath
|
||||
let file_path: FilePath = match &path {
|
||||
SafeFilePath::Url(url) => FilePath::Url(url.clone()),
|
||||
SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()),
|
||||
};
|
||||
|
||||
// Only handle file URLs
|
||||
if let FilePath::Url(url) = &file_path {
|
||||
if url.scheme() == "file" {
|
||||
use objc2_foundation::{NSString, NSURL};
|
||||
|
||||
let url_nsstring = NSString::from_str(url.as_str());
|
||||
let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) };
|
||||
if let Some(ns_url) = ns_url {
|
||||
// Check if already active
|
||||
let security_scoped_resources =
|
||||
webview.state::<crate::SecurityScopedResources>();
|
||||
if security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
log::debug!(
|
||||
"Security-scoped resource already active for URL: {}",
|
||||
url.as_str()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Start accessing the security-scoped resource
|
||||
unsafe {
|
||||
let success = ns_url.startAccessingSecurityScopedResource();
|
||||
if success {
|
||||
log::debug!(
|
||||
"Started accessing security-scoped resource for URL: {}",
|
||||
url.as_str()
|
||||
);
|
||||
security_scoped_resources.track_manually(url.as_str().to_string());
|
||||
} else {
|
||||
log::warn!(
|
||||
"Failed to start accessing security-scoped resource for URL: {}",
|
||||
url.as_str()
|
||||
);
|
||||
return Err(CommandError::from(format!(
|
||||
"Failed to start accessing security-scoped resource for URL: {}",
|
||||
url.as_str()
|
||||
)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(CommandError::from(format!(
|
||||
"Failed to create NSURL from URL: {}",
|
||||
url.as_str()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
{
|
||||
// No-op on non-iOS platforms
|
||||
let _ = webview;
|
||||
let _ = path;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn stop_accessing_security_scoped_resource<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
path: SafeFilePath,
|
||||
) -> CommandResult<()> {
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
use crate::{FilePath, FsExt};
|
||||
// Convert SafeFilePath to FilePath
|
||||
let file_path: FilePath = match &path {
|
||||
SafeFilePath::Url(url) => FilePath::Url(url.clone()),
|
||||
SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()),
|
||||
};
|
||||
|
||||
// Only handle file URLs
|
||||
if let FilePath::Url(url) = file_path {
|
||||
if url.scheme() == "file" {
|
||||
let security_scoped_resources = webview.state::<crate::SecurityScopedResources>();
|
||||
|
||||
// Check if it's tracked
|
||||
if !security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
log::debug!(
|
||||
"Security-scoped resource not tracked as active for URL: {}",
|
||||
url.as_str()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Stop accessing the security-scoped resource
|
||||
webview
|
||||
.fs()
|
||||
.stop_accessing_security_scoped_resource(FilePath::Url(url.clone()))?;
|
||||
|
||||
// Remove from tracking
|
||||
security_scoped_resources.remove(url.as_str());
|
||||
log::debug!(
|
||||
"Stopped accessing security-scoped resource for URL: {}",
|
||||
url.as_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
{
|
||||
// No-op on non-iOS platforms
|
||||
let _ = webview;
|
||||
let _ = path;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dir_size(path: &PathBuf) -> CommandResult<u64> {
|
||||
let mut size = 0;
|
||||
|
||||
@@ -1390,7 +1010,7 @@ fn get_dir_size(path: &PathBuf) -> CommandResult<u64> {
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn resolve_file<R: Runtime>(
|
||||
permission: &str,
|
||||
webview: &Webview<R>,
|
||||
@@ -1398,7 +1018,7 @@ pub fn resolve_file<R: Runtime>(
|
||||
command_scope: &CommandScope<Entry>,
|
||||
path: SafeFilePath,
|
||||
open_options: OpenOptions,
|
||||
) -> CommandResult<FileHandle<R>> {
|
||||
) -> CommandResult<(File, PathBuf)> {
|
||||
resolve_file_in_fs(
|
||||
permission,
|
||||
webview,
|
||||
@@ -1416,9 +1036,8 @@ fn resolve_file_in_fs<R: Runtime>(
|
||||
command_scope: &CommandScope<Entry>,
|
||||
path: SafeFilePath,
|
||||
open_options: OpenOptions,
|
||||
) -> CommandResult<FileHandle<R>> {
|
||||
let path_ = path.clone();
|
||||
let resolved_path_handle = resolve_path(
|
||||
) -> CommandResult<(File, PathBuf)> {
|
||||
let path = resolve_path(
|
||||
permission,
|
||||
webview,
|
||||
global_scope,
|
||||
@@ -1428,24 +1047,17 @@ fn resolve_file_in_fs<R: Runtime>(
|
||||
)?;
|
||||
|
||||
let file = std::fs::OpenOptions::from(open_options.options)
|
||||
.open(&*resolved_path_handle)
|
||||
.open(&path)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"failed to open file at path: {} with error: {e}",
|
||||
resolved_path_handle.display()
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let app_handle = webview.app_handle().clone();
|
||||
Ok(FileHandle::new(
|
||||
file,
|
||||
PathKind::Handle(resolved_path_handle),
|
||||
path_,
|
||||
app_handle,
|
||||
))
|
||||
Ok((file, path))
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn resolve_file<R: Runtime>(
|
||||
permission: &str,
|
||||
webview: &Webview<R>,
|
||||
@@ -1453,23 +1065,16 @@ pub fn resolve_file<R: Runtime>(
|
||||
command_scope: &CommandScope<Entry>,
|
||||
path: SafeFilePath,
|
||||
open_options: OpenOptions,
|
||||
) -> CommandResult<FileHandle<R>> {
|
||||
) -> CommandResult<(File, PathBuf)> {
|
||||
use crate::FsExt;
|
||||
|
||||
let path_ = path.clone();
|
||||
match path {
|
||||
SafeFilePath::Url(url) => {
|
||||
let resolved_path = url.as_str().into();
|
||||
let path = url.as_str().into();
|
||||
let file = webview
|
||||
.fs()
|
||||
.open(SafeFilePath::Url(url.clone()), open_options.options)?;
|
||||
let app_handle = webview.app_handle().clone();
|
||||
Ok(FileHandle::new(
|
||||
file,
|
||||
PathKind::Path(resolved_path),
|
||||
path_,
|
||||
app_handle,
|
||||
))
|
||||
.open(SafeFilePath::Url(url), open_options.options)?;
|
||||
Ok((file, path))
|
||||
}
|
||||
SafeFilePath::Path(path) => resolve_file_in_fs(
|
||||
permission,
|
||||
@@ -1489,47 +1094,9 @@ pub fn resolve_path<R: Runtime>(
|
||||
command_scope: &CommandScope<Entry>,
|
||||
path: SafeFilePath,
|
||||
base_dir: Option<BaseDirectory>,
|
||||
) -> CommandResult<PathHandle<R>> {
|
||||
let path_ = path.clone();
|
||||
// On iOS, start accessing security-scoped resource if the path is a file URL
|
||||
// Only if it hasn't been started already via start_accessing_security_scoped_resource
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
if let SafeFilePath::Url(url) = &path {
|
||||
if url.scheme() == "file" {
|
||||
use objc2_foundation::{NSString, NSURL};
|
||||
|
||||
let security_scoped_resources = webview.state::<crate::SecurityScopedResources>();
|
||||
|
||||
// Check if already active (started via start_accessing_security_scoped_resource)
|
||||
if !security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
let url_nsstring = NSString::from_str(url.as_str());
|
||||
let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) };
|
||||
if let Some(ns_url) = ns_url {
|
||||
// Start accessing the security-scoped resource
|
||||
// This is required for files outside the app's sandbox (e.g., from file picker)
|
||||
unsafe {
|
||||
let success = ns_url.startAccessingSecurityScopedResource();
|
||||
if success {
|
||||
log::debug!("Started accessing security-scoped resource for URL: {} (via resolve_path)", url.as_str());
|
||||
// Track it so we know to clean it up
|
||||
security_scoped_resources.track_manually(url.as_str().to_string());
|
||||
} else {
|
||||
log::warn!("Failed to start accessing security-scoped resource for URL: {}", url.as_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::debug!("Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", url.as_str());
|
||||
}
|
||||
} else {
|
||||
log::debug!("Security-scoped resource already active for URL: {} (started via start_accessing_security_scoped_resource), skipping", url.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
) -> CommandResult<PathBuf> {
|
||||
let path = path.into_path()?;
|
||||
let resolved_path = if let Some(base_dir) = base_dir {
|
||||
let path = if let Some(base_dir) = base_dir {
|
||||
webview.path().resolve(&path, base_dir)?
|
||||
} else {
|
||||
path
|
||||
@@ -1558,24 +1125,23 @@ pub fn resolve_path<R: Runtime>(
|
||||
|
||||
let require_literal_leading_dot = fs_scope.require_literal_leading_dot.unwrap_or(cfg!(unix));
|
||||
|
||||
if is_forbidden(&fs_scope.scope, &resolved_path, require_literal_leading_dot)
|
||||
|| is_forbidden(&scope, &resolved_path, require_literal_leading_dot)
|
||||
if is_forbidden(&fs_scope.scope, &path, require_literal_leading_dot)
|
||||
|| is_forbidden(&scope, &path, require_literal_leading_dot)
|
||||
{
|
||||
return Err(CommandError::Plugin(Error::PathForbidden(resolved_path)));
|
||||
return Err(CommandError::Plugin(Error::PathForbidden(path)));
|
||||
}
|
||||
|
||||
if fs_scope.scope.is_allowed(&resolved_path) || scope.is_allowed(&resolved_path) {
|
||||
let app_handle = webview.app_handle().clone();
|
||||
Ok(PathHandle::new(resolved_path, path_, app_handle))
|
||||
if fs_scope.scope.is_allowed(&path) || scope.is_allowed(&path) {
|
||||
Ok(path)
|
||||
} else {
|
||||
#[cfg(not(debug_assertions))]
|
||||
return Err(CommandError::Plugin(Error::PathForbidden(resolved_path)));
|
||||
return Err(CommandError::Plugin(Error::PathForbidden(path)));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
Err(
|
||||
anyhow::anyhow!(
|
||||
"forbidden path: {}, maybe it is not allowed on the scope for `allow-{permission}` permission in your capability file",
|
||||
resolved_path.display()
|
||||
path.display()
|
||||
)
|
||||
)
|
||||
.map_err(Into::into)
|
||||
@@ -1621,55 +1187,38 @@ fn is_forbidden<P: AsRef<Path>>(
|
||||
}
|
||||
}
|
||||
|
||||
struct StdFileResource<R: Runtime>(Mutex<FileHandle<R>>);
|
||||
struct StdFileResource(Mutex<File>);
|
||||
|
||||
impl<R: Runtime> StdFileResource<R> {
|
||||
fn new(file_handle: FileHandle<R>) -> Self {
|
||||
Self(Mutex::new(file_handle))
|
||||
impl StdFileResource {
|
||||
fn new(file: File) -> Self {
|
||||
Self(Mutex::new(file))
|
||||
}
|
||||
|
||||
fn with_lock<Ret, F: FnMut(&mut File) -> Ret>(&self, mut f: F) -> Ret {
|
||||
let mut file_handle = self.0.lock().unwrap();
|
||||
f(&mut file_handle)
|
||||
fn with_lock<R, F: FnMut(&File) -> R>(&self, mut f: F) -> R {
|
||||
let file = self.0.lock().unwrap();
|
||||
f(&file)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> Resource for StdFileResource<R> {}
|
||||
impl Resource for StdFileResource {}
|
||||
|
||||
/// Same as [std::io::Lines] but with bytes
|
||||
struct LinesBytes<T: BufRead> {
|
||||
bytes: T,
|
||||
lf_bytes: Vec<u8>,
|
||||
cr_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<T: BufRead> LinesBytes<T> {
|
||||
fn new(bytes: T, lf_bytes: Vec<u8>, cr_bytes: Vec<u8>) -> Self {
|
||||
LinesBytes {
|
||||
bytes,
|
||||
lf_bytes,
|
||||
cr_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
struct LinesBytes<T: BufRead>(T);
|
||||
|
||||
impl<B: BufRead> Iterator for LinesBytes<B> {
|
||||
type Item = std::io::Result<Vec<u8>>;
|
||||
|
||||
fn next(&mut self) -> Option<std::io::Result<Vec<u8>>> {
|
||||
let mut buf = Vec::new();
|
||||
// Search for '\n'
|
||||
match read_until_bytes(&mut self.bytes, &self.lf_bytes, &mut buf) {
|
||||
match self.0.read_until(b'\n', &mut buf) {
|
||||
Ok(0) => None,
|
||||
Ok(_n) => {
|
||||
// Remove '\n' or '\r\n'
|
||||
if buf.ends_with(&self.lf_bytes) {
|
||||
buf.truncate(buf.len() - self.lf_bytes.len());
|
||||
if buf.ends_with(&self.cr_bytes) {
|
||||
buf.truncate(buf.len() - self.cr_bytes.len());
|
||||
if buf.last() == Some(&b'\n') {
|
||||
buf.pop();
|
||||
if buf.last() == Some(&b'\r') {
|
||||
buf.pop();
|
||||
}
|
||||
}
|
||||
|
||||
Some(Ok(buf))
|
||||
}
|
||||
Err(e) => Some(Err(e)),
|
||||
@@ -1677,35 +1226,11 @@ impl<B: BufRead> Iterator for LinesBytes<B> {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_until_bytes(
|
||||
r: &mut impl BufRead,
|
||||
bytes: &[u8],
|
||||
buf: &mut Vec<u8>,
|
||||
) -> std::io::Result<usize> {
|
||||
let last_byte = *bytes
|
||||
.last()
|
||||
.ok_or_else(|| std::io::Error::other("invalid empty bytes"))?;
|
||||
|
||||
if bytes.len() == 1 {
|
||||
return r.read_until(last_byte, buf);
|
||||
}
|
||||
|
||||
let mut total_n = 0;
|
||||
loop {
|
||||
let n = r.read_until(last_byte, buf)?;
|
||||
total_n += n;
|
||||
|
||||
if n == 0 || buf.ends_with(bytes) {
|
||||
return Ok(total_n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StdLinesResource(Mutex<LinesBytes<BufReader<File>>>);
|
||||
|
||||
impl StdLinesResource {
|
||||
fn new(lines: BufReader<File>, lf_bytes: Vec<u8>, cr_bytes: Vec<u8>) -> Self {
|
||||
Self(Mutex::new(LinesBytes::new(lines, lf_bytes, cr_bytes)))
|
||||
fn new(lines: BufReader<File>) -> Self {
|
||||
Self(Mutex::new(LinesBytes(lines)))
|
||||
}
|
||||
|
||||
fn with_lock<R, F: FnMut(&mut LinesBytes<BufReader<File>>) -> R>(&self, mut f: F) -> R {
|
||||
@@ -1829,60 +1354,21 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_lines_bytes() {
|
||||
// UTF-8
|
||||
{
|
||||
let base = String::from("line 1\nline2\nline 3\r\nline 4");
|
||||
let bytes = base.as_bytes();
|
||||
let base = String::from("line 1\nline2\nline 3\nline 4");
|
||||
let bytes = base.as_bytes();
|
||||
|
||||
let string1 = base.lines().collect::<String>();
|
||||
let string2 = BufReader::new(bytes)
|
||||
.lines()
|
||||
.map_while(Result::ok)
|
||||
.collect::<String>();
|
||||
let string3 = LinesBytes::new(BufReader::new(bytes), vec![b'\n'], vec![b'\r'])
|
||||
.flatten()
|
||||
.flat_map(String::from_utf8)
|
||||
.collect::<String>();
|
||||
let string1 = base.lines().collect::<String>();
|
||||
let string2 = BufReader::new(bytes)
|
||||
.lines()
|
||||
.map_while(Result::ok)
|
||||
.collect::<String>();
|
||||
let string3 = LinesBytes(BufReader::new(bytes))
|
||||
.flatten()
|
||||
.flat_map(String::from_utf8)
|
||||
.collect::<String>();
|
||||
|
||||
assert_eq!(string1, string2);
|
||||
assert_eq!(string1, string3);
|
||||
assert_eq!(string2, string3);
|
||||
}
|
||||
|
||||
// UTF-16 LE
|
||||
{
|
||||
fn utf16(text: &str) -> Vec<u8> {
|
||||
text.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
let base = String::from("line 1\nline2\nline 3\r\nline 4\n");
|
||||
let bytes = utf16(&base);
|
||||
|
||||
let mut lines = LinesBytes::new(BufReader::new(&bytes[..]), utf16("\n"), utf16("\r"));
|
||||
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 1")));
|
||||
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line2")));
|
||||
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 3")));
|
||||
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 4")));
|
||||
assert!(lines.next().is_none());
|
||||
}
|
||||
|
||||
// UTF-16 BE
|
||||
{
|
||||
fn utf16(text: &str) -> Vec<u8> {
|
||||
text.encode_utf16().flat_map(|u| u.to_be_bytes()).collect()
|
||||
}
|
||||
|
||||
// ਗ (U+0A17) encodes to 0x0A 0x17,
|
||||
// which contains 0x0A but is not a line feed (U+000A = 0x00 0x0A).
|
||||
let base = String::from("line 1\nline2ਗ\nline 3\r\nline 4");
|
||||
let bytes = utf16(&base);
|
||||
|
||||
let mut lines = LinesBytes::new(BufReader::new(&bytes[..]), utf16("\n"), utf16("\r"));
|
||||
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 1")));
|
||||
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line2ਗ")));
|
||||
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 3")));
|
||||
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 4")));
|
||||
assert!(lines.next().is_none());
|
||||
}
|
||||
assert_eq!(string1, string2);
|
||||
assert_eq!(string1, string3);
|
||||
assert_eq!(string2, string3);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user