Compare commits

..

146 Commits

Author SHA1 Message Date
Tony b86e999beb chore(deps): update tauri packages to 2.11 (#3407)
* chore(deps): update tauri packages to 2.11

* Downgrade derive_more

* Fix audit

* Update tauri-plugin
2026-05-01 16:39:43 +08:00
Fabian-Lars c463d8ab14 chore(deps): update rustls-webpki in lockfile, ignore core2 in audit (#3405) 2026-04-23 17:48:22 +02:00
dependabot[bot] 1bb7beb307 chore(deps): bump openssl (#3402)
Bumps [openssl](https://github.com/rust-openssl/rust-openssl) from 0.10.72 to 0.10.78.
- [Release notes](https://github.com/rust-openssl/rust-openssl/releases)
- [Commits](https://github.com/rust-openssl/rust-openssl/compare/openssl-v0.10.72...openssl-v0.10.78)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.78
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 11:52:55 +08:00
Fabian-Lars 3412fa2741 docs(readme): fix platform support matrix (opener supports mobile) 2026-04-22 11:40:46 +02:00
Fabian-Lars af81fdac98 docs(readme): fix platform support matrix (mobile is supported) 2026-04-22 11:39:26 +02:00
Tony c1fd33b3a2 fix(opener): allow open network share locations (#3343)
* fix(opener): allow open network share locations

* Clippy

* Move to a seperate file

* Add license header

* test(opener): add unit tests for shell_parent_path and absolute functions (#1)

* Keep `absolute` non pub in `windows_shell_path`

* Add change file

---------

Co-authored-by: Mark Gandolfo <mark@gandolfo.com.au>
2026-04-18 19:02:09 +08:00
renovate[bot] 250857b7b7 chore(deps): update dependency typescript to v6 (#3363)
* chore(deps): update dependency typescript to v6

* Use 6.0.3

* Fix eslint

* Add `rootDir` since it's no longer inferred
  > https://github.com/microsoft/TypeScript/issues/62508#issuecomment-3348659946

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-04-18 18:57:41 +08:00
Tony 964e13f124 fix(store): dead lock trying to set while exiting (#3395)
* fix(store): dead lock trying to set while exiting

* Add change file
2026-04-18 00:45:16 +08:00
Tony e6cdc9f52e fix(deep-link): inverted is app link filter (#3396)
Fix #3392, regression from #3143
2026-04-17 22:15:07 +08:00
renovate[bot] 9228419e13 chore(deps): update dependency eslint to v10.2.0 (#3381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 12:49:27 +02:00
renovate[bot] 04d92139c8 chore(deps): update dependency typescript-eslint to v8.58.1 (#3364)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 10:55:45 +02:00
renovate[bot] 5260fc90b0 chore(deps): update rust crate tokio-tungstenite to 0.29 (#3354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 10:09:04 +02:00
renovate[bot] ea5c70060b chore(deps): update rust crate rand to v0.9.3 [security] (#3388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 12:12:42 +08:00
renovate[bot] d016a8ab1e chore(deps): update dependency vite to v8.0.5 [security] (#3380)
* chore(deps): update dependency vite to v8.0.5 [security]

* Fix audit
2026-04-07 12:03:37 +08:00
github-actions[bot] d6a3898001 Publish New Versions (v2) (#3268)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-04 18:47:18 +02:00
Tony 2e5bcdf202 chore(deps): fix audits (#3373) 2026-03-31 18:28:05 +08:00
Fabian-Lars 4374b4fc1a chore(notification): remove unused dev-deps (#3372) 2026-03-31 10:33:38 +02:00
Tony f75d21db33 chore(deps): remove used of tauri-utils build feature (#3360) 2026-03-25 22:22:30 +08:00
renovate[bot] 4b95f5e079 chore(deps): update dependency eslint to v10.1.0 (#3357)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 22:00:32 +01:00
dependabot[bot] 99c3e37b54 chore(deps): bump tar in /plugins/updater/tests/updater-migration/v1-app (#3352)
Bumps [tar](https://github.com/alexcrichton/tar-rs) from 0.4.41 to 0.4.45.
- [Commits](https://github.com/alexcrichton/tar-rs/compare/0.4.41...0.4.45)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 0.4.45
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 11:07:40 +08:00
renovate[bot] eaac19a5b7 chore(deps): update rust crate tar to v0.4.45 [security] (#3353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-21 10:07:39 +08:00
renovate[bot] 5183e314cb chore(deps): update dependency typescript-eslint to v8.57.1 (#3344)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 20:32:54 +08:00
renovate[bot] 2c0883e64e chore(deps): update dependency vite to v8 (#3346)
* chore(deps): update dependency vite to v8

* `pnpm update --recursive`

* Regenerate api-iife

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-03-20 20:05:30 +08:00
Josh 024ec0c29c fix(deep-link): ChromeOS deep link calls filtered and ignored by plugin (fix #3207) (#3214) 2026-03-12 23:02:08 +01:00
FabianLars 35aad24773 chore(deps): update quinn-proto in Cargo.lock, closes #3341 2026-03-10 14:11:28 +01:00
Minkin Aleksei ab037b70d6 fix(nfc): remove extra lambda brackets (fix #3338) (#3339)
Co-authored-by: Minkin Aleksei <minkinaleksei@minkins-dev-center.local>
Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com>
2026-03-10 10:55:47 +01:00
renovate[bot] 1198ad4fb0 chore(deps): update dependency eslint to v10.0.3 (#3342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 09:47:29 +01:00
Tony ea033fe3b8 Add AI tool policy to contributing guide (#3336) 2026-03-09 19:35:41 +08:00
Fabian-Lars 2e432f70c9 chore(deps): remove covector from package.json (#3334) 2026-03-08 11:39:12 +01:00
renovate[bot] b19dcd25cb chore(deps): update dependency @rollup/plugin-terser to v1 (#3333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 10:52:07 +01:00
renovate[bot] 995e76436f chore(deps): update dependency @tauri-apps/cli to v2.10.1 (#3332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 21:56:47 +08:00
Mike 1dc3612862 fix(sql): add SQL support for Postgres NUMERIC and custom data types (fix: #3158) (#3275) 2026-03-05 13:31:20 +01:00
Lucas Fernandes Nogueira f5f68063e4 feat(fs): access security scoped resources on iOS (#3185)
Co-authored-by: FabianLars <30730186+FabianLars@users.noreply.github.com>
2026-03-04 13:59:03 +01:00
Tony 36d3d19247 fix(ci): bump rustsec/audit-check to v2 (#3329) 2026-03-04 18:28:32 +08:00
hrzlgnm 31ab6f8d24 fix(updater): preserve file extension of updater package (fix: #3283) (#3285)
* fix: preserve file extension of updated package (fix: #3283)

Otherwise users may get confused when seing a sudo dialog
which suggests a `rpm` package is installed using `dpkg -i`

* pass on package extension more thoroughly

* add changes file

Update the updater package to preserve file extension, clarifying installation prompts for users.

* Apply suggestion from @hrzlgnm

* Apply suggestion from @hrzlgnm

* Apply suggestion from @Legend-Master

* More rpm and log `pkg_path` instead

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-03-03 13:53:48 +08:00
renovate[bot] 21ae430ea3 chore(deps): update rust crate cookie_store to 0.22.0 (#3308)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FabianLars <30730186+FabianLars@users.noreply.github.com>
2026-03-02 17:36:16 +01:00
Lucas Fernandes Nogueira 015e817cf2 feat(deep-link): validate new intent URLs against configured deep links (#3186) 2026-03-02 14:33:21 +01:00
dependabot[bot] 2a6d4b42bb chore(deps): bump time in fixture (#3261)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 09:34:50 +01:00
Tony 550d137c64 chore(deps): disable renovate bot on v1 (#3305) 2026-03-02 16:29:31 +08:00
Tony 3f21f39584 refactor(dialog): handle okLabel in js side (#3295)
* refactor(dialog): handle `okLabel` in js side

* Allow unused instead of `cfg(desktop)`
2026-03-01 23:22:25 +08:00
renovate[bot] 759c22758e chore(deps): update eslint monorepo to v10 (v2) (#3293)
* chore(deps): update eslint monorepo to v9.39.3

* Update to eslint 10
2026-03-01 11:25:21 +08:00
renovate[bot] 6816dd1960 chore(deps): update dependency rollup to v4.59.0 [security] (v2) (#3291)
* chore(deps): update dependency rollup to v4.59.0 [security]

* Update svelte

* pnpm dedupe

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-03-01 11:11:05 +08:00
Tony 24154472a6 refactor(dialog): reuse message command for confirm (#3287)
* chore(dialog): reuse `message` command for confirm

* Add change file

* Remove ask and confirm from default permissions

* Format

* Remove extra `toString`

* Point `allow-confirm` to `allow-message`
2026-03-01 10:55:33 +08:00
Thibault Tisserand 2574ec89e7 refactor(store-example): replace load_from_store with TryFrom impl in example (#3277) 2026-02-19 00:22:25 +01:00
Niklas Volcz 2971289252 fix(http): correct Response header initialization to support cloning (fix #14892) (#3252)
Previously, headers were patched onto the Response object after construction, which bypassed the internal header list and caused `response.clone().headers` to be empty. This change passes the headers directly to the Response constructor, ensuring they are properly stored and clonable.
2026-02-16 16:35:09 +02:00
aiueo13 6c3da6d290 fix(fs): use correct line detection for encodings in readTextFileLines (#3273)
Co-authored-by: OkaYu <aiueo13>
2026-02-13 03:41:43 +02:00
Seiji Okuda e97a4dedab feat(fs): #3243 add encoding option to readTextFile functions (#3244)
* feat(fs): #3243 add encoding option to readFile and readTextFile functions

* feat(fs): add encoding option to ReadFileOptions and update readTextFileLines functions

* add change file
2026-02-09 16:08:26 +02:00
github-actions[bot] 05045f9a72 publish new versions (#3249)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-05 18:13:33 +01:00
Fabian-Lars 4f53e36a50 fix(single-instance): append version at the end 2026-02-05 18:08:35 +01:00
Lucas Fernandes Nogueira 8374e997b8 fix(deep-link): runtime register on Linux when app path has spaces (#3258) 2026-02-05 12:30:29 -03:00
Lucas Nogueira 35aa52f45e fix(single-instance): dbus_path should start with /, can't include - 2026-02-05 10:28:58 -03:00
renovate[bot] bbc177150f chore(deps): update dependency prettier to v3.8.1 (#3235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 23:37:55 +08:00
renovate[bot] e6e2edca11 chore(deps): update dependency typescript-eslint to v8.54.0 (#3232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 23:08:54 +08:00
renovate[bot] 7ecd19da51 chore(deps): update dependency rollup to v4.57.1 (#3230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 23:08:24 +08:00
dependabot[bot] a0b6c8ff3b chore(deps): bump bytes (#3248)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.7.1 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.7.1...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-04 23:07:49 +08:00
renovate[bot] c12ea9306a chore(deps): update rust crate bytes to v1.11.1 [security] (#3247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 23:53:44 +01:00
Demir Yerli 98e2c11eef fix(single-instance): unconventional dbus names (fixes #3184) (#3194)
Co-authored-by: FabianLars <github@fabianlars.de>
Co-authored-by: Amr Bashir <github@amrbashir.me>
2026-02-03 23:36:48 +01:00
github-actions[bot] 50b159f668 Publish New Versions (v2) (#3211)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-03 12:18:37 +01:00
Fabian-Lars 305c4f6b4e ci: add workflow_dipatch trigger to integration tests 2026-02-03 10:55:10 +01:00
Lucas Fernandes Nogueira 89f3e17952 chore: bump tauri to 2.10 (#3246)
* chore: bump tauri to 2.10

* pnpm i

* audit fix
2026-02-02 21:48:08 -03:00
Ale Sanchez d4613ff002 fix(shell): Make sidecars work in tests (fix #13767) (#3234) 2026-01-26 17:12:42 +01:00
Tony 69a9d5771c chore(updater): use keep from tempfile 3.20 (#3229) 2026-01-22 11:40:43 +08:00
Del 61e9b0ab64 fix(http): ensure body resources are released on stream cancellation (#3228)
Co-authored-by: Amr Bashir <github@amrbashir.me>
2026-01-22 05:20:29 +02:00
dependabot[bot] 25ad21beff chore(deps): bump diff from 5.2.0 to 5.2.2 (#3226)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: FabianLars <github@fabianlars.de>
2026-01-20 20:30:35 +01:00
Madhusudhan 2dc3f3f039 fix(sql): uuid columns returning null in postgres query results (#2039) (#3144)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2026-01-19 17:30:28 +01:00
Lucas Fernandes Nogueira c27af9128d fix(barcode-scanner): check if iOS platform supports scanning (#3222) 2026-01-19 13:16:31 -03:00
Lucas Fernandes Nogueira b60dd88702 fix(barcode-scanner): start capture session on a separate thread (#3223)
fixes the given warning:

Thread Performance Checker: -[AVCaptureSession startRunning] should be called from background thread. Calling it on the main thread can lead to UI unresponsiveness

which might cause a UI freeze
2026-01-19 13:16:16 -03:00
renovate[bot] a97033bcd9 chore(deps): update dependency rollup to v4.55.1 (#3201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 16:44:07 +01:00
miaoshengkun 4375c98bed feat(updater): add no_proxy config to disable system proxy (#3073)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2026-01-19 16:17:37 +01:00
Fabian-Lars 82fbb0c790 fix(barcode-scanner): early return scanner if missing permissions (#3221) 2026-01-19 10:21:33 -03:00
Tony de6bf68585 docs(store): fix outdated fn link (#3220) 2026-01-19 12:21:55 +01:00
renovate[bot] 734c627084 chore(deps): update dependency prettier to v3.8.0 (#3217) 2026-01-18 10:54:16 +08:00
Tony 05c5da072b chore(deps): update reqwest to 0.13 for updater (#3213) 2026-01-16 18:37:20 +08:00
renovate[bot] 0d126ff0ad chore(deps): update dependency typescript-eslint to v8.53.0 (v2) (#3189)
* chore(deps): update dependency typescript-eslint to v8.53.0

* Update api-iife.js
2026-01-16 18:36:41 +08:00
FabianLars f122ee98c6 Revert "temp: delete updater changefiles"
This reverts commit 82c404635b.
2026-01-14 20:48:59 +01:00
github-actions[bot] 2308f2299d publish new versions (#3203)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-14 20:48:22 +01:00
FabianLars 82c404635b temp: delete updater changefiles 2026-01-14 20:30:54 +01:00
Fabian-Lars b1dbee2c55 fix(http): manually set origin header to tauri://localhost (#3210) 2026-01-14 20:26:17 +01:00
Andrew de Waal d7a0bb325d feat(dialog) - Support fileAccessMode for open dialog (#3030) (#3136)
* feat(dialog) - Support fileAccessMode for open dialog (#3030)

On iOS, when trying to access a file that exists outside of the app sandbox, one of 2 things need to happen to be able to perform any operations on said file:

* A copy of the file needs to be made to the internal app sandbox
* The method startAccessingSecurityScopedResource needs to be called.

Previously, a copy of the file was always being made when a file was selected through the picker dialog.

While this did ensure there were no file access exceptions when reading from the file, it does not scale well for large files.

To resolve this, we now support `fileAccessMode`, which allows a file handle to be returned without copying the file to the app sandbox.

This MR only supports this change for iOS; MacOS has a different set of needs for security scoped resources.

See discussion in #3716 for more discussion of the difference between iOS and MacOS.
See MR #3185 to see how these scoped files will be accessible using security scoping.

* fmt, clippy

* use enum

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2026-01-14 08:35:06 -03:00
FabianLars f3d75f7abb Revert "temp: remove updater changefiles"
This reverts commit 9a2c98f450.
2026-01-08 15:54:33 +01:00
github-actions[bot] 6e2e7e48c3 publish new versions (#3070)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-08 15:34:17 +01:00
FabianLars 9a2c98f450 temp: remove updater changefiles 2026-01-08 15:17:30 +01:00
FabianLars 4a2ecb6287 chore(deps): update rkyv, closes #3196 2026-01-06 11:05:04 +01:00
Tunglies 31415effdf docs(shell): update example to include Encoding usage in Command::spawn (#3183)
* docs(shell): update example to include Encoding usage in `Command::spawn`

* docs(shell): update patch level to minor for Encoding usage in `Command::spawn` documentation

* Update .changes/doc-to-Encoding-usage-in-shell.md

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* docs(shell): update examples to demonstrate manual Encoding usage in command output

* Update plugins/shell/src/process/mod.rs

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* docs(shell): update example to use 'windows-1252' encoding in command output

* Update plugins/shell/src/process/mod.rs

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* docs(shell): update example command in documentation to use 'some-program' with 'some-arg'

* Bump shell-js in change file

* Fix indent
2025-12-29 10:58:55 +08:00
renovate[bot] 04b33ea0b0 chore(deps): update dependency typescript-eslint to v8.50.1 (#3181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-27 22:30:26 +08:00
renovate[bot] 54e21f142b chore(deps): update dependency rollup to v4.54.0 (#3179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 22:50:36 +08:00
renovate[bot] d528c88b4f chore(deps): update dependency rollup to v4.53.5 (#3172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 19:16:32 +08:00
renovate[bot] 69146fa852 chore(deps): update dependency rollup to v4.53.4 (#3167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 11:20:50 +08:00
renovate[bot] 9f68f2d827 chore(deps): update dependency typescript-eslint to v8.50.0 (#3170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 10:58:03 +08:00
Fabian-Lars 3d0d2e041b fix(opener): ignore inAppBrowser on desktop (#3163) 2025-12-16 07:07:15 -03:00
renovate[bot] 1e3d7ef16e chore(deps): update eslint monorepo to v9.39.2 (#3164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 12:36:13 +08:00
renovate[bot] ce6835d50f chore(deps): update dependency @tauri-apps/cli to v2.9.6 (#3162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 22:13:33 +08:00
renovate[bot] 1d9f47c882 chore(deps): update dependency typescript-eslint to v8.49.0 (#3160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 10:39:11 +08:00
Fabian-Lars e221a04ef4 docs(stronghold): add scrypt opt-level recommendation, closes #2048 2025-12-09 11:50:09 +01:00
Tony eebfd2ed3e fix(store): error instead of panic on invalid rid (#3157) 2025-12-09 18:02:23 +08:00
Elias* 521cd8b372 fix(fs): off by one error in readTextFileLines (#3155)
* fix: off by one error in readTextFileLines (#3154)

Signed-off-by: EliasStar <31409841+EliasStar@users.noreply.github.com>

* Format and regenerate `api-iife.js`

---------

Signed-off-by: EliasStar <31409841+EliasStar@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-12-09 11:48:18 +08:00
renovate[bot] e4a40f4423 chore(deps): update tauri monorepo (v2) (#3142)
* chore(deps): update tauri monorepo

* Build

* Bump rust tauri

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-12-07 00:03:50 +08:00
renovate[bot] fa8b11f19d chore(deps): update dependency prettier to v3.7.4 (#3150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 13:10:40 +08:00
renovate[bot] baefd761e0 chore(deps): update dependency typescript-eslint to v8.48.1 (#3146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-05 18:35:15 +08:00
Tunglies 2804803949 fix: deep-link clippy warnings (#3143)
* Update build.rs

* fix: address clippy warnings in deep-link module

* fix: add deep-link-js patch to address clippy warnings
2025-12-04 20:04:56 +08:00
renovate[bot] d9d51eb8ea chore(deps): update dependency prettier to v3.7.3 (#3140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 09:58:49 +08:00
renovate[bot] 1483c63101 chore(deps): update dependency prettier to v3.7.2 (#3137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 10:31:13 +08:00
renovate[bot] 26ed78989a chore(deps): update dependency prettier to v3.7.1 (#3134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-30 20:01:16 +08:00
renovate[bot] f1564e58b6 chore(deps): update dependency prettier to v3.7.0 (#3132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-30 19:53:50 +08:00
Fabian-Lars f2d4abb9e2 fix(dialog): rfd feature flags (#3129) 2025-11-28 15:25:58 +01:00
renovate[bot] 3dcf7522b1 chore(deps): update dependency typescript-eslint to v8.48.0 (#3126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 14:45:59 +08:00
renovate[bot] 02068550e8 chore(deps): update rust crate rfd to 0.16 (#3128)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 13:47:22 +08:00
Fabian-Lars 66a75ece27 fix(websocket): install crypto provider if needed (#3124) 2025-11-25 20:02:47 +01:00
Christian Leingang fa601e8754 feat(updater): allow accepting invalid TLS certs/hostnames via config (#3057)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-11-24 14:56:59 +01:00
Niccolò Gnani d8bfe61f20 fix(barcode-scanner): remove unnecessary permission checks on Android (fix #2312) (#3121) 2025-11-24 14:27:46 +01:00
Amr Bashir e8915f17e4 fix(http): fix aborting a streaming response (#2562) 2025-11-24 13:55:11 +01:00
Lucas Fernandes Nogueira 6de61f854b chore(log): remove iOS simulator workarounds (#3113) 2025-11-24 13:35:19 +01:00
renovate[bot] ad7a8d3e42 chore(deps): update dependency rollup to v4.53.3 (#3119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 16:25:46 +08:00
FabianLars d5509a9ac8 chore: add docs issue template and enable blank issues 2025-11-23 13:55:52 +01:00
FabianLars 06727b19c1 chore: fix issue templates 2025-11-23 13:51:01 +01:00
Fabian-Lars 2f9fa79747 chore: add issue templates (#3122) 2025-11-23 13:42:24 +01:00
renovate[bot] 1db4b0719d chore(deps): update dependency typescript-eslint to v8.47.0 (#3117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-21 10:47:11 +08:00
Andrew de Waal dff6fa986a feat(dialog) - Support picker mode for open dialog (#3030) (#3034)
Co-authored-by: Andrew de Waal <andrewldewaal@gmail.com>
2025-11-20 22:28:13 +01:00
Nazar Antoniuk a4aa53ab90 chore(shell): clarify that the code field in ExitStatus is intentionally left private (#3116)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-11-20 15:33:19 +01:00
liuzhch1 ae278ddf60 fix(log): custom writer to rotate log file when exceeds max_file_size (fix #707) (#3110) 2025-11-20 10:29:17 +01:00
Jacob Bolda e644f38673 remove token for oidc publishing (#3077) 2025-11-19 21:52:57 -06:00
Lucas Fernandes Nogueira 8bfa445023 chore(localhost): disable caching (#3112)
* chore(localhost): disable caching

mostly seeing this cache being a problem on CEF - the response is cached so app updates are not reflected properly

* fmt
2025-11-19 17:03:32 -03:00
Lucas Nogueira 14fb36e347 chore: remove test logs 2025-11-19 17:00:14 -03:00
Fabian-Lars 5767b848fa chore: add PR ref in updater changefile
Added reference to pull request for clarity on changes.
2025-11-18 12:11:12 +01:00
renovate[bot] 277a45f56c chore(deps): update dependency rollup to v4.53.2 (#3103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 00:07:40 +08:00
renovate[bot] 8dbe7e3233 chore(deps): update dependency typescript-eslint to v8.46.4 (v2) (#3105)
* chore(deps): update dependency typescript-eslint to v8.46.4

* Fix audit (bump js-yaml)

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-11-17 23:44:02 +08:00
Blaine c23fa03f07 feat(dialog): add xdg-portal as an optional feature for rfd (#3098) 2025-11-17 14:56:02 +01:00
Kingsword 631d0e256a fix(barcode-scanner): fix the cameraView is not removed after scanning in iOS (#2440)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-11-16 22:31:30 +01:00
Ben Clarke b4348cee92 fix(nfc): Resolve boolean in isAvailable to agree with TypeScript API (#3101)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-11-13 20:38:15 +01:00
renovate[bot] 3019063ae1 chore(deps): update dependency rollup to v4.53.1 (#3093)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 22:06:11 +08:00
renovate[bot] 944614f46a chore(deps): update dependency @tauri-apps/cli to v2.9.4 (#3100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 21:15:09 +08:00
Tony a368cef912 docs(updater): installer args and on_before_exit (#3099)
* docs(updater): installer args and `on_before_exit`

* Update plugins/updater/src/updater.rs

Co-authored-by: Fabian-Lars <github@fabianlars.de>

* Explicitly state there are internal args

* s
2025-11-12 20:49:41 +08:00
Fabian-Lars 27790aa67c chore: fix upload test on macos ci (#3097) 2025-11-11 22:56:09 +01:00
Matthew Richardson ad910b1135 feat(upload): Add HTTP method selection for upload (#2991)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-11-10 21:31:57 +01:00
Fabian-Lars 6b854421a1 docs(localhost): improve readme example, closes #3095 2025-11-10 21:27:39 +01:00
Fabian-Lars 5438a5cd22 chore: apply clippy fixes (#3092) 2025-11-09 18:43:17 +01:00
Fabian-Lars 5cd7778723 docs(dialog): add/fix doc item links, closes #3089 (#3091) 2025-11-09 17:07:35 +01:00
renovate[bot] 1a03e9761f chore(deps): update dependency @tauri-apps/cli to v2.9.3 (#3088)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-08 13:34:05 +08:00
renovate[bot] 1d4bffadda chore(deps): update eslint monorepo to v9.39.1 (#3086)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-08 12:36:37 +08:00
renovate[bot] b8794272ae chore(deps): update dependency typescript-eslint to v8.46.3 (#3083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-08 12:02:18 +08:00
Michelle Tilley 2a625adff3 feat(log): Allow a log formatter per target (#3065)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-11-04 15:52:46 +01:00
FabianLars 371cd8227c chore(deps): update pino to 9.14 to remove fast-redact 2025-11-04 10:00:51 +01:00
renovate[bot] 70ef6f8d3e chore(deps): update eslint monorepo to v9.39.0 (#3079)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 09:54:50 +01:00
renovate[bot] 5f0ac1436f chore(deps): update dependency @tauri-apps/cli to v2.9.2 (#3078)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-02 09:50:14 +08:00
Fabian-Lars ea172bfa3c docs(global-shortcut): import Emitter instead of Manager in example 2025-10-29 11:46:16 +01:00
FabianLars 6aead24047 Revert "chore: temp delete updater changefile"
This reverts commit b5550a3b0d.
2025-10-27 11:43:23 +01:00
229 changed files with 6062 additions and 4392 deletions
+4 -8
View File
@@ -1,11 +1,7 @@
[advisories]
ignore = [
# 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",
# time crate can't be updated in the repo because of MSRV, users are unaffected
"RUSTSEC-2026-0009",
# libflate crates can't be updated in the repo because of MSRV, users are unaffected
"RUSTSEC-2026-0105",
]
@@ -0,0 +1,6 @@
---
deep-link: patch
deep-link-js: patch
---
Fix broken iOS custom URL schemes
+6
View File
@@ -0,0 +1,6 @@
---
"opener": patch
"opener-js": patch
---
Fix `revealItemInDir`/`reveal_items_in_dir` can't reveal network paths like `\\wsl.localhost\Ubuntu\etc` on Windows
+6
View File
@@ -0,0 +1,6 @@
---
store: patch
store-js: patch
---
Fix a deadlock when calling `Store::set` while exiting (on `RunEvent::Exit`)
+11
View File
@@ -5,6 +5,7 @@ Hi! We, the maintainers, are really excited that you are interested in contribut
- [Issue Reporting Guidelines](#issue-reporting-guidelines)
- [Pull Request Guidelines](#pull-request-guidelines)
- [Development Guide](#development-guide)
- [AI Tool Policy](#ai-tool-policy)
## Issue Reporting Guidelines
@@ -60,3 +61,13 @@ The easiest way to test your changes is to use the [example app](https://github.
To test local changes against your own application simply point the plugin create to your local repository, for example:
`tauri-plugin-sample = { path = "path/to/local/tauri-plugin-sample/" }`
## AI Tool Policy
It takes a lot of time to review a Pull Request while it's very easy to make a nonsensical but plausible looking one using AI tools.
It is unfair for other contributors and the reviewers to spend much of the time dealing with this, hence these rules:
1. Review and test all LLM-generated content before submitting, you're the one responsible for it, not the AI.
2. Don't use AI to respond to review comments (except for translations).
We will close the Pull Request with a `ai-slop` tag if you failed to do so.
+63
View File
@@ -0,0 +1,63 @@
# 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.
+12
View File
@@ -0,0 +1,12 @@
# 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
+5
View File
@@ -0,0 +1,5 @@
---
name: 📚 Docs Report
about: Create a report to help us improve the docs
labels: 'type: documentation'
---
@@ -0,0 +1,46 @@
# 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.
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rustsec/audit-check@v1
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
# https://github.com/tauri-apps/plugins-workspace/issues/774
@@ -11,7 +11,7 @@ on:
- v2
permissions:
# required for npm provenance
# required for oidc token
id-token: write
# required to create the GitHub Release
contents: write
@@ -62,7 +62,6 @@ jobs:
id: covector
env:
CARGO_TARGET_DIR: /mnt/target
NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}
with:
token: ${{ secrets.GITHUB_TOKEN }}
command: 'version-or-publish'
+1
View File
@@ -5,6 +5,7 @@
name: integration tests
on:
workflow_dispatch:
push:
branches:
- v1
+1
View File
@@ -154,4 +154,5 @@ jobs:
run: cargo clippy --package ${{ matrix.package }} --all-targets -- -D warnings
- name: clippy ${{ matrix.package }} --all-features
if: matrix.package != 'tauri-plugin-dialog'
run: cargo clippy --package ${{ matrix.package }} --all-targets --all-features -- -D warnings
+2 -2
View File
@@ -246,9 +246,9 @@ jobs:
run: cargo +stable install cross --git https://github.com/cross-rs/cross
- name: test ${{ matrix.package }}
if: matrix.package != 'tauri-plugin-http'
if: ${{ matrix.package != 'tauri-plugin-http' && 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 }}
if: matrix.package == 'tauri-plugin-http'
if: ${{ matrix.package == 'tauri-plugin-http' || matrix.package == 'tauri-plugin-dialog' }}
run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets
+1
View File
@@ -21,6 +21,7 @@ target/
package-lock.json
yarn.lock
bun.lockb
bun.lock
# rust compiled folders
target/
Generated
+586 -568
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -12,10 +12,10 @@ resolver = "2"
serde = { version = "1", features = ["derive"] }
tracing = "0.1"
log = "0.4"
tauri = { version = "2.8.2", default-features = false }
tauri-build = "2.4"
tauri-plugin = "2.4"
tauri-utils = "2.7"
tauri = { version = "2.10", default-features = false }
tauri-build = "2.5"
tauri-plugin = "2.5"
tauri-utils = "2.8"
serde_json = "1"
thiserror = "2"
url = "2"
+1 -1
View File
@@ -22,7 +22,7 @@ This repo and all plugins require a Rust version of at least **1.77.2**
| [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [nfc](plugins/nfc) | Read and write NFC tags on Android and iOS. | ? | ? | ? | ✅ | ✅ |
| [notification](plugins/notification) | Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [opener](plugins/opener) | Open files and URLs using their default application. | ✅ | ✅ | ✅ | ? | ? |
| [opener](plugins/opener) | Open files and URLs using their default application. | ✅ | ✅ | ✅ | | |
| [os](plugins/os) | Read information about the operating system. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [persisted-scope](plugins/persisted-scope) | Persist runtime scope changes on the filesystem. | ✅ | ✅ | ✅ | ? | ? |
| [positioner](plugins/positioner) | Move windows to common locations. | ✅ | ✅ | ✅ | ❌ | ❌ |
+40
View File
@@ -1,5 +1,45 @@
# Changelog
## \[2.0.38]
### Dependencies
- Upgraded to `fs-js@2.5.0`
- Upgraded to `http-js@2.5.8`
- Upgraded to `updater-js@2.10.1`
- Upgraded to `nfc-js@2.3.5`
- Upgraded to `dialog-js@2.7.0`
## \[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
+21 -21
View File
@@ -1,7 +1,7 @@
{
"name": "api",
"private": true,
"version": "2.0.34",
"version": "2.0.38",
"type": "module",
"scripts": {
"dev": "vite --clearScreen false",
@@ -10,36 +10,36 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "2.9.0",
"@tauri-apps/plugin-barcode-scanner": "^2.4.2",
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-barcode-scanner": "^2.4.4",
"@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.4.2",
"@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-geolocation": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.7.0",
"@tauri-apps/plugin-fs": "^2.5.0",
"@tauri-apps/plugin-geolocation": "^2.3.2",
"@tauri-apps/plugin-global-shortcut": "^2.3.1",
"@tauri-apps/plugin-haptics": "^2.2.0",
"@tauri-apps/plugin-http": "^2.5.4",
"@tauri-apps/plugin-nfc": "^2.3.3",
"@tauri-apps/plugin-haptics": "^2.3.2",
"@tauri-apps/plugin-http": "^2.5.8",
"@tauri-apps/plugin-nfc": "^2.3.5",
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2.5.2",
"@tauri-apps/plugin-opener": "^2.5.3",
"@tauri-apps/plugin-os": "^2.3.2",
"@tauri-apps/plugin-process": "^2.3.1",
"@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",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-store": "^2.4.2",
"@tauri-apps/plugin-updater": "^2.10.1",
"@tauri-apps/plugin-upload": "^2.4.0",
"@zerodevx/svelte-json-view": "1.0.11"
},
"devDependencies": {
"@iconify-json/codicon": "^1.2.12",
"@iconify-json/codicon": "^1.2.49",
"@iconify-json/ph": "^1.2.2",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tauri-apps/cli": "2.9.1",
"@unocss/extractor-svelte": "^66.3.3",
"svelte": "^5.20.4",
"unocss": "^66.3.3",
"vite": "^7.0.7"
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tauri-apps/cli": "2.11.0",
"@unocss/extractor-svelte": "^66.6.7",
"svelte": "^5.54.0",
"unocss": "^66.6.7",
"vite": "^8.0.1"
}
}
+40
View File
@@ -1,5 +1,45 @@
# Changelog
## \[2.0.42]
### Dependencies
- Upgraded to `fs@2.5.0`
- Upgraded to `http@2.5.8`
- Upgraded to `updater@2.10.1`
- Upgraded to `nfc@2.3.5`
- Upgraded to `dialog@2.7.0`
## \[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
+11 -11
View File
@@ -1,7 +1,7 @@
[package]
name = "api"
publish = false
version = "2.0.38"
version = "2.0.42"
description = "An example Tauri Application showcasing the api"
edition = "2021"
rust-version = { workspace = true }
@@ -20,24 +20,24 @@ serde = { workspace = true }
tiny_http = "0.12"
time = "0.3"
log = { workspace = true }
tauri-plugin-log = { path = "../../../plugins/log", version = "2.7.1" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.4.4", features = [
tauri-plugin-log = { path = "../../../plugins/log", version = "2.8.0" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.5.0", features = [
"watch",
] }
tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.3.2" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.4.2" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.7.0" }
tauri-plugin-http = { path = "../../../plugins/http", features = [
"multipart",
"cookies",
], version = "2.5.4" }
], version = "2.5.8" }
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.2" }
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.3" }
tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.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-upload = { path = "../../../plugins/upload", version = "2.3.0" }
[dependencies.tauri]
@@ -57,12 +57,12 @@ 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.9.0" }
tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.10.1" }
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.2" }
tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.3.3" }
tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.4.4" }
tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.3.5" }
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" }
@@ -23,11 +23,7 @@
"core:window:allow-start-dragging",
"notification:default",
"os:allow-platform",
"dialog:allow-open",
"dialog:allow-ask",
"dialog:allow-save",
"dialog:allow-confirm",
"dialog:allow-message",
"dialog:default",
{
"identifier": "shell:allow-spawn",
"allow": [
+75 -49
View File
@@ -8,6 +8,8 @@
let filter = null;
let multiple = false;
let directory = false;
let pickerMode = "document";
let fileAccessMode = "scoped";
function arrayBufferToBase64(buffer, callback) {
var blob = new Blob([buffer], {
@@ -41,7 +43,7 @@
}
async function msg() {
await message("Tauri is awesome!");
await message("Tauri is awesome!").then((res) => onMessage(res));
}
async function msgCustom(result) {
@@ -51,53 +53,60 @@
.catch(onMessage);
}
function openDialog() {
open({
title: "My wonderful open dialog",
defaultPath,
filters: filter
? [
{
name: "Tauri Example",
extensions: filter.split(",").map((f) => f.trim()),
},
]
: [],
multiple,
directory,
})
.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 {
onMessage(res);
}
})
.catch(onMessage(res));
}
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,
})
.catch(onMessage);
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>');
}
);
} else {
// Convert byte array to UTF-8 string
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(new Uint8Array(res));
onMessage(text);
}
} else {
onMessage(res);
}
})
}
} catch(exception) {
onMessage(exception)
}
}
function saveDialog() {
@@ -112,7 +121,7 @@
},
]
: [],
})
})
.then(onMessage)
.catch(onMessage);
}
@@ -142,6 +151,23 @@
<input type="checkbox" id="dialog-directory" bind:checked={directory} />
<label for="dialog-directory">Directory</label>
</div>
<div>
<label for="dialog-picker-mode">Picker Mode:</label>
<select id="dialog-picker-mode" bind:value={pickerMode}>
<option value="">None</option>
<option value="media">Media</option>
<option value="image">Image</option>
<option value="video">Video</option>
<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">
@@ -156,4 +182,4 @@
<button class="btn" id="message-dialog" on:click={msg}>Message</button>
<button class="btn" id="message-dialog" on:click={msgCustom}>Message (custom)</button>
</div>
</div>
+44 -3
View File
@@ -1,5 +1,5 @@
<script>
import { download, upload } from '@tauri-apps/plugin-upload'
import { download, upload, HttpMethod } from '@tauri-apps/plugin-upload'
import { open } from '@tauri-apps/plugin-dialog'
import { JsonView } from '@zerodevx/svelte-json-view'
import { appDataDir } from '@tauri-apps/api/path'
@@ -16,6 +16,22 @@
let uploadUrl = 'https://httpbin.org/post'
let uploadFilePath = ''
let uploadMethod = HttpMethod.Post
// Update URL when method changes
$: {
switch (uploadMethod) {
case HttpMethod.Post:
uploadUrl = 'https://httpbin.org/post'
break
case HttpMethod.Put:
uploadUrl = 'https://httpbin.org/put'
break
case HttpMethod.Patch:
uploadUrl = 'https://httpbin.org/patch'
break
}
}
let uploadProgress = null
let uploadResult = null
let isUploading = false
@@ -197,7 +213,8 @@
},
new Map([
['User-Agent', 'Tauri Upload Plugin Demo']
])
]),
uploadMethod
)
uploadResult = {
@@ -340,12 +357,36 @@
</div>
</div>
<div>
<label for="upload-method" class="block text-sm font-medium text-gray-700 mb-1">HTTP Method:</label>
<select
id="upload-method"
bind:value={uploadMethod}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
disabled={isUploading}
>
<option value={HttpMethod.Post}>POST</option>
<option value={HttpMethod.Put}>PUT</option>
<option value={HttpMethod.Patch}>PATCH</option>
</select>
<p class="text-xs text-gray-500 mt-1">Choose the HTTP method for the upload request</p>
</div>
<div class="bg-blue-50 border border-blue-200 p-3 rounded-md">
<div class="text-sm text-blue-800">
<strong>Upload Configuration:</strong>
<div class="font-mono text-xs mt-1">
Method: {uploadMethod} | URL: {uploadUrl || 'Not set'}
</div>
</div>
</div>
<button
on:click={startUpload}
class="w-full px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isUploading || !uploadUrl || !uploadFilePath}
>
{isUploading ? 'Uploading...' : 'Upload File'}
{isUploading ? `Uploading (${uploadMethod})...` : `Upload File (${uploadMethod})`}
</button>
{#if uploadProgress}
+8 -9
View File
@@ -11,19 +11,18 @@
"example:api:dev": "pnpm run --filter \"api\" tauri dev"
},
"devDependencies": {
"@eslint/js": "9.38.0",
"@eslint/js": "10.0.1",
"@rollup/plugin-node-resolve": "16.0.3",
"@rollup/plugin-terser": "0.4.4",
"@rollup/plugin-terser": "1.0.0",
"@rollup/plugin-typescript": "12.3.0",
"covector": "^0.12.4",
"eslint": "9.38.0",
"eslint": "10.2.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-security": "3.0.1",
"prettier": "3.6.2",
"rollup": "4.52.5",
"eslint-plugin-security": "4.0.0",
"prettier": "3.8.1",
"rollup": "4.59.0",
"tslib": "2.8.1",
"typescript": "5.9.3",
"typescript-eslint": "8.46.2"
"typescript": "6.0.3",
"typescript-eslint": "8.58.2"
},
"minimumReleaseAge": 4320,
"pnpm": {
+1 -1
View File
@@ -24,6 +24,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.8.0"
"@tauri-apps/api": "^2.11.0"
}
}
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+11
View File
@@ -1,5 +1,16 @@
# 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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-barcode-scanner"
version = "2.4.2"
version = "2.4.4"
description = "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS"
edition = { workspace = true }
authors = { workspace = true }
@@ -54,7 +54,6 @@ 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 {
@@ -354,17 +353,6 @@ 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) {
@@ -380,9 +368,7 @@ 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) {
if (!activity.shouldShowRequestPermissionRationale(PERMISSION_NAME)) {
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.DENIED)
}
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.DENIED)
} else {
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.GRANTED)
}
@@ -401,20 +387,12 @@ 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) {
if (firstPermissionRequest() || activity.shouldShowRequestPermissionRationale(
PERMISSION_NAME
)
) {
markFirstPermissionRequest()
requestPermissionForAlias(
PERMISSION_ALIAS_CAMERA,
invoke,
"cameraPermissionCallback"
)
return
} else {
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.DENIED)
}
requestPermissionForAlias(
PERMISSION_ALIAS_CAMERA,
invoke,
"cameraPermissionCallback"
)
return
} else {
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.GRANTED)
}
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_BARCODE_SCANNER__=function(n){"use strict";async function a(n,a={},e){return window.__TAURI_INTERNALS__.invoke(n,a,e)}var e;return"function"==typeof SuppressedError&&SuppressedError,n.Format=void 0,(e=n.Format||(n.Format={})).QRCode="QR_CODE",e.UPC_A="UPC_A",e.UPC_E="UPC_E",e.EAN8="EAN_8",e.EAN13="EAN_13",e.Code39="CODE_39",e.Code93="CODE_93",e.Code128="CODE_128",e.Codabar="CODABAR",e.ITF="ITF",e.Aztec="AZTEC",e.DataMatrix="DATA_MATRIX",e.PDF417="PDF_417",e.GS1DataBar="GS1_DATA_BAR",e.GS1DataBarLimited="GS1_DATA_BAR_LIMITED",e.GS1DataBarExpanded="GS1_DATA_BAR_EXPANDED",n.cancel=async function(){await a("plugin:barcode-scanner|cancel")},n.checkPermissions=async function(){return await async function(n){return a(`plugin:${n}|check_permissions`)}("barcode-scanner").then((n=>n.camera))},n.openAppSettings=async function(){await a("plugin:barcode-scanner|open_app_settings")},n.requestPermissions=async function(){return await async function(n){return a(`plugin:${n}|request_permissions`)}("barcode-scanner").then((n=>n.camera))},n.scan=async function(n){return await a("plugin:barcode-scanner|scan",{...n})},n}({});Object.defineProperty(window.__TAURI__,"barcodeScanner",{value:__TAURI_PLUGIN_BARCODE_SCANNER__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_BARCODE_SCANNER__=function(n){"use strict";async function a(n,a={},e){return window.__TAURI_INTERNALS__.invoke(n,a,e)}var e;return"function"==typeof SuppressedError&&SuppressedError,n.Format=void 0,(e=n.Format||(n.Format={})).QRCode="QR_CODE",e.UPC_A="UPC_A",e.UPC_E="UPC_E",e.EAN8="EAN_8",e.EAN13="EAN_13",e.Code39="CODE_39",e.Code93="CODE_93",e.Code128="CODE_128",e.Codabar="CODABAR",e.ITF="ITF",e.Aztec="AZTEC",e.DataMatrix="DATA_MATRIX",e.PDF417="PDF_417",e.GS1DataBar="GS1_DATA_BAR",e.GS1DataBarLimited="GS1_DATA_BAR_LIMITED",e.GS1DataBarExpanded="GS1_DATA_BAR_EXPANDED",n.cancel=async function(){await a("plugin:barcode-scanner|cancel")},n.checkPermissions=async function(){return await async function(n){return a(`plugin:${n}|check_permissions`)}("barcode-scanner").then(n=>n.camera)},n.openAppSettings=async function(){await a("plugin:barcode-scanner|open_app_settings")},n.requestPermissions=async function(){return await async function(n){return a(`plugin:${n}|request_permissions`)}("barcode-scanner").then(n=>n.camera)},n.scan=async function(n){return await a("plugin:barcode-scanner|scan",{...n})},n}({});Object.defineProperty(window.__TAURI__,"barcodeScanner",{value:__TAURI_PLUGIN_BARCODE_SCANNER__})}
@@ -191,6 +191,7 @@ class BarcodeScannerPlugin: Plugin, AVCaptureMetadataOutputObjectsDelegate {
if self.captureSession != nil {
self.captureSession!.stopRunning()
self.cameraView.removePreviewLayer()
self.cameraView.removeFromSuperview()
self.captureVideoPreviewLayer = nil
self.metaOutput = nil
self.captureSession = nil
@@ -260,27 +261,34 @@ 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
self.captureSession!.startRunning()
DispatchQueue.main.async {
self.captureSession!.startRunning()
}
self.isScanning = true
}
@@ -297,6 +305,13 @@ 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" {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-barcode-scanner",
"version": "2.4.2",
"version": "2.4.4",
"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.8.0"
"@tauri-apps/api": "^2.11.0"
}
}
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+1 -1
View File
@@ -25,6 +25,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.8.0"
"@tauri-apps/api": "^2.11.0"
}
}
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+1 -1
View File
@@ -24,6 +24,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.8.0"
"@tauri-apps/api": "^2.11.0"
}
}
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_CLIPBOARD_MANAGER__=function(e){"use strict";var n;async function t(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}"function"==typeof SuppressedError&&SuppressedError;class r{get rid(){return function(e,n,t,r){if("function"==typeof n?e!==n||!r:!n.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===t?r:"a"===t?r.call(e):r?r.value:n.get(e)}(this,n,"f")}constructor(e){n.set(this,void 0),function(e,n,t){if("function"==typeof n||!n.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");n.set(e,t)}(this,n,e)}async close(){return t("plugin:resources|close",{rid:this.rid})}}n=new WeakMap;class a extends r{constructor(e){super(e)}static async new(e,n,r){return t("plugin:image|new",{rgba:i(e),width:n,height:r}).then((e=>new a(e)))}static async fromBytes(e){return t("plugin:image|from_bytes",{bytes:i(e)}).then((e=>new a(e)))}static async fromPath(e){return t("plugin:image|from_path",{path:e}).then((e=>new a(e)))}async rgba(){return t("plugin:image|rgba",{rid:this.rid}).then((e=>new Uint8Array(e)))}async size(){return t("plugin:image|size",{rid:this.rid})}}function i(e){return null==e?null:"string"==typeof e?e:e instanceof a?e.rid:e}return e.clear=async function(){await t("plugin:clipboard-manager|clear")},e.readImage=async function(){return await t("plugin:clipboard-manager|read_image").then((e=>new a(e)))},e.readText=async function(){return await t("plugin:clipboard-manager|read_text")},e.writeHtml=async function(e,n){await t("plugin:clipboard-manager|write_html",{html:e,altText:n})},e.writeImage=async function(e){await t("plugin:clipboard-manager|write_image",{image:i(e)})},e.writeText=async function(e,n){await t("plugin:clipboard-manager|write_text",{label:n?.label,text:e})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_PLUGIN_CLIPBOARD_MANAGER__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_CLIPBOARD_MANAGER__=function(e){"use strict";var n;async function t(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}"function"==typeof SuppressedError&&SuppressedError;class r{get rid(){return function(e,n,t,r){if("function"==typeof n?e!==n||!r:!n.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===t?r:"a"===t?r.call(e):r?r.value:n.get(e)}(this,n,"f")}constructor(e){n.set(this,void 0),function(e,n,t){if("function"==typeof n||!n.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");n.set(e,t)}(this,n,e)}async close(){return t("plugin:resources|close",{rid:this.rid})}}n=new WeakMap;class a extends r{constructor(e){super(e)}static async new(e,n,r){return t("plugin:image|new",{rgba:i(e),width:n,height:r}).then(e=>new a(e))}static async fromBytes(e){return t("plugin:image|from_bytes",{bytes:i(e)}).then(e=>new a(e))}static async fromPath(e){return t("plugin:image|from_path",{path:e}).then(e=>new a(e))}async rgba(){return t("plugin:image|rgba",{rid:this.rid}).then(e=>new Uint8Array(e))}async size(){return t("plugin:image|size",{rid:this.rid})}}function i(e){return null==e?null:"string"==typeof e?e:e instanceof a?e.rid:e}return e.clear=async function(){await t("plugin:clipboard-manager|clear")},e.readImage=async function(){return await t("plugin:clipboard-manager|read_image").then(e=>new a(e))},e.readText=async function(){return await t("plugin:clipboard-manager|read_text")},e.writeHtml=async function(e,n){await t("plugin:clipboard-manager|write_html",{html:e,altText:n})},e.writeImage=async function(e){await t("plugin:clipboard-manager|write_image",{image:i(e)})},e.writeText=async function(e,n){await t("plugin:clipboard-manager|write_text",{label:n?.label,text:e})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_PLUGIN_CLIPBOARD_MANAGER__})}
+1 -1
View File
@@ -24,6 +24,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.8.0"
"@tauri-apps/api": "^2.11.0"
}
}
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+13
View File
@@ -1,5 +1,18 @@
# Changelog
## \[2.4.8]
- [`024ec0c2`](https://github.com/tauri-apps/plugins-workspace/commit/024ec0c29c20cf94579dab9b79d6be0da61a8daa) ([#3214](https://github.com/tauri-apps/plugins-workspace/pull/3214) by [@joshIsCoding](https://github.com/tauri-apps/plugins-workspace/../../joshIsCoding)) Account for differing Android VIEW intent in ChromeOS, fixing deep-link behaviour on Chromium platforms.
- [`015e817c`](https://github.com/tauri-apps/plugins-workspace/commit/015e817cf2d7f66c1b9268606af8318dfe0bc4ee) ([#3186](https://github.com/tauri-apps/plugins-workspace/pull/3186) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Validate Android new intent is actually a deep link before triggering the onOpenUrl event.
## \[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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-deep-link"
version = "2.4.5"
version = "2.4.8"
description = "Set your Tauri application as the default handler for an URL"
authors = { workspace = true }
license = { workspace = true }
@@ -6,9 +6,8 @@ package app.tauri.deep_link
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.PatternMatcher
import android.webkit.WebView
import app.tauri.Logger
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
@@ -16,23 +15,43 @@ 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
}
private fun isViewIntent(action: String?): Boolean {
return action == Intent.ACTION_VIEW || action == "org.chromium.arc.intent.action.VIEW"
}
@Command
fun getCurrent(invoke: Invoke) {
val ret = JSObject()
@@ -51,27 +70,105 @@ class DeepLinkPlugin(private val activity: Activity): Plugin(activity) {
override fun load(webView: WebView) {
instance = this
val intent = activity.intent
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)
}
config = getConfig(PluginConfig::class.java)
super.load(webView)
this.webView = webView
val intent = activity.intent
if (isViewIntent(intent.action) && 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)
}
}
}
override fun onNewIntent(intent: Intent) {
if (intent.action == Intent.ACTION_VIEW) {
this.currentUrl = intent.data.toString()
val event = JSObject()
event.put("url", this.currentUrl)
this.channel?.send(event)
if (isViewIntent(intent.action) && 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)
}
}
}
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
}
}
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_DEEP_LINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function i(e,t,i){const _={kind:"Any"};return r("plugin:event|listen",{event:e,target:_,handler:n(t)}).then((n=>async()=>async function(e,n){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(e,n),await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(t||(t={})),e.getCurrent=async function(){return await r("plugin:deep-link|get_current")},e.isRegistered=async function(e){return await r("plugin:deep-link|is_registered",{protocol:e})},e.onOpenUrl=async function(e){return await i("deep-link://new-url",(n=>{e(n.payload)}))},e.register=async function(e){return await r("plugin:deep-link|register",{protocol:e})},e.unregister=async function(e){return await r("plugin:deep-link|unregister",{protocol:e})},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEP_LINK__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_DEEP_LINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function i(e,t,i){const _={kind:"Any"};return r("plugin:event|listen",{event:e,target:_,handler:n(t)}).then(n=>async()=>async function(e,n){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(e,n),await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n))}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_SUSPENDED="tauri://suspended",e.WINDOW_RESUMED="tauri://resumed",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(t||(t={})),e.getCurrent=async function(){return await r("plugin:deep-link|get_current")},e.isRegistered=async function(e){return await r("plugin:deep-link|is_registered",{protocol:e})},e.onOpenUrl=async function(e){return await i("deep-link://new-url",n=>{e(n.payload)})},e.register=async function(e){return await r("plugin:deep-link|register",{protocol:e})},e.unregister=async function(e){return await r("plugin:deep-link|unregister",{protocol:e})},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEP_LINK__})}
+4 -8
View File
@@ -25,6 +25,8 @@ fn intent_filter(domain: &AssociatedDomain) -> String {
format!(
r#"<intent-filter {auto_verify}>
<action android:name="android.intent.action.VIEW" />
<!-- ChromeOS ARC++ uses a different action for deep links -->
<action android:name="org.chromium.arc.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
{schemes}
@@ -137,13 +139,7 @@ fn main() {
let deep_link_domains = config
.mobile
.iter()
.filter_map(|domain| {
if domain.is_app_link() {
return None;
}
Some(domain)
})
.filter(|domain| !domain.is_app_link())
.collect::<Vec<_>>();
if deep_link_domains.is_empty() {
@@ -177,7 +173,7 @@ fn main() {
);
dict.insert(
"CFBundleURLName".into(),
format!("{}", domain.scheme[0]).into(),
domain.scheme[0].clone().into(),
);
plist::Value::Dictionary(dict)
})
@@ -1,5 +1,23 @@
# Changelog
## \[2.2.11]
### Dependencies
- Upgraded to `deep-link-js@2.4.8`
## \[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
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "deep-link-example",
"private": true,
"version": "2.2.8",
"version": "2.2.11",
"type": "module",
"scripts": {
"dev": "vite",
@@ -10,12 +10,12 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "2.9.0",
"@tauri-apps/plugin-deep-link": "2.4.5"
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-deep-link": "2.4.8"
},
"devDependencies": {
"@tauri-apps/cli": "2.9.1",
"typescript": "^5.7.3",
"vite": "^7.0.7"
"@tauri-apps/cli": "2.11.0",
"typescript": "^6.0.0",
"vite": "^8.0.1"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-deep-link",
"version": "2.4.5",
"version": "2.4.8",
"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.8.0"
"@tauri-apps/api": "^2.11.0"
},
"devDependencies": {
"@tauri-apps/cli": "2.9.1"
"@tauri-apps/cli": "2.11.0"
}
}
+1 -1
View File
@@ -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");
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+24
View File
@@ -1,5 +1,29 @@
# Changelog
## \[2.7.0]
- [`24154472`](https://github.com/tauri-apps/plugins-workspace/commit/24154472a6710a690173df0a121125d1f1b871e8) ([#3287](https://github.com/tauri-apps/plugins-workspace/pull/3287) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Re-use `message` command in Rust side for `ask` and `confirm` commands, `allow-ask` and `allow-confirm` permissions are now aliases to `allow-message`
### Dependencies
- Upgraded to `fs-js@2.5.0`
## \[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.
+9 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-dialog"
version = "2.4.2"
version = "2.7.0"
description = "Native system dialogs for opening and saving files along with message dialogs on your Tauri application."
edition = { workspace = true }
authors = { workspace = true }
@@ -9,6 +9,11 @@ rust-version = { workspace = true }
repository = { workspace = true }
links = "tauri-plugin-dialog"
[features]
default = ["gtk3"]
xdg-portal = ["rfd/xdg-portal", "rfd/tokio", "rfd/wayland"]
gtk3 = ["rfd/gtk3"]
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu", "x86_64-linux-android"]
@@ -32,15 +37,14 @@ tauri = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
tauri-plugin-fs = { path = "../fs", version = "2.4.4" }
tauri-plugin-fs = { path = "../fs", version = "2.5.0" }
[target.'cfg(target_os = "ios")'.dependencies]
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.15", default-features = false, features = [
"tokio",
"gtk3",
rfd = { version = "0.16", default-features = false, features = [
"common-controls-v6",
] }
raw-window-handle = "0.6"
+18
View File
@@ -31,6 +31,24 @@ tauri-plugin-dialog = "2.0.0"
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
```
### Linux XDG Desktop Portal Support
By default, this plugin uses gtk to show dialogs, however since `v2.5.0` you can switch to using [XDG Desktop Portal](https://flatpak.github.io/xdg-desktop-portal/) by adding the following to your `Cargo.toml` file:
```toml
[dependencies]
tauri-plugin-dialog = { version = "2.5.0", default-features = false, features = ["xdg-portal"] }
# alternatively with Git:
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2", default-features = false, features = ["xdg-portal"] }
```
Do note if you use the `xdg-portal` feature, you need to ensure that [`zenity`](https://gitlab.gnome.org/GNOME/zenity) and an [XDG Desktop Portal backend](https://flatpak.github.io/xdg-desktop-portal#using-portals) is installed with your program.
For more information, see [XDG Desktop Portal documentation](https://flatpak.github.io/xdg-desktop-portal/) and [`rfd` documentation](https://docs.rs/rfd/latest/rfd#xdg-desktop-portal-backend).
### JavaScript
You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
```sh
@@ -31,6 +31,7 @@ class Filter {
class FilePickerOptions {
lateinit var filters: Array<Filter>
var multiple: Boolean? = null
var pickerMode: String? = null
}
@InvokeArg
@@ -61,10 +62,19 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
// TODO: ACTION_OPEN_DOCUMENT ??
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
if (parsedTypes.isNotEmpty()) {
if (args.pickerMode == "image") {
intent.type = "image/*"
} else if (args.pickerMode == "video") {
intent.type = "video/*"
} else if (args.pickerMode == "media") {
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("video/*", "image/*"))
} else if (parsedTypes.isNotEmpty()) {
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_MIME_TYPES, parsedTypes)
} else {
intent.type = "*/*"
}
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, args.multiple ?: false)
+1 -1
View File
@@ -1 +1 @@
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__})}
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__})}
+1 -1
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
const COMMANDS: &[&str] = &["open", "save", "message", "ask", "confirm"];
const COMMANDS: &[&str] = &["open", "save", "message"];
fn main() {
let result = tauri_plugin::Builder::new(COMMANDS)
+114 -23
View File
@@ -14,6 +14,16 @@ interface DialogFilter {
name: string
/**
* Extensions to filter, without a `.` prefix.
*
* **Note:** Mobile platforms have different APIs for filtering that may not support extensions.
* iOS: Extensions are supported in the document picker, but not in the media picker.
* Android: Extensions are not supported.
*
* For these platforms, MIME types are the primary way to filter files, as opposed to extensions.
* This means the string values here labeled as `extensions` may also be a MIME type.
* This property name of `extensions` is being kept for backwards compatibility, but this may be revisited to
* specify the difference between extension or MIME type filtering.
*
* @example
* ```typescript
* extensions: ['svg', 'png']
@@ -30,7 +40,14 @@ interface DialogFilter {
interface OpenDialogOptions {
/** The title of the dialog window (desktop only). */
title?: string
/** The filters of the dialog. */
/**
* The filters of the dialog.
* On mobile platforms, if either:
* A) the {@linkcode pickerMode} is set to `media`, `image`, or `video`
* -- or --
* B) the filters include **only** either image or video mime types, the media picker will be displayed.
* Otherwise, the document picker will be displayed.
*/
filters?: DialogFilter[]
/**
* Initial directory or file path.
@@ -52,6 +69,38 @@ interface OpenDialogOptions {
recursive?: boolean
/** Whether to allow creating directories in the dialog. Enabled by default. **macOS Only** */
canCreateDirectories?: boolean
/**
* The preferred mode of the dialog.
* This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers.
* If not provided, the dialog will automatically choose the best mode based on the MIME types or extensions of the {@linkcode filters}.
* 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
}
/**
@@ -77,6 +126,26 @@ interface SaveDialogOptions {
canCreateDirectories?: boolean
}
/**
* The preferred mode of the dialog.
* This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers.
* On desktop, this option is ignored.
* If not provided, the dialog will automatically choose the best mode based on the MIME types or extensions of the {@linkcode filters}.
*
* **Note:** This option is only supported on iOS 14 and above. This parameter is ignored on iOS 13 and below.
*/
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.
*
@@ -336,6 +405,18 @@ 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
@@ -358,18 +439,17 @@ async function message(
options?: string | MessageDialogOptions
): Promise<MessageDialogResult> {
const opts = typeof options === 'string' ? { title: options } : options
return invoke<MessageDialogResult>('plugin:dialog|message', {
message: message.toString(),
title: opts?.title?.toString(),
kind: opts?.kind,
okButtonLabel: opts?.okLabel?.toString(),
buttons: buttonsToRust(opts?.buttons)
})
if (opts && !opts.buttons && opts.okLabel) {
opts.buttons = { ok: opts.okLabel }
}
return messageCommand(message, opts)
}
/**
* 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';
@@ -389,17 +469,24 @@ async function ask(
options?: string | ConfirmDialogOptions
): Promise<boolean> {
const opts = typeof options === 'string' ? { title: options } : options
return await invoke('plugin:dialog|ask', {
message: message.toString(),
title: opts?.title?.toString(),
kind: opts?.kind,
yesButtonLabel: opts?.okLabel?.toString(),
noButtonLabel: opts?.cancelLabel?.toString()
})
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
)
}
/**
* 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';
@@ -419,13 +506,17 @@ async function confirm(
options?: string | ConfirmDialogOptions
): Promise<boolean> {
const opts = typeof options === 'string' ? { title: options } : options
return await invoke('plugin:dialog|confirm', {
message: message.toString(),
title: opts?.title?.toString(),
kind: opts?.kind,
okButtonLabel: opts?.okLabel?.toString(),
cancelButtonLabel: opts?.cancelLabel?.toString()
})
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
)
}
export type {
+116 -47
View File
@@ -8,6 +8,7 @@ import PhotosUI
import SwiftRs
import Tauri
import UIKit
import UniformTypeIdentifiers
import WebKit
enum FilePickerEvent {
@@ -32,6 +33,8 @@ struct FilePickerOptions: Decodable {
var multiple: Bool?
var filters: [Filter]?
var defaultPath: String?
var pickerMode: PickerMode?
var fileAccessMode: FileAccessMode?
}
struct SaveFileDialogOptions: Decodable {
@@ -39,6 +42,18 @@ struct SaveFileDialogOptions: Decodable {
var defaultPath: String?
}
enum FileAccessMode: String, Decodable {
case copy
case scoped
}
enum PickerMode: String, Decodable {
case document
case media
case image
case video
}
class DialogPlugin: Plugin {
var filePickerController: FilePickerController!
@@ -47,31 +62,12 @@ class DialogPlugin: Plugin {
override init() {
super.init()
filePickerController = FilePickerController(self)
}
@objc public func showFilePicker(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(FilePickerOptions.self)
let parsedTypes = parseFiltersOption(args.filters ?? [])
var isMedia = !parsedTypes.isEmpty
var uniqueMimeType: Bool? = nil
var mimeKind: String? = nil
if !parsedTypes.isEmpty {
uniqueMimeType = true
for mime in parsedTypes {
let kind = mime.components(separatedBy: "/")[0]
if kind != "image" && kind != "video" {
isMedia = false
}
if mimeKind == nil {
mimeKind = kind
} else if mimeKind != kind {
uniqueMimeType = false
}
}
}
onFilePickerResult = { (event: FilePickerEvent) -> Void in
switch event {
case .selected(let urls):
@@ -83,49 +79,59 @@ class DialogPlugin: Plugin {
}
}
if uniqueMimeType == true || isMedia {
DispatchQueue.main.async {
if #available(iOS 14, *) {
if #available(iOS 14, *) {
let parsedTypes = parseFiltersOption(args.filters ?? [])
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" })
// If the picker mode is media, images, or videos, we always want to show the media picker regardless of what's in the filters.
// Otherwise, if the filters A) do not include non-media types and B) include either image or video, we want to show the media picker.
if args.pickerMode == .media
|| args.pickerMode == .image
|| args.pickerMode == .video
|| (!filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo))
{
DispatchQueue.main.async {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.selectionLimit = (args.multiple ?? false) ? 0 : 1
if uniqueMimeType == true {
if mimeKind == "image" {
configuration.filter = .images
} else if mimeKind == "video" {
configuration.filter = .videos
}
// If the filters include image or video, use the appropriate filter.
// If both are true, don't define a filter, which means we will display all media.
if args.pickerMode == .image || (filtersIncludeImage && !filtersIncludeVideo) {
configuration.filter = .images
} else if args.pickerMode == .video || (filtersIncludeVideo && !filtersIncludeImage) {
configuration.filter = .videos
}
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self.filePickerController
picker.modalPresentationStyle = .fullScreen
self.presentViewController(picker)
} else {
let picker = UIImagePickerController()
picker.delegate = self.filePickerController
}
} else {
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)
if uniqueMimeType == true && mimeKind == "image" {
picker.sourceType = .photoLibrary
if let defaultPath = args.defaultPath {
picker.directoryURL = URL(string: defaultPath)
}
picker.sourceType = .photoLibrary
picker.delegate = self.filePickerController
picker.allowsMultipleSelection = args.multiple ?? false
picker.modalPresentationStyle = .fullScreen
self.presentViewController(picker)
}
}
} else {
let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes
DispatchQueue.main.async {
let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
if let defaultPath = args.defaultPath {
picker.directoryURL = URL(string: defaultPath)
}
picker.delegate = self.filePickerController
picker.allowsMultipleSelection = args.multiple ?? false
picker.modalPresentationStyle = .fullScreen
self.presentViewController(picker)
}
showFilePickerLegacy(args: args)
}
}
@@ -173,7 +179,68 @@ class DialogPlugin: Plugin {
self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil)
}
private func parseFiltersOption(_ filters: [Filter]) -> [String] {
@available(iOS 14, *)
private func parseFiltersOption(_ filters: [Filter]) -> [UTType] {
var parsedTypes: [UTType] = []
for filter in filters {
for ext in filter.extensions ?? [] {
// We need to support extensions as well as MIME types.
if let utType = UTType(mimeType: ext) {
parsedTypes.append(utType)
} else if let utType = UTType(filenameExtension: ext) {
parsedTypes.append(utType)
}
}
}
return parsedTypes
}
/// This function is only used for iOS < 14, and should be removed if/when the deployment target is raised to 14.
private func showFilePickerLegacy(args: FilePickerOptions) {
let parsedTypes = parseFiltersOptionLegacy(args.filters ?? [])
var filtersIncludeImage: Bool = false
var filtersIncludeVideo: Bool = false
var filtersIncludeNonMedia: Bool = false
if !parsedTypes.isEmpty {
let mimeKinds = Set(parsedTypes.map { $0.components(separatedBy: "/")[0] })
filtersIncludeImage = mimeKinds.contains("image")
filtersIncludeVideo = mimeKinds.contains("video")
filtersIncludeNonMedia = mimeKinds.contains(where: { $0 != "image" && $0 != "video" })
}
if !filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo) {
DispatchQueue.main.async {
let picker = UIImagePickerController()
picker.delegate = self.filePickerController
if filtersIncludeImage && !filtersIncludeVideo {
picker.sourceType = .photoLibrary
}
picker.modalPresentationStyle = .fullScreen
self.presentViewController(picker)
}
} else {
let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes
DispatchQueue.main.async {
let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
if let defaultPath = args.defaultPath {
picker.directoryURL = URL(string: defaultPath)
}
picker.delegate = self.filePickerController
picker.allowsMultipleSelection = args.multiple ?? false
picker.modalPresentationStyle = .fullScreen
self.presentViewController(picker)
}
}
}
/// This function is only used for iOS < 14, and should be removed if/when the deployment target is raised to 14.
private func parseFiltersOptionLegacy(_ filters: [Filter]) -> [String] {
var parsedTypes: [String] = []
for filter in filters {
for ext in filter.extensions ?? [] {
@@ -186,6 +253,7 @@ class DialogPlugin: Plugin {
parsedTypes.append(utType)
}
}
return parsedTypes
}
@@ -236,6 +304,7 @@ class DialogPlugin: Plugin {
manager.viewController?.present(alert, animated: true, completion: nil)
}
}
}
@_cdecl("init_plugin_dialog")
@@ -95,16 +95,33 @@ 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 apps 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 {
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
}
@@ -119,8 +136,7 @@ public class FilePickerController: NSObject {
extension FilePickerController: UIDocumentPickerDelegate {
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
do {
let temporaryUrls = try urls.map { try saveTemporaryFile($0) }
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
self.plugin.onFilePickerEvent(.selected(urls))
} catch {
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
}
@@ -191,6 +207,8 @@ 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 {
@@ -212,6 +230,8 @@ 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 {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-dialog",
"version": "2.4.2",
"version": "2.7.0",
"license": "MIT OR Apache-2.0",
"authors": [
"Tauri Programme within The Commons Conservancy"
@@ -24,6 +24,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.8.0"
"@tauri-apps/api": "^2.11.0"
}
}
+11
View File
@@ -0,0 +1,11 @@
"$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"]
@@ -1,13 +0,0 @@
# 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"]
@@ -1,13 +0,0 @@
# 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,8 +9,6 @@ All dialog types are enabled.
#### This default permission set includes the following:
- `allow-ask`
- `allow-confirm`
- `allow-message`
- `allow-save`
- `allow-open`
@@ -32,7 +30,7 @@ All dialog types are enabled.
</td>
<td>
Enables the ask command without any pre-configured scope.
Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)
</td>
</tr>
@@ -45,33 +43,7 @@ Enables the ask command without any pre-configured scope.
</td>
<td>
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.
Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)
</td>
</tr>
@@ -151,6 +123,32 @@ 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>
+11
View File
@@ -0,0 +1,11 @@
"$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"]
+1 -7
View File
@@ -11,10 +11,4 @@ All dialog types are enabled.
"""
permissions = [
"allow-ask",
"allow-confirm",
"allow-message",
"allow-save",
"allow-open",
]
permissions = ["allow-message", "allow-save", "allow-open"]
+18 -18
View File
@@ -295,28 +295,16 @@
"type": "string",
"oneOf": [
{
"description": "Enables the ask command without any pre-configured scope.",
"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)",
"type": "string",
"const": "allow-ask",
"markdownDescription": "Enables the ask command without any pre-configured scope."
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"description": "Denies the ask command without any pre-configured scope.",
"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)",
"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."
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"description": "Enables the message command without any pre-configured scope.",
@@ -355,10 +343,22 @@
"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`",
"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)",
"type": "string",
"const": "allow-confirm",
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
},
{
"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)",
"type": "string",
"const": "deny-confirm",
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
},
{
"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-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`"
"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-message`\n- `allow-save`\n- `allow-open`"
}
]
}
+28 -89
View File
@@ -9,8 +9,8 @@ use tauri::{command, Manager, Runtime, State, Window};
use tauri_plugin_fs::FsExt;
use crate::{
Dialog, FileDialogBuilder, FilePath, MessageDialogBuilder, MessageDialogButtons,
MessageDialogKind, MessageDialogResult, Result, CANCEL, NO, OK, YES,
Dialog, FileAccessMode, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind,
MessageDialogResult, PickerMode, Result,
};
#[derive(Serialize)]
@@ -56,6 +56,17 @@ pub struct OpenDialogOptions {
recursive: bool,
/// Whether to allow creating directories in the dialog **macOS Only**
can_create_directories: Option<bool>,
/// The preferred mode of the dialog.
/// This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers.
/// On desktop, this option is ignored.
/// If not provided, the dialog will automatically choose the best mode based on the MIME types of the filters.
#[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.
@@ -127,10 +138,16 @@ pub(crate) async fn open<R: Runtime>(
if let Some(can) = options.can_create_directories {
dialog_builder = dialog_builder.set_can_create_directories(can);
}
if let Some(picker_mode) = options.picker_mode {
dialog_builder = dialog_builder.set_picker_mode(picker_mode);
}
for filter in options.filters {
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
}
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)]
@@ -241,17 +258,20 @@ pub(crate) async fn save<R: Runtime>(
Ok(path.map(|p| p.simplified()))
}
fn message_dialog<R: Runtime>(
#[allow(unused_variables)] window: Window<R>,
#[command]
pub(crate) async fn message<R: Runtime>(
#[allow(unused)] window: Window<R>,
dialog: State<'_, Dialog<R>>,
title: Option<String>,
message: String,
kind: Option<MessageDialogKind>,
buttons: MessageDialogButtons,
) -> MessageDialogBuilder<R> {
buttons: Option<MessageDialogButtons>,
) -> Result<MessageDialogResult> {
let mut builder = dialog.message(message);
builder = builder.buttons(buttons);
if let Some(buttons) = buttons {
builder = builder.buttons(buttons);
}
if let Some(title) = title {
builder = builder.title(title);
@@ -266,86 +286,5 @@ fn message_dialog<R: Runtime>(
builder = builder.kind(kind);
}
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())
Ok(builder.blocking_show_with_result())
}
+80 -11
View File
@@ -9,7 +9,7 @@
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use serde::Serialize;
use serde::{Deserialize, Serialize};
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
@@ -44,9 +44,28 @@ pub use desktop::Dialog;
#[cfg(mobile)]
pub use mobile::Dialog;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum PickerMode {
Document,
Media,
Image,
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 {
@@ -181,8 +200,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::open,
commands::save,
commands::message,
commands::ask,
commands::confirm
])
.setup(|app, api| {
#[cfg(mobile)]
@@ -230,8 +247,8 @@ impl<R: Runtime> MessageDialogBuilder<R> {
dialog,
title: title.into(),
message: message.into(),
kind: Default::default(),
buttons: Default::default(),
kind: MessageDialogKind::default(),
buttons: MessageDialogButtons::default(),
#[cfg(desktop)]
parent: None,
}
@@ -369,6 +386,8 @@ pub struct FileDialogBuilder<R: Runtime> {
pub(crate) file_name: Option<String>,
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>,
}
@@ -380,6 +399,8 @@ pub(crate) struct FileDialogPayload<'a> {
file_name: &'a Option<String>,
filters: &'a Vec<Filter>,
multiple: bool,
picker_mode: &'a Option<PickerMode>,
file_access_mode: &'a Option<FileAccessMode>,
}
// raw window handle :(
@@ -395,6 +416,8 @@ impl<R: Runtime> FileDialogBuilder<R> {
file_name: None,
title: None,
can_create_directories: None,
picker_mode: None,
file_access_mode: None,
#[cfg(desktop)]
parent: None,
}
@@ -406,6 +429,8 @@ impl<R: Runtime> FileDialogBuilder<R> {
file_name: &self.file_name,
filters: &self.filters,
multiple,
picker_mode: &self.picker_mode,
file_access_mode: &self.file_access_mode,
}
}
@@ -466,11 +491,29 @@ impl<R: Runtime> FileDialogBuilder<R> {
self
}
/// Set the picker mode of the dialog.
/// This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers.
/// On desktop, this option is ignored.
/// If not provided, the dialog will automatically choose the best mode based on the MIME types of the filters.
pub fn set_picker_mode(mut self, mode: PickerMode) -> Self {
self.picker_mode.replace(mode);
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,
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
///
/// For usage in other contexts such as commands, prefer [`Self::pick_file`].
/// See [`Self::blocking_pick_file`] for a blocking version for use in other contexts.
///
/// # Examples
///
@@ -490,9 +533,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
}
/// Shows the dialog to select multiple files.
///
/// This is not a blocking operation,
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
///
/// See [`Self::blocking_pick_files`] for a blocking version for use in other contexts.
///
/// # Reading the files
///
/// The file paths cannot be read directly on Android as they are behind a content URI.
@@ -535,9 +581,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
}
/// Shows the dialog to select a single folder.
///
/// This is not a blocking operation,
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
///
/// See [`Self::blocking_pick_folder`] for a blocking version for use in other contexts.
///
/// # Examples
///
/// ```
@@ -557,9 +606,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
}
/// Shows the dialog to select multiple folders.
///
/// This is not a blocking operation,
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
///
/// See [`Self::blocking_pick_folders`] for a blocking version for use in other contexts.
///
/// # Examples
///
/// ```
@@ -583,6 +635,8 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// This is not a blocking operation,
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
///
/// See [`Self::blocking_save_file`] for a blocking version for use in other contexts.
///
/// # Examples
///
/// ```
@@ -604,8 +658,11 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// Blocking APIs.
impl<R: Runtime> FileDialogBuilder<R> {
/// Shows the dialog to select a single file.
///
/// This is a blocking operation,
/// and should *NOT* be used when running on the main thread context.
/// and should *NOT* be used when running on the main thread.
///
/// See [`Self::pick_file`] for a non-blocking version for use in main-thread contexts.
///
/// # Examples
///
@@ -623,8 +680,11 @@ impl<R: Runtime> FileDialogBuilder<R> {
}
/// Shows the dialog to select multiple files.
///
/// This is a blocking operation,
/// and should *NOT* be used when running on the main thread context.
/// and should *NOT* be used when running on the main thread.
///
/// See [`Self::pick_files`] for a non-blocking version for use in main-thread contexts.
///
/// # Examples
///
@@ -642,8 +702,11 @@ impl<R: Runtime> FileDialogBuilder<R> {
}
/// Shows the dialog to select a single folder.
///
/// This is a blocking operation,
/// and should *NOT* be used when running on the main thread context.
/// and should *NOT* be used when running on the main thread.
///
/// See [`Self::pick_folder`] for a non-blocking version for use in main-thread contexts.
///
/// # Examples
///
@@ -662,8 +725,11 @@ impl<R: Runtime> FileDialogBuilder<R> {
}
/// Shows the dialog to select multiple folders.
///
/// This is a blocking operation,
/// and should *NOT* be used when running on the main thread context.
/// and should *NOT* be used when running on the main thread.
///
/// See [`Self::pick_folders`] for a non-blocking version for use in main-thread contexts.
///
/// # Examples
///
@@ -682,8 +748,11 @@ impl<R: Runtime> FileDialogBuilder<R> {
}
/// Shows the dialog to save a file.
///
/// This is a blocking operation,
/// and should *NOT* be used when running on the main thread context.
/// and should *NOT* be used when running on the main thread.
///
/// See [`Self::save_file`] for a non-blocking version for use in main-thread contexts.
///
/// # Examples
///
+2 -7
View File
@@ -6,9 +6,10 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// Types of message, ask and confirm dialogs.
#[non_exhaustive]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
pub enum MessageDialogKind {
/// Information dialog.
#[default]
Info,
/// Warning dialog.
Warning,
@@ -16,12 +17,6 @@ pub enum MessageDialogKind {
Error,
}
impl Default for MessageDialogKind {
fn default() -> Self {
Self::Info
}
}
impl<'de> Deserialize<'de> for MessageDialogKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+12
View File
@@ -1,5 +1,17 @@
# Changelog
## \[2.5.0]
- [`e97a4ded`](https://github.com/tauri-apps/plugins-workspace/commit/e97a4dedab8608d9d57029784ee8a2607ca3a996) ([#3244](https://github.com/tauri-apps/plugins-workspace/pull/3244) by [@SeijiOkuda](https://github.com/tauri-apps/plugins-workspace/../../SeijiOkuda)) Add `encoding` option for `readTextFile` and `readTextFileLines`
- [`f75d21db`](https://github.com/tauri-apps/plugins-workspace/commit/f75d21db3351d6f12adf585c2c797c20ece94f7f) ([#3360](https://github.com/tauri-apps/plugins-workspace/pull/3360) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Removed the dependency on `tauri-utils`'s `build` feature
- [`f5f68063`](https://github.com/tauri-apps/plugins-workspace/commit/f5f68063e459c33522f44a04df3120d9d039ec13) ([#3185](https://github.com/tauri-apps/plugins-workspace/pull/3185) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Enable access for security-scoped resources on iOS by automatically calling `NSURL::startAccessingSecurityScopedResource` on resource access and adding the `stopAccessingSecurityScopedResource` API.
## \[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.
+6 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-fs"
version = "2.4.4"
version = "2.5.0"
description = "Access the file system."
authors = { workspace = true }
license = { workspace = true }
@@ -21,7 +21,7 @@ tauri-plugin = { workspace = true, features = ["build"] }
schemars = { workspace = true }
serde = { workspace = true }
toml = "0.9"
tauri-utils = { workspace = true, features = ["build"] }
tauri-utils = { workspace = true }
[dependencies]
serde = { workspace = true }
@@ -30,6 +30,7 @@ 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
@@ -41,5 +42,8 @@ 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
+2
View File
@@ -104,6 +104,8 @@ const COMMANDS: &[(&str, &[&str])] = &[
// TODO: Remove this in v3
("unwatch", &[]),
("size", &[]),
("start_accessing_security_scoped_resource", &[]),
("stop_accessing_security_scoped_resource", &[]),
];
fn main() {
+101 -6
View File
@@ -5,6 +5,19 @@
/**
* 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
@@ -723,6 +736,8 @@ 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
}
/**
@@ -753,7 +768,7 @@ async function readFile(
}
/**
* Reads and returns the entire contents of a file as UTF-8 string.
* Reads and returns the entire contents of a file as a string using the specified encoding (default: UTF-8).
* @example
* ```typescript
* import { readTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
@@ -777,11 +792,11 @@ async function readTextFile(
const bytes = arr instanceof ArrayBuffer ? arr : Uint8Array.from(arr)
return new TextDecoder().decode(bytes)
return new TextDecoder(options?.encoding ?? 'utf-8').decode(bytes)
}
/**
* Returns an async {@linkcode AsyncIterableIterator} over the lines of a file as UTF-8 string.
* Returns an async {@linkcode AsyncIterableIterator} over the lines of a file, decoded using the specified encoding (default: UTF-8).
* @example
* ```typescript
* import { readTextFileLines, BaseDirectory } from '@tauri-apps/plugin-fs';
@@ -810,10 +825,15 @@ 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: options != null ? { ...options, encoding } : undefined
})
}
@@ -838,7 +858,7 @@ async function readTextFileLines(
return { value: null, done }
}
const line = new TextDecoder().decode(bytes.slice(0, bytes.byteLength))
const line = decoder.decode(bytes.slice(0, bytes.byteLength - 1))
return {
value: line,
@@ -1346,6 +1366,79 @@ 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,
@@ -1394,5 +1487,7 @@ export {
exists,
watch,
watchImmediate,
size
size,
startAccessingSecurityScopedResource,
stopAccessingSecurityScopedResource
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-fs",
"version": "2.4.4",
"version": "2.5.0",
"description": "Access the file system.",
"license": "MIT OR Apache-2.0",
"authors": [
@@ -25,6 +25,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.8.0"
"@tauri-apps/api": "^2.11.0"
}
}
@@ -0,0 +1,13 @@
# 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"]
@@ -0,0 +1,13 @@
# 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"]
@@ -3435,6 +3435,32 @@ 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>
@@ -3461,6 +3487,32 @@ 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>
@@ -1860,6 +1860,18 @@
"const": "deny-size",
"markdownDescription": "Denies the size command without any pre-configured scope."
},
{
"description": "Enables the start_accessing_security_scoped_resource command without any pre-configured scope.",
"type": "string",
"const": "allow-start-accessing-security-scoped-resource",
"markdownDescription": "Enables the start_accessing_security_scoped_resource command without any pre-configured scope."
},
{
"description": "Denies the start_accessing_security_scoped_resource command without any pre-configured scope.",
"type": "string",
"const": "deny-start-accessing-security-scoped-resource",
"markdownDescription": "Denies the start_accessing_security_scoped_resource command without any pre-configured scope."
},
{
"description": "Enables the stat command without any pre-configured scope.",
"type": "string",
@@ -1872,6 +1884,18 @@
"const": "deny-stat",
"markdownDescription": "Denies the stat command without any pre-configured scope."
},
{
"description": "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope.",
"type": "string",
"const": "allow-stop-accessing-security-scoped-resource",
"markdownDescription": "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope."
},
{
"description": "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope.",
"type": "string",
"const": "deny-stop-accessing-security-scoped-resource",
"markdownDescription": "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope."
},
{
"description": "Enables the truncate command without any pre-configured scope.",
"type": "string",
@@ -3,37 +3,31 @@
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{
plugin::{PluginApi, PluginHandle},
AppHandle, Runtime,
};
use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::{models::*, FilePath, OpenOptions};
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "com.plugin.fs";
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_fs);
pub struct Fs<R: Runtime>(tauri::plugin::PluginHandle<R>);
// initializes the Kotlin or Swift plugin classes
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Fs<R>> {
#[cfg(target_os = "android")]
let handle = api
.register_android_plugin(PLUGIN_IDENTIFIER, "FsPlugin")
.unwrap();
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_android - intent - send)?;
Ok(Fs(handle))
}
/// Access to the android-intent-send APIs.
pub struct Fs<R: Runtime>(PluginHandle<R>);
impl<R: Runtime> Fs<R> {
/// Open a file.
///
/// # Platform-specific
///
/// - **iOS**: This method will automatically start accessing a security-scoped resource if the path is a file URL.
/// You must call `stop_accessing_security_scoped_resource` when you're done accessing the file.
pub fn open<P: Into<FilePath>>(
&self,
path: P,
@@ -68,29 +62,25 @@ impl<R: Runtime> Fs<R> {
}
}
#[cfg(target_os = "android")]
fn resolve_content_uri(
&self,
uri: impl Into<String>,
mode: impl Into<String>,
) -> crate::Result<std::fs::File> {
#[cfg(target_os = "android")]
{
let result = self.0.run_mobile_plugin::<GetFileDescriptorResponse>(
"getFileDescriptor",
GetFileDescriptorPayload {
uri: uri.into(),
mode: mode.into(),
},
)?;
if let Some(fd) = result.fd {
Ok(unsafe {
use std::os::fd::FromRawFd;
std::fs::File::from_raw_fd(fd)
})
} else {
unimplemented!()
}
let result = self.0.run_mobile_plugin::<GetFileDescriptorResponse>(
"getFileDescriptor",
GetFileDescriptorPayload {
uri: uri.into(),
mode: mode.into(),
},
)?;
if let Some(fd) = result.fd {
Ok(unsafe {
use std::os::fd::FromRawFd;
std::fs::File::from_raw_fd(fd)
})
} else {
unimplemented!()
}
}
}
+591 -77
View File
@@ -16,6 +16,7 @@ use std::{
borrow::Cow,
fs::File,
io::{BufRead, BufReader, Read, Write},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
str::FromStr,
sync::Mutex,
@@ -70,6 +71,209 @@ 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 {
@@ -84,7 +288,8 @@ pub fn create<R: Runtime>(
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<ResourceId> {
let resolved_path = resolve_path(
let path_ = path.clone();
let resolved_path_handle = resolve_path(
"create",
&webview,
&global_scope,
@@ -92,13 +297,22 @@ pub fn create<R: Runtime>(
path,
options.and_then(|o| o.base_dir),
)?;
let file = File::create(&resolved_path).map_err(|e| {
let file = File::create(&*resolved_path_handle).map_err(|e| {
format!(
"failed to create file at path: {} with error: {e}",
resolved_path.display()
resolved_path_handle.display()
)
})?;
let rid = webview.resources_table().add(StdFileResource::new(file));
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));
Ok(rid)
}
@@ -119,7 +333,7 @@ pub fn open<R: Runtime>(
path: SafeFilePath,
options: Option<OpenOptions>,
) -> CommandResult<ResourceId> {
let (file, _path) = resolve_file(
let file_handle = resolve_file(
"open",
&webview,
&global_scope,
@@ -147,7 +361,9 @@ pub fn open<R: Runtime>(
},
)?;
let rid = webview.resources_table().add(StdFileResource::new(file));
let rid = webview
.resources_table()
.add(StdFileResource::new(file_handle));
Ok(rid)
}
@@ -308,8 +524,8 @@ pub async fn read<R: Runtime>(
len: usize,
) -> CommandResult<tauri::ipc::Response> {
let mut data = vec![0; len];
let file = webview.resources_table().get::<StdFileResource>(rid)?;
let nread = StdFileResource::with_lock(&file, |mut file| file.read(&mut data))
let file: std::sync::Arc<StdFileResource<R>> = webview.resources_table().get(rid)?;
let nread = StdFileResource::with_lock(&file, |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)
@@ -345,7 +561,7 @@ async fn read_file_inner<R: Runtime>(
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<tauri::ipc::Response> {
let (mut file, path) = resolve_file(
let mut file_handle = resolve_file(
permission,
&webview,
&global_scope,
@@ -364,10 +580,10 @@ async fn read_file_inner<R: Runtime>(
let mut contents = Vec::new();
file.read_to_end(&mut contents).map_err(|e| {
file_handle.read_to_end(&mut contents).map_err(|e| {
format!(
"failed to read file as text at path: {} with error: {e}",
path.display()
file_handle.path().display()
)
})?;
@@ -393,6 +609,14 @@ 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>(
@@ -419,7 +643,7 @@ pub fn read_text_file_lines<R: Runtime>(
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
options: Option<ReadTextFileOptions>,
) -> CommandResult<ResourceId> {
let resolved_path = resolve_path(
"read-text-file-lines",
@@ -427,7 +651,7 @@ pub fn read_text_file_lines<R: Runtime>(
&global_scope,
&command_scope,
path,
options.as_ref().and_then(|o| o.base_dir),
options.as_ref().and_then(|o| o.base.base_dir),
)?;
let file = File::open(&resolved_path).map_err(|e| {
@@ -437,12 +661,43 @@ 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));
let rid = webview
.resources_table()
.add(StdLinesResource::new(lines, lf_bytes, cr_bytes));
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>,
@@ -599,8 +854,8 @@ pub async fn seek<R: Runtime>(
whence: SeekMode,
) -> CommandResult<u64> {
use std::io::{Seek, SeekFrom};
let file = webview.resources_table().get::<StdFileResource>(rid)?;
StdFileResource::with_lock(&file, |mut file| {
let file: std::sync::Arc<StdFileResource<R>> = webview.resources_table().get(rid)?;
StdFileResource::with_lock(&file, |file| {
file.seek(match whence {
SeekMode::Start => SeekFrom::Start(offset as u64),
SeekMode::Current => SeekFrom::Current(offset),
@@ -623,7 +878,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, path) = resolve_file(
let file_handle = resolve_file(
permission,
webview,
global_scope,
@@ -637,10 +892,10 @@ fn get_metadata<R: Runtime, F: FnOnce(&PathBuf) -> std::io::Result<std::fs::Meta
},
},
)?;
file.metadata().map_err(|e| {
file_handle.metadata().map_err(|e| {
format!(
"failed to get metadata of path: {} with error: {e}",
path.display()
file_handle.path().display()
)
.into()
})
@@ -747,7 +1002,7 @@ pub fn lstat<R: Runtime>(
#[tauri::command]
pub fn fstat<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> CommandResult<FileInfo> {
let file = webview.resources_table().get::<StdFileResource>(rid)?;
let file: std::sync::Arc<StdFileResource<R>> = webview.resources_table().get(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))
@@ -795,7 +1050,7 @@ pub async fn ftruncate<R: Runtime>(
rid: ResourceId,
len: Option<u64>,
) -> CommandResult<()> {
let file = webview.resources_table().get::<StdFileResource>(rid)?;
let file: std::sync::Arc<StdFileResource<R>> = webview.resources_table().get(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)
@@ -807,8 +1062,8 @@ pub async fn write<R: Runtime>(
rid: ResourceId,
data: Vec<u8>,
) -> CommandResult<usize> {
let file = webview.resources_table().get::<StdFileResource>(rid)?;
StdFileResource::with_lock(&file, |mut file| file.write(&data))
let file: std::sync::Arc<StdFileResource<R>> = webview.resources_table().get(rid)?;
StdFileResource::with_lock(&file, |file| file.write(&data))
.map_err(|e| format!("failed to write bytes to file with error: {e}"))
.map_err(Into::into)
}
@@ -856,7 +1111,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, path) = resolve_file(
let mut file_handle = resolve_file(
permission,
&webview,
&global_scope,
@@ -903,11 +1158,12 @@ async fn write_file_inner<R: Runtime>(
_ => return Err(anyhow::anyhow!("unexpected invoke body").into()),
};
file.write_all(&data)
file_handle
.write_all(&data)
.map_err(|e| {
format!(
"failed to write bytes to file at path: {} with error: {e}",
path.display()
file_handle.path().display()
)
})
.map_err(Into::into)
@@ -993,6 +1249,130 @@ 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;
@@ -1010,7 +1390,7 @@ fn get_dir_size(path: &PathBuf) -> CommandResult<u64> {
Ok(size)
}
#[cfg(not(target_os = "android"))]
#[cfg(desktop)]
pub fn resolve_file<R: Runtime>(
permission: &str,
webview: &Webview<R>,
@@ -1018,7 +1398,7 @@ pub fn resolve_file<R: Runtime>(
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
) -> CommandResult<FileHandle<R>> {
resolve_file_in_fs(
permission,
webview,
@@ -1036,8 +1416,9 @@ fn resolve_file_in_fs<R: Runtime>(
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
let path = resolve_path(
) -> CommandResult<FileHandle<R>> {
let path_ = path.clone();
let resolved_path_handle = resolve_path(
permission,
webview,
global_scope,
@@ -1047,17 +1428,24 @@ fn resolve_file_in_fs<R: Runtime>(
)?;
let file = std::fs::OpenOptions::from(open_options.options)
.open(&path)
.open(&*resolved_path_handle)
.map_err(|e| {
format!(
"failed to open file at path: {} with error: {e}",
path.display()
resolved_path_handle.display()
)
})?;
Ok((file, path))
let app_handle = webview.app_handle().clone();
Ok(FileHandle::new(
file,
PathKind::Handle(resolved_path_handle),
path_,
app_handle,
))
}
#[cfg(target_os = "android")]
#[cfg(mobile)]
pub fn resolve_file<R: Runtime>(
permission: &str,
webview: &Webview<R>,
@@ -1065,16 +1453,23 @@ pub fn resolve_file<R: Runtime>(
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
) -> CommandResult<FileHandle<R>> {
use crate::FsExt;
let path_ = path.clone();
match path {
SafeFilePath::Url(url) => {
let path = url.as_str().into();
let resolved_path = url.as_str().into();
let file = webview
.fs()
.open(SafeFilePath::Url(url), open_options.options)?;
Ok((file, path))
.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,
))
}
SafeFilePath::Path(path) => resolve_file_in_fs(
permission,
@@ -1094,9 +1489,47 @@ pub fn resolve_path<R: Runtime>(
command_scope: &CommandScope<Entry>,
path: SafeFilePath,
base_dir: Option<BaseDirectory>,
) -> CommandResult<PathBuf> {
) -> 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());
}
}
}
}
let path = path.into_path()?;
let path = if let Some(base_dir) = base_dir {
let resolved_path = if let Some(base_dir) = base_dir {
webview.path().resolve(&path, base_dir)?
} else {
path
@@ -1125,23 +1558,24 @@ 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, &path, require_literal_leading_dot)
|| is_forbidden(&scope, &path, require_literal_leading_dot)
if is_forbidden(&fs_scope.scope, &resolved_path, require_literal_leading_dot)
|| is_forbidden(&scope, &resolved_path, require_literal_leading_dot)
{
return Err(CommandError::Plugin(Error::PathForbidden(path)));
return Err(CommandError::Plugin(Error::PathForbidden(resolved_path)));
}
if fs_scope.scope.is_allowed(&path) || scope.is_allowed(&path) {
Ok(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))
} else {
#[cfg(not(debug_assertions))]
return Err(CommandError::Plugin(Error::PathForbidden(path)));
return Err(CommandError::Plugin(Error::PathForbidden(resolved_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",
path.display()
resolved_path.display()
)
)
.map_err(Into::into)
@@ -1187,38 +1621,55 @@ fn is_forbidden<P: AsRef<Path>>(
}
}
struct StdFileResource(Mutex<File>);
struct StdFileResource<R: Runtime>(Mutex<FileHandle<R>>);
impl StdFileResource {
fn new(file: File) -> Self {
Self(Mutex::new(file))
impl<R: Runtime> StdFileResource<R> {
fn new(file_handle: FileHandle<R>) -> Self {
Self(Mutex::new(file_handle))
}
fn with_lock<R, F: FnMut(&File) -> R>(&self, mut f: F) -> R {
let file = self.0.lock().unwrap();
f(&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)
}
}
impl Resource for StdFileResource {}
impl<R: Runtime> Resource for StdFileResource<R> {}
/// Same as [std::io::Lines] but with bytes
struct LinesBytes<T: BufRead>(T);
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,
}
}
}
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();
match self.0.read_until(b'\n', &mut buf) {
// Search for '\n'
match read_until_bytes(&mut self.bytes, &self.lf_bytes, &mut buf) {
Ok(0) => None,
Ok(_n) => {
if buf.last() == Some(&b'\n') {
buf.pop();
if buf.last() == Some(&b'\r') {
buf.pop();
// 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());
}
}
Some(Ok(buf))
}
Err(e) => Some(Err(e)),
@@ -1226,11 +1677,35 @@ 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>) -> Self {
Self(Mutex::new(LinesBytes(lines)))
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 with_lock<R, F: FnMut(&mut LinesBytes<BufReader<File>>) -> R>(&self, mut f: F) -> R {
@@ -1354,21 +1829,60 @@ mod test {
#[test]
fn test_lines_bytes() {
let base = String::from("line 1\nline2\nline 3\nline 4");
let bytes = base.as_bytes();
// UTF-8
{
let base = String::from("line 1\nline2\nline 3\r\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(BufReader::new(bytes))
.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::new(BufReader::new(bytes), vec![b'\n'], vec![b'\r'])
.flatten()
.flat_map(String::from_utf8)
.collect::<String>();
assert_eq!(string1, string2);
assert_eq!(string1, string3);
assert_eq!(string2, string3);
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());
}
}
}
+6
View File
@@ -24,6 +24,12 @@ fn path_or_err<P: Into<FilePath>>(p: P) -> std::io::Result<PathBuf> {
}
impl<R: Runtime> Fs<R> {
/// Open a file.
///
/// # Platform-specific
///
/// - **iOS**: This method will automatically start accessing a security-scoped resource if the path is a file URL.
/// You must call `stop_accessing_security_scoped_resource` when you're done accessing the file.
pub fn open<P: Into<FilePath>>(
&self,
path: P,
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::{FilePath, OpenOptions};
pub struct Fs<R: Runtime> {
_phantom: std::marker::PhantomData<fn() -> R>,
}
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
_api: PluginApi<R, C>,
) -> crate::Result<Fs<R>> {
Ok(Fs {
_phantom: std::marker::PhantomData,
})
}
impl<R: Runtime> Fs<R> {
/// Open a file.
///
/// # Platform-specific
///
/// - **iOS**: This method will automatically start accessing a security-scoped resource if the path is a file URL.
/// You must call [`Self::stop_accessing_security_scoped_resource`] when you're done accessing the file.
pub fn open<P: Into<FilePath>>(
&self,
path: P,
opts: OpenOptions,
) -> std::io::Result<std::fs::File> {
use objc2_foundation::{NSString, NSURL};
match path.into() {
FilePath::Url(url) if url.scheme() == "file" => {
// Handle security-scoped URLs on iOS
let url_string = url.as_str();
let url_nsstring = NSString::from_str(url_string);
// Create NSURL from the URL string
// URLWithString may return None for invalid URLs, but file:// URLs should be valid
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)
// Note: We don't call stopAccessingSecurityScopedResource here because
// the file handle needs to remain accessible while the File is in use.
// The access will be automatically stopped when the app is backgrounded or terminated.
unsafe {
let success = ns_url.startAccessingSecurityScopedResource();
if success {
log::debug!(
"Started accessing security-scoped resource for URL: {}",
url_string
);
} else {
log::warn!(
"Failed to start accessing security-scoped resource for URL: {}",
url_string
);
}
}
} else {
log::debug!("Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", url_string);
}
// Convert URL to path and open the file
let path = url.to_file_path().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid file URL")
})?;
std::fs::OpenOptions::from(opts).open(path)
}
FilePath::Url(_) => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cannot use a non-file URL to load files on iOS",
)),
FilePath::Path(p) => {
// Regular path, no security-scoped resource handling needed
std::fs::OpenOptions::from(opts).open(p)
}
}
}
/// Stops accessing a security-scoped resource for the given file path or 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).
///
/// # Arguments
///
/// * `path` - A file path or URL that was previously accessed via `startAccessingSecurityScopedResource`
///
/// # Returns
///
/// Returns `Ok(())` if successful, or an error if the path/URL is invalid or not a file URL.
pub fn stop_accessing_security_scoped_resource<P: Into<FilePath>>(
&self,
path: P,
) -> crate::Result<()> {
use objc2_foundation::{NSString, NSURL};
let file_path = path.into();
let url_string = match file_path {
FilePath::Url(url) => {
if url.scheme() != "file" {
return Err(crate::Error::InvalidPathUrl);
}
url.as_str().to_string()
}
FilePath::Path(p) => {
// Convert path to file URL
url::Url::from_file_path(&p)
.map_err(|_| crate::Error::InvalidPathUrl)?
.as_str()
.to_string()
}
};
let url_nsstring = NSString::from_str(&url_string);
let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) };
if let Some(ns_url) = ns_url {
// Stop accessing the security-scoped resource
unsafe {
ns_url.stopAccessingSecurityScopedResource();
}
} else {
return Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"failed to create NSURL from URL",
)));
}
Ok(())
}
}
+75 -8
View File
@@ -4,12 +4,17 @@
//! Access the file system.
// TODO(v3): consider redesign the API to implement automatic stopAccessingSecurityScopedResource on iOS
// this likely requires returning a handle to a resource so we can impl Drop for it
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use std::io::Read;
#[cfg(target_os = "ios")]
use std::sync::Mutex;
use serde::Deserialize;
use tauri::{
@@ -19,24 +24,28 @@ use tauri::{
AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent,
};
#[cfg(target_os = "android")]
mod android;
mod commands;
mod config;
#[cfg(not(target_os = "android"))]
#[cfg(desktop)]
mod desktop;
mod error;
mod file_path;
#[cfg(target_os = "android")]
mod mobile;
#[cfg(target_os = "ios")]
mod ios;
#[cfg(target_os = "android")]
mod models;
mod scope;
#[cfg(feature = "watch")]
mod watcher;
#[cfg(not(target_os = "android"))]
pub use desktop::Fs;
#[cfg(target_os = "android")]
pub use mobile::Fs;
pub use android::Fs;
#[cfg(desktop)]
pub use desktop::Fs;
#[cfg(target_os = "ios")]
pub use ios::Fs;
pub use error::Error;
@@ -369,6 +378,56 @@ pub(crate) struct Scope {
pub(crate) require_literal_leading_dot: Option<bool>,
}
/// Tracks which paths have active security-scoped resource access on iOS.
#[cfg(target_os = "ios")]
pub(crate) struct SecurityScopedResources {
/// Set of file URLs that are currently accessing security-scoped resources.
/// The key is the URL string representation.
pub(crate) active_urls: Mutex<std::collections::HashSet<String>>,
}
#[cfg(target_os = "ios")]
impl SecurityScopedResources {
pub(crate) fn new() -> Self {
Self {
active_urls: Mutex::new(std::collections::HashSet::new()),
}
}
pub(crate) fn is_tracked_manually(&self, url: &str) -> bool {
self.active_urls.lock().unwrap().contains(url)
}
pub(crate) fn track_manually(&self, url: String) {
self.active_urls.lock().unwrap().insert(url);
}
pub(crate) fn remove(&self, url: &str) {
self.active_urls.lock().unwrap().remove(url);
}
}
#[cfg(not(target_os = "ios"))]
pub(crate) struct SecurityScopedResources;
#[cfg(not(target_os = "ios"))]
impl SecurityScopedResources {
pub(crate) fn new() -> Self {
Self
}
#[allow(dead_code)] // Used on iOS, but not on other platforms
pub(crate) fn is_tracked_manually(&self, _url: &str) -> bool {
false
}
#[allow(dead_code)] // Used on iOS, but not on other platforms
pub(crate) fn track_manually(&self, _url: String) {}
#[allow(dead_code)] // Used on iOS, but not on other platforms
pub(crate) fn remove(&self, _url: &str) {}
}
pub trait FsExt<R: Runtime> {
fn fs_scope(&self) -> tauri::fs::Scope;
fn try_fs_scope(&self) -> Option<tauri::fs::Scope>;
@@ -417,6 +476,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
commands::write_text_file,
commands::exists,
commands::size,
commands::start_accessing_security_scoped_resource,
commands::stop_accessing_security_scoped_resource,
#[cfg(feature = "watch")]
watcher::watch,
])
@@ -431,13 +492,19 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
#[cfg(target_os = "android")]
{
let fs = mobile::init(app, api)?;
let fs = android::init(app, api)?;
app.manage(fs);
}
#[cfg(not(target_os = "android"))]
#[cfg(target_os = "ios")]
{
let fs = ios::init(app, api)?;
app.manage(fs);
}
#[cfg(desktop)]
app.manage(Fs(app.clone()));
app.manage(scope);
app.manage(SecurityScopedResources::new());
Ok(())
})
.on_event(|app, event| {
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
@@ -49,8 +49,6 @@ public class Geolocation(private val context: Context) {
val lowPrio = if (networkEnabled) Priority.PRIORITY_BALANCED_POWER_ACCURACY else Priority.PRIORITY_LOW_POWER
val prio = if (enableHighAccuracy) Priority.PRIORITY_HIGH_ACCURACY else lowPrio
Logger.error(prio.toString())
LocationServices
.getFusedLocationProviderClient(context)
.getCurrentLocation(prio, null)
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_GEOLOCATION__=function(t){"use strict";function n(t,n,i,e){if("function"==typeof n?t!==n||!e:!n.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?e:"a"===i?e.call(t):e?e.value:n.get(t)}function i(t,n,i,e,s){if("function"==typeof n||!n.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return n.set(t,i),i}var e,s,o,a;"function"==typeof SuppressedError&&SuppressedError;const r="__TAURI_TO_IPC_KEY__";class c{constructor(t){e.set(this,void 0),s.set(this,0),o.set(this,[]),a.set(this,void 0),i(this,e,t||(()=>{})),this.id=function(t,n=!1){return window.__TAURI_INTERNALS__.transformCallback(t,n)}((t=>{const r=t.index;if("end"in t)return void(r==n(this,s,"f")?this.cleanupCallback():i(this,a,r));const c=t.message;if(r==n(this,s,"f")){for(n(this,e,"f").call(this,c),i(this,s,n(this,s,"f")+1);n(this,s,"f")in n(this,o,"f");){const t=n(this,o,"f")[n(this,s,"f")];n(this,e,"f").call(this,t),delete n(this,o,"f")[n(this,s,"f")],i(this,s,n(this,s,"f")+1)}n(this,s,"f")===n(this,a,"f")&&this.cleanupCallback()}else n(this,o,"f")[r]=c}))}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){i(this,e,t)}get onmessage(){return n(this,e,"f")}[(e=new WeakMap,s=new WeakMap,o=new WeakMap,a=new WeakMap,r)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[r]()}}async function _(t,n={},i){return window.__TAURI_INTERNALS__.invoke(t,n,i)}return t.checkPermissions=async function(){return await async function(t){return _(`plugin:${t}|check_permissions`)}("geolocation")},t.clearWatch=async function(t){await _("plugin:geolocation|clear_watch",{channelId:t})},t.getCurrentPosition=async function(t){return await _("plugin:geolocation|get_current_position",{options:t})},t.requestPermissions=async function(t){return await _("plugin:geolocation|request_permissions",{permissions:t})},t.watchPosition=async function(t,n){const i=new c;return i.onmessage=t=>{"string"==typeof t?n(null,t):n(t)},await _("plugin:geolocation|watch_position",{options:t,channel:i}),i.id},t}({});Object.defineProperty(window.__TAURI__,"geolocation",{value:__TAURI_PLUGIN_GEOLOCATION__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_GEOLOCATION__=function(t){"use strict";function n(t,n,i,e){if("function"==typeof n?t!==n||!e:!n.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?e:"a"===i?e.call(t):e?e.value:n.get(t)}function i(t,n,i,e,s){if("function"==typeof n||!n.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return n.set(t,i),i}var e,s,o,a;"function"==typeof SuppressedError&&SuppressedError;const r="__TAURI_TO_IPC_KEY__";class c{constructor(t){e.set(this,void 0),s.set(this,0),o.set(this,[]),a.set(this,void 0),i(this,e,t||(()=>{})),this.id=function(t,n=!1){return window.__TAURI_INTERNALS__.transformCallback(t,n)}(t=>{const r=t.index;if("end"in t)return void(r==n(this,s,"f")?this.cleanupCallback():i(this,a,r));const c=t.message;if(r==n(this,s,"f")){for(n(this,e,"f").call(this,c),i(this,s,n(this,s,"f")+1);n(this,s,"f")in n(this,o,"f");){const t=n(this,o,"f")[n(this,s,"f")];n(this,e,"f").call(this,t),delete n(this,o,"f")[n(this,s,"f")],i(this,s,n(this,s,"f")+1)}n(this,s,"f")===n(this,a,"f")&&this.cleanupCallback()}else n(this,o,"f")[r]=c})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){i(this,e,t)}get onmessage(){return n(this,e,"f")}[(e=new WeakMap,s=new WeakMap,o=new WeakMap,a=new WeakMap,r)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[r]()}}async function _(t,n={},i){return window.__TAURI_INTERNALS__.invoke(t,n,i)}return t.checkPermissions=async function(){return await async function(t){return _(`plugin:${t}|check_permissions`)}("geolocation")},t.clearWatch=async function(t){await _("plugin:geolocation|clear_watch",{channelId:t})},t.getCurrentPosition=async function(t){return await _("plugin:geolocation|get_current_position",{options:t})},t.requestPermissions=async function(t){return await _("plugin:geolocation|request_permissions",{permissions:t})},t.watchPosition=async function(t,n){const i=new c;return i.onmessage=t=>{"string"==typeof t?n(null,t):n(t)},await _("plugin:geolocation|watch_position",{options:t,channel:i}),i.id},t}({});Object.defineProperty(window.__TAURI__,"geolocation",{value:__TAURI_PLUGIN_GEOLOCATION__})}
+1 -1
View File
@@ -24,6 +24,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.8.0"
"@tauri-apps/api": "^2.11.0"
}
}
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+1 -1
View File
@@ -54,7 +54,7 @@ fn main() {
.setup(|app| {
#[cfg(desktop)]
{
use tauri::Manager;
use tauri::Emitter;
use tauri_plugin_global_shortcut::{Code, Modifiers, ShortcutState};
app.handle().plugin(
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_GLOBAL_SHORTCUT__=function(t){"use strict";function e(t,e,s,i){if("function"==typeof e?t!==e||!i:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?i:"a"===s?i.call(t):i?i.value:e.get(t)}function s(t,e,s,i,r){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,s),s}var i,r,n,a;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";class c{constructor(t){i.set(this,void 0),r.set(this,0),n.set(this,[]),a.set(this,void 0),s(this,i,t||(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((t=>{const o=t.index;if("end"in t)return void(o==e(this,r,"f")?this.cleanupCallback():s(this,a,o));const c=t.message;if(o==e(this,r,"f")){for(e(this,i,"f").call(this,c),s(this,r,e(this,r,"f")+1);e(this,r,"f")in e(this,n,"f");){const t=e(this,n,"f")[e(this,r,"f")];e(this,i,"f").call(this,t),delete e(this,n,"f")[e(this,r,"f")],s(this,r,e(this,r,"f")+1)}e(this,r,"f")===e(this,a,"f")&&this.cleanupCallback()}else e(this,n,"f")[o]=c}))}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){s(this,i,t)}get onmessage(){return e(this,i,"f")}[(i=new WeakMap,r=new WeakMap,n=new WeakMap,a=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}async function u(t,e={},s){return window.__TAURI_INTERNALS__.invoke(t,e,s)}return t.isRegistered=async function(t){return await u("plugin:global-shortcut|is_registered",{shortcut:t})},t.register=async function(t,e){const s=new c;return s.onmessage=e,await u("plugin:global-shortcut|register",{shortcuts:Array.isArray(t)?t:[t],handler:s})},t.unregister=async function(t){return await u("plugin:global-shortcut|unregister",{shortcuts:Array.isArray(t)?t:[t]})},t.unregisterAll=async function(){return await u("plugin:global-shortcut|unregister_all",{})},t}({});Object.defineProperty(window.__TAURI__,"globalShortcut",{value:__TAURI_PLUGIN_GLOBAL_SHORTCUT__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_GLOBAL_SHORTCUT__=function(t){"use strict";function e(t,e,s,i){if("function"==typeof e?t!==e||!i:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?i:"a"===s?i.call(t):i?i.value:e.get(t)}function s(t,e,s,i,r){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,s),s}var i,r,n,a;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";class c{constructor(t){i.set(this,void 0),r.set(this,0),n.set(this,[]),a.set(this,void 0),s(this,i,t||(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}(t=>{const o=t.index;if("end"in t)return void(o==e(this,r,"f")?this.cleanupCallback():s(this,a,o));const c=t.message;if(o==e(this,r,"f")){for(e(this,i,"f").call(this,c),s(this,r,e(this,r,"f")+1);e(this,r,"f")in e(this,n,"f");){const t=e(this,n,"f")[e(this,r,"f")];e(this,i,"f").call(this,t),delete e(this,n,"f")[e(this,r,"f")],s(this,r,e(this,r,"f")+1)}e(this,r,"f")===e(this,a,"f")&&this.cleanupCallback()}else e(this,n,"f")[o]=c})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){s(this,i,t)}get onmessage(){return e(this,i,"f")}[(i=new WeakMap,r=new WeakMap,n=new WeakMap,a=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}async function u(t,e={},s){return window.__TAURI_INTERNALS__.invoke(t,e,s)}return t.isRegistered=async function(t){return await u("plugin:global-shortcut|is_registered",{shortcut:t})},t.register=async function(t,e){const s=new c;return s.onmessage=e,await u("plugin:global-shortcut|register",{shortcuts:Array.isArray(t)?t:[t],handler:s})},t.unregister=async function(t){return await u("plugin:global-shortcut|unregister",{shortcuts:Array.isArray(t)?t:[t]})},t.unregisterAll=async function(){return await u("plugin:global-shortcut|unregister_all",{})},t}({});Object.defineProperty(window.__TAURI__,"globalShortcut",{value:__TAURI_PLUGIN_GLOBAL_SHORTCUT__})}
+1 -1
View File
@@ -24,6 +24,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.8.0"
"@tauri-apps/api": "^2.11.0"
}
}
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_HAPTICS__=function(r){"use strict";async function t(r,t={},e){return window.__TAURI_INTERNALS__.invoke(r,t,e)}var e;"function"==typeof SuppressedError&&SuppressedError,function(r){r.WINDOW_RESIZED="tauri://resize",r.WINDOW_MOVED="tauri://move",r.WINDOW_CLOSE_REQUESTED="tauri://close-requested",r.WINDOW_DESTROYED="tauri://destroyed",r.WINDOW_FOCUS="tauri://focus",r.WINDOW_BLUR="tauri://blur",r.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",r.WINDOW_THEME_CHANGED="tauri://theme-changed",r.WINDOW_CREATED="tauri://window-created",r.WEBVIEW_CREATED="tauri://webview-created",r.DRAG_ENTER="tauri://drag-enter",r.DRAG_OVER="tauri://drag-over",r.DRAG_DROP="tauri://drag-drop",r.DRAG_LEAVE="tauri://drag-leave"}(e||(e={}));const a={async vibrate(r){try{return{status:"ok",data:await t("plugin:haptics|vibrate",{duration:r})}}catch(r){if(r instanceof Error)throw r;return{status:"error",error:r}}},async impactFeedback(r){try{return{status:"ok",data:await t("plugin:haptics|impact_feedback",{style:r})}}catch(r){if(r instanceof Error)throw r;return{status:"error",error:r}}},async notificationFeedback(r){try{return{status:"ok",data:await t("plugin:haptics|notification_feedback",{type:r})}}catch(r){if(r instanceof Error)throw r;return{status:"error",error:r}}},async selectionFeedback(){try{return{status:"ok",data:await t("plugin:haptics|selection_feedback")}}catch(r){if(r instanceof Error)throw r;return{status:"error",error:r}}}},{vibrate:i,impactFeedback:c,notificationFeedback:n,selectionFeedback:o}=a;return r.impactFeedback=c,r.notificationFeedback=n,r.selectionFeedback=o,r.vibrate=i,r}({});Object.defineProperty(window.__TAURI__,"haptics",{value:__TAURI_PLUGIN_HAPTICS__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_HAPTICS__=function(r){"use strict";async function t(r,t={},e){return window.__TAURI_INTERNALS__.invoke(r,t,e)}var e;"function"==typeof SuppressedError&&SuppressedError,function(r){r.WINDOW_RESIZED="tauri://resize",r.WINDOW_MOVED="tauri://move",r.WINDOW_CLOSE_REQUESTED="tauri://close-requested",r.WINDOW_DESTROYED="tauri://destroyed",r.WINDOW_FOCUS="tauri://focus",r.WINDOW_BLUR="tauri://blur",r.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",r.WINDOW_THEME_CHANGED="tauri://theme-changed",r.WINDOW_CREATED="tauri://window-created",r.WINDOW_SUSPENDED="tauri://suspended",r.WINDOW_RESUMED="tauri://resumed",r.WEBVIEW_CREATED="tauri://webview-created",r.DRAG_ENTER="tauri://drag-enter",r.DRAG_OVER="tauri://drag-over",r.DRAG_DROP="tauri://drag-drop",r.DRAG_LEAVE="tauri://drag-leave"}(e||(e={}));const a={async vibrate(r){try{return{status:"ok",data:await t("plugin:haptics|vibrate",{duration:r})}}catch(r){if(r instanceof Error)throw r;return{status:"error",error:r}}},async impactFeedback(r){try{return{status:"ok",data:await t("plugin:haptics|impact_feedback",{style:r})}}catch(r){if(r instanceof Error)throw r;return{status:"error",error:r}}},async notificationFeedback(r){try{return{status:"ok",data:await t("plugin:haptics|notification_feedback",{type:r})}}catch(r){if(r instanceof Error)throw r;return{status:"error",error:r}}},async selectionFeedback(){try{return{status:"ok",data:await t("plugin:haptics|selection_feedback")}}catch(r){if(r instanceof Error)throw r;return{status:"error",error:r}}}},{vibrate:i,impactFeedback:c,notificationFeedback:n,selectionFeedback:o}=a;return r.impactFeedback=c,r.notificationFeedback=n,r.selectionFeedback=o,r.vibrate=i,r}({});Object.defineProperty(window.__TAURI__,"haptics",{value:__TAURI_PLUGIN_HAPTICS__})}
@@ -2,12 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import AudioToolbox
import CoreHaptics
import SwiftRs
import Tauri
import UIKit
import WebKit
import CoreHaptics
import AudioToolbox
class ImpactFeedbackOptions: Decodable {
let style: ImpactFeedbackStyle
@@ -69,19 +69,21 @@ class HapticsPlugin: Plugin {
try engine.start()
engine.resetHandler = { [] in
do {
try engine.start()
try engine.start()
} catch {
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
}
}
// TODO: Make some of this (or all) configurable?
let intensity: CHHapticEventParameter = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
let sharpness: CHHapticEventParameter = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
let intensity: CHHapticEventParameter = CHHapticEventParameter(
parameterID: .hapticIntensity, value: 1.0)
let sharpness: CHHapticEventParameter = CHHapticEventParameter(
parameterID: .hapticSharpness, value: 1.0)
let continuousEvent = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [intensity, sharpness],
relativeTime: 0.0,
duration: args.duration/1000
duration: args.duration / 1000
)
let pattern = try CHHapticPattern(events: [continuousEvent], parameters: [])
let player = try engine.makePlayer(with: pattern)
@@ -94,8 +96,6 @@ class HapticsPlugin: Plugin {
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
}
Logger.error("VIBRATE END")
invoke.resolve()
}
+1 -1
View File
@@ -24,6 +24,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.8.0"
"@tauri-apps/api": "^2.11.0"
}
}

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