Compare commits

...

87 Commits

Author SHA1 Message Date
renovate[bot] 2347843600 chore(deps): update windows-rs and webview2 crates 2026-07-18 14:30:22 +00:00
renovate[bot] 57ac986453 chore(deps): update dependency @tauri-apps/cli to v2.11.4 (#3463)
* chore(deps): update dependency @tauri-apps/cli to v2.11.4

* Use workspace dependencies in example

* Bump tauri

* Leftover

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-07-18 22:28:03 +08:00
Tony 2ed6d6c1de fix(store): StoreOptions.defaults should not be required (#3499) 2026-07-18 19:46:31 +08:00
Tony cdfd462955 chore(example): set color-cheme (#3493)
This allows the unstyled user agent elements (like the checkbox) match the selected theme colors
2026-07-16 21:31:40 +08:00
Tony d6e0b6bbb1 chore(example): clean up and migrate to ts partially (#3492)
* chore(example): clean up and migrate to ts

* Add missing permissions

* Mirror line-height: 1.5 and remove h-*
2026-07-16 19:47:52 +08:00
Tony 40ae0a7fa0 enhance(dialog): use MaterialAlertDialogBuilder (#3491)
* Fix example insets

* Add app-region: drag

* Use `Theme.Material3.DayNight.NoActionBar`

* Re-generate some kotlin files

* Use MaterialAlertDialogBuilder

* Add change file

* Revert back to margin-top: 0.5rem

* Re-generate outdated gradle wrapper from #3039

* Move title bar to its own file

* Fix cancel message
2026-07-16 17:27:19 +08:00
Tony edc52ea056 chore(deps): bump create-pull-request to v8 (#3489) 2026-07-15 23:49:37 +08:00
Tony a0d949d93e chore(deps): update pnpm to v11 (#3487)
* chore(deps): update pnpm to v11

* Remove all `version: 11`

* Bump tsx
2026-07-15 20:24:01 +08:00
Tony 13c63af965 chore: fix clippy (#3488) 2026-07-15 16:01:07 +08:00
github-actions[bot] cad301fcc1 publish new versions (#3447)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-14 01:10:29 +02:00
Bajoca 254f222e0e chore(log): use kv stable feature rather than unstable (#3477) 2026-07-13 13:23:56 +02:00
Tony b1439be667 chore: ignore cargo audit errors (#3481)
* chore: ignore cargo audit errors

* Bump anyhow
2026-07-13 16:51:50 +08:00
Bajoca d157387722 fix(single-instance): Yield unix listener in macos (#3466)
* Use tokio UnixListener so the task can yield and release the thread

* use standard unix listener, convert to non blocking and cast into tokio yielding one

* Create fix-yield-single-instance-macos.md

* Update .changes/fix-yield-single-instance-macos.md

Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com>

* use net feature in Tokio dependency

* Use tokio UnixListener::bind, inside the tokio task

---------

Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com>
2026-06-29 11:00:59 +02:00
Tony 41f6274270 docs(log): some builder methods and defaults (#3460)
* docs(log): some builder methods and defaults

* Too much copy pastes

* Merge branch 'v2' into log-docs
2026-06-17 18:38:28 +08:00
Bajoca f08980f123 feat(log): rotate log file on each session (#3445)
* Update lib.rs

* docstring for file_open_strategy

* Create log-file-open-strategy.md

* Update plugins/log/src/lib.rs

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

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2026-06-17 16:25:24 +08:00
renovate[bot] 78df86f810 chore(deps): update dependency vite to v8.0.16 [security] (#3458)
* chore(deps): update dependency vite to v8.0.16 [security]

* Bump esbuild

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-06-16 11:18:07 +08:00
nandanpugalia aecec42d5d docs(http): fix connectTimeout option in README (#3451) 2026-06-15 10:08:31 +02:00
renovate[bot] 0767dcbd5c chore(deps): update dependency eslint-plugin-security to v4.0.1 (#3452) 2026-06-15 09:59:15 +02:00
Slava Minamoto 4be7690085 fix(positioner): return error instead of panicking when window has no monitor (#3449) 2026-06-11 21:05:20 +08:00
dependabot[bot] c2b3981248 chore(deps): bump tar in /plugins/updater/tests/updater-migration/v1-app (#3448)
Bumps [tar](https://github.com/composefs/tar-rs) from 0.4.45 to 0.4.46.
- [Release notes](https://github.com/composefs/tar-rs/releases)
- [Commits](https://github.com/composefs/tar-rs/compare/0.4.45...0.4.46)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-11 14:44:45 +08:00
beef 0c23b8ecfe remove unused dependency byte-unit from log plugin (#3446)
* remove unused dependency `byte-unit` from log plugin

* Update .changes/rmdep.md

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

* Update .changes/rmdep.md

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

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2026-06-11 14:24:37 +08:00
renovate[bot] 4350ca652d chore(deps): update rust crate tar to v0.4.46 [security] (#3435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-30 22:03:15 +08:00
renovate[bot] 1953f00188 chore(deps): update dependency prettier to v3.8.3 (#3387) 2026-05-28 16:14:25 +02:00
github-actions[bot] 9883f396dc Publish New Versions (v2) (#3425) 2026-05-28 12:06:17 +02:00
zankowitch alexis d8645ab3e5 fix(barcode-scanner): dispatch iOS cancel() cleanup to the main thread (#3393)
Signed-off-by: Alexis Zankowitch <a.zankowitch@reply.de>
2026-05-28 11:46:25 +02:00
renovate[bot] cb863d57c6 chore(deps): update rust crate global-hotkey to 0.8 (#3408)
Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com>
2026-05-27 11:50:02 +02:00
FabianLars d67e4f8cdc chore(deps): update rand 2026-05-27 10:30:18 +02:00
Jsu e571e0be9d docs(fs): typo wether -> whether in commands.rs (#3429) 2026-05-23 21:28:55 +02:00
renovate[bot] 83ac944abf chore(deps): update dependency @tauri-apps/cli to v2.11.2 (#3427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-20 10:07:23 +08:00
dependabot[bot] 0139c54d0e chore(deps): bump openssl (#3428)
Bumps [openssl](https://github.com/rust-openssl/rust-openssl) from 0.10.79 to 0.10.80.
- [Release notes](https://github.com/rust-openssl/rust-openssl/releases)
- [Commits](https://github.com/rust-openssl/rust-openssl/compare/openssl-v0.10.79...openssl-v0.10.80)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 10:06:59 +08:00
renovate[bot] cad34b73fa chore(deps): update dependency eslint to v10.4.0 (#3400)
* chore(deps): update dependency eslint to v10.4.0

* Fix audit

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-05-20 09:58:26 +08:00
Urs de Swardt c0d64bf7d9 fix: replace panics with error returns (#3420)
* refactor: replace panics with error returns

* fix: lints

* chore: add changes md
2026-05-18 21:56:33 +08:00
周诚 e919bab3eb fix(upload): wrong progress callback parameter names in README (#3406)
* Update progress callback parameter names in README

Signed-off-by: zhoucheng <zhoucheng133@outlook.com>

* formatting

Signed-off-by: zhoucheng <zhoucheng133@outlook.com>

* Fixed the misssing braces

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
Signed-off-by: zhoucheng <zhoucheng133@outlook.com>

* docs(readme): fix remaining destructuring syntax in examples

Signed-off-by: zhoucheng <zhoucheng133@outlook.com>

---------

Signed-off-by: zhoucheng <zhoucheng133@outlook.com>
Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2026-05-15 18:14:17 +08:00
renovate[bot] 95c9291cee chore(deps): update dependency svelte to v5.55.7 [security] (#3422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 10:03:55 +08:00
renovate[bot] 114e902537 chore(deps): update dependency rollup to v4.60.3 (#3358)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 15:16:49 +02:00
renovate[bot] 6972acdca6 chore(deps): update dependency @tauri-apps/cli to v2.11.1 (#3417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 14:47:31 +02:00
renovate[bot] 8bbc7a0d1d chore(deps): update rust crate tauri to v2.11.1 [security] (#3413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 21:23:19 +02:00
dependabot[bot] 2d013294ef chore(deps): bump openssl from 0.10.78 to 0.10.79 in /plugins/updater/tests/updater-migration/v1-app (#3412)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 21:20:11 +02:00
github-actions[bot] 5c7668b6bb publish new versions (#3397)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-02 19:37:51 +08:00
renovate[bot] ec0540138b chore(deps): update rust crate toml to v1 (#3323)
* chore(deps): update rust crate toml to v1

* downgrade to 1.0.x in lockfile

* Add change file

* Downgrade indexmap

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-05-01 18:09:41 +08:00
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
209 changed files with 5101 additions and 4370 deletions
+14 -8
View File
@@ -1,11 +1,17 @@
[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",
# rsa Marvin Attack
"RUSTSEC-2023-0071",
# 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",
# quick-xml, need an update in plist, can't be updated in the repo because of MSRV, users are unaffected
# Also needs on update in wayland-scanner > arboard > tauri-plugin-clipboard-manager
"RUSTSEC-2026-0194",
# quick-xml, need an update in plist, can't be updated in the repo because of MSRV, users are unaffected
# Also needs on update in wayland-scanner > arboard > tauri-plugin-clipboard-manager
"RUSTSEC-2026-0195",
# quinn-proto, can't be updated in the repo because of MSRV, users are unaffected
"RUSTSEC-2026-0185",
]
+6
View File
@@ -0,0 +1,6 @@
---
dialog: patch
dialog-js: patch
---
Use `com.google.android.material.dialog.MaterialAlertDialogBuilder` instead of `AlertDialog` so the dialog follows the app's theme
+6
View File
@@ -0,0 +1,6 @@
---
store: patch
store-js: patch
---
Fix `StoreOptions` requires `defaults` field
+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.
+4 -5
View File
@@ -33,20 +33,19 @@ jobs:
audit-js:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Cache pnpm modules
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: 'lts/*'
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
with:
version: 10.x.x
run_install: true
- name: audit
run: pnpm audit
+2 -2
View File
@@ -33,8 +33,8 @@ jobs:
audit-rust:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rustsec/audit-check@v1
- uses: actions/checkout@v7
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
# https://github.com/tauri-apps/plugins-workspace/issues/774
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: check change files end with .md
run: |
+5 -6
View File
@@ -21,7 +21,7 @@ jobs:
outputs:
packages: ${{ steps.filter.outputs.changes }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v2
id: filter
with:
@@ -153,21 +153,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Cache pnpm modules
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: 'lts/*'
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
with:
version: 10.x.x
run_install: true
- name: build api
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v2
id: filter
with:
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0 # required for use of git history
- name: covector status
@@ -28,18 +28,17 @@ jobs:
successfulPublish: ${{ steps.covector.outputs.successfulPublish }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0 # required for use of git history
- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
with:
version: 10.x.x
run_install: true
- name: cargo login
@@ -74,7 +73,7 @@ jobs:
- name: Create Pull Request With Versions Bumped
id: cpr
uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # 7.0.7
uses: peter-evans/create-pull-request@v8.1.1
if: steps.covector.outputs.commandRan == 'version'
with:
title: 'Publish New Versions (${{ github.ref_name }})'
+6 -7
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: install Rust stable and rustfmt
uses: dtolnay/rust-toolchain@stable
@@ -25,20 +25,19 @@ jobs:
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Cache pnpm modules
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: 'lts/*'
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
with:
version: 10.x.x
run_install: true
- run: pnpm format:check
@@ -46,7 +45,7 @@ jobs:
name: taplo (.toml files)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
platform: [ubuntu-22.04, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
+4 -5
View File
@@ -36,20 +36,19 @@ jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Cache pnpm modules
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: 'lts/*'
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
with:
version: 10.x.x
run_install: true
- name: eslint
run: pnpm lint
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
outputs:
packages: ${{ steps.filter.outputs.changes }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v2
id: filter
with:
@@ -136,7 +136,7 @@ jobs:
package: ${{ fromJSON(needs.changes.outputs.packages) }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: install webkit2gtk
run: |
+4 -5
View File
@@ -19,26 +19,25 @@ jobs:
sync-to-mirrors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Fetch git tags
run: git fetch origin 'refs/tags/*:refs/tags/*'
- name: Cache pnpm modules
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: 'lts/*'
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
with:
version: 10.x.x
run_install: true
- name: Build packages
+2 -2
View File
@@ -38,7 +38,7 @@ jobs:
outputs:
packages: ${{ steps.filter.outputs.changes }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v2
id: filter
with:
@@ -225,7 +225,7 @@ jobs:
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: install webkit2gtk
if: contains(matrix.platform.target, 'unknown-linux')
Generated
+526 -627
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -11,7 +11,7 @@ resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tracing = "0.1"
log = "0.4"
log = "0.4.21"
tauri = { version = "2.10", default-features = false }
tauri-build = "2.5"
tauri-plugin = "2.5"
+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. | ✅ | ✅ | ✅ | ❌ | ❌ |
+33
View File
@@ -1,5 +1,38 @@
# Changelog
## \[2.0.41]
### Dependencies
- Upgraded to `log-js@2.9.0`
## \[2.0.40]
### Dependencies
- Upgraded to `barcode-scanner-js@2.4.5`
- Upgraded to `global-shortcut-js@2.3.2`
## \[2.0.39]
### Dependencies
- Upgraded to `fs-js@2.5.1`
- Upgraded to `opener-js@2.5.4`
- Upgraded to `store-js@2.4.3`
- Upgraded to `dialog-js@2.7.1`
- Upgraded to `http-js@2.5.9`
## \[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
+3 -2
View File
@@ -1,12 +1,13 @@
<!doctype html>
<html lang="en" theme="dark">
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=0"
/>
<title>Svelte + Vite App</title>
<meta name="color-scheme" content="dark light" />
<title>API Example App</title>
</head>
<body>
+28 -28
View File
@@ -1,7 +1,7 @@
{
"name": "api",
"private": true,
"version": "2.0.37",
"version": "2.0.41",
"type": "module",
"scripts": {
"dev": "vite --clearScreen false",
@@ -10,36 +10,36 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2.10.1",
"@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.6.0",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-geolocation": "^2.2.0",
"@tauri-apps/plugin-global-shortcut": "^2.3.1",
"@tauri-apps/plugin-haptics": "^2.2.0",
"@tauri-apps/plugin-http": "^2.5.7",
"@tauri-apps/plugin-nfc": "^2.3.4",
"@tauri-apps/plugin-notification": "^2.3.3",
"@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.5",
"@tauri-apps/plugin-store": "^2.4.2",
"@tauri-apps/plugin-updater": "^2.10.0",
"@tauri-apps/plugin-upload": "^2.3.0",
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-barcode-scanner": "workspace:*",
"@tauri-apps/plugin-biometric": "workspace:*",
"@tauri-apps/plugin-cli": "workspace:*",
"@tauri-apps/plugin-clipboard-manager": "workspace:*",
"@tauri-apps/plugin-dialog": "workspace:*",
"@tauri-apps/plugin-fs": "workspace:*",
"@tauri-apps/plugin-geolocation": "workspace:*",
"@tauri-apps/plugin-global-shortcut": "workspace:*",
"@tauri-apps/plugin-haptics": "workspace:*",
"@tauri-apps/plugin-http": "workspace:*",
"@tauri-apps/plugin-nfc": "workspace:*",
"@tauri-apps/plugin-notification": "workspace:*",
"@tauri-apps/plugin-opener": "workspace:*",
"@tauri-apps/plugin-os": "workspace:*",
"@tauri-apps/plugin-process": "workspace:*",
"@tauri-apps/plugin-shell": "workspace:*",
"@tauri-apps/plugin-store": "workspace:*",
"@tauri-apps/plugin-updater": "workspace:*",
"@tauri-apps/plugin-upload": "workspace:*",
"@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.10.0",
"@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.4",
"@unocss/extractor-svelte": "^66.6.7",
"svelte": "^5.54.0",
"unocss": "^66.6.7",
"vite": "^8.0.1"
}
}
+33
View File
@@ -1,5 +1,38 @@
# Changelog
## \[2.0.45]
### Dependencies
- Upgraded to `log@2.9.0`
## \[2.0.44]
### Dependencies
- Upgraded to `barcode-scanner@2.4.5`
- Upgraded to `global-shortcut@2.3.2`
## \[2.0.43]
### Dependencies
- Upgraded to `fs@2.5.1`
- Upgraded to `opener@2.5.4`
- Upgraded to `store@2.4.3`
- Upgraded to `dialog@2.7.1`
- Upgraded to `http@2.5.9`
## \[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
+11 -11
View File
@@ -1,7 +1,7 @@
[package]
name = "api"
publish = false
version = "2.0.41"
version = "2.0.45"
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.8.0" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.4.5", features = [
tauri-plugin-log = { path = "../../../plugins/log", version = "2.9.0" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.5.1", features = [
"watch",
] }
tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.3.2" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.6.0" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.7.1" }
tauri-plugin-http = { path = "../../../plugins/http", features = [
"multipart",
"cookies",
], version = "2.5.7" }
], version = "2.5.9" }
tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.3.3", features = [
"windows7-compat",
] }
tauri-plugin-os = { path = "../../../plugins/os", version = "2.3.2" }
tauri-plugin-process = { path = "../../../plugins/process", version = "2.3.1" }
tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.5.3" }
tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.5.4" }
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.5" }
tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.2" }
tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.3" }
tauri-plugin-upload = { path = "../../../plugins/upload", version = "2.3.0" }
[dependencies.tauri]
@@ -56,13 +56,13 @@ features = [
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
tauri-plugin-cli = { path = "../../../plugins/cli", version = "2.4.1" }
tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", version = "2.3.1" }
tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.10.0" }
tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", version = "2.3.2" }
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.4" }
tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.3.4" }
tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.4.5" }
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" }
@@ -15,19 +15,15 @@
]
},
"core:default",
"core:app:allow-set-app-theme",
"fs:default",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-toggle-maximize",
"core:window:allow-close",
"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": [
@@ -1,3 +1,11 @@
package com.tauri.api
class MainActivity : TauriActivity()
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}
}
@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.api" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="Theme.api" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.api" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="Theme.api" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
@@ -21,7 +21,23 @@ open class BuildTask : DefaultTask() {
runTauriCli(executable)
} catch (e: Exception) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
runTauriCli("$executable.cmd")
// Try different Windows-specific extensions
val fallbacks = listOf(
"$executable.exe",
"$executable.cmd",
"$executable.bat",
)
var lastException: Exception = e
for (fallback in fallbacks) {
try {
runTauriCli(fallback)
return
} catch (fallbackException: Exception) {
lastException = fallbackException
}
}
throw lastException
} else {
throw e;
}
@@ -1,6 +1,6 @@
#Tue May 10 19:22:52 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
+128 -195
View File
@@ -1,7 +1,16 @@
<script>
<script module lang="ts">
export type ViewProps = {
onMessage: (value: unknown) => void
}
</script>
<script lang="ts">
import { onMount, tick } from 'svelte'
import { writable } from 'svelte/store'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { MediaQuery } from 'svelte/reactivity'
import { getCurrentWindow, type Theme } from '@tauri-apps/api/window'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { setTheme } from '@tauri-apps/api/app'
import * as os from '@tauri-apps/plugin-os'
import Welcome from './views/Welcome.svelte'
@@ -23,11 +32,10 @@
import Biometric from './views/Biometric.svelte'
import Geolocation from './views/Geolocation.svelte'
import Haptics from './views/Haptics.svelte'
import { onMount, tick } from 'svelte'
import { ask } from '@tauri-apps/plugin-dialog'
import Nfc from './views/Nfc.svelte'
import TitleBar from './lib/TitleBar.svelte'
const appWindow = getCurrentWindow()
if (appWindow.label !== 'main') {
@@ -148,114 +156,98 @@
component: Haptics,
icon: 'i-ph-vibrate'
}
]
].filter(Boolean) as {
label: string
component: typeof Haptics
icon: string
}[]
let selected = $state.raw(views[0])
let selected = views[0]
function select(view) {
selected = view
}
// dark/light themes
const preferDark = new MediaQuery('prefers-color-scheme: dark')
let theme = $state<Theme | 'auto'>(
(localStorage.getItem('theme') as Theme | null) || 'auto'
)
// Window controls
let isWindowMaximized
onMount(async () => {
isWindowMaximized = await appWindow.isMaximized()
appWindow.onResized(async () => {
isWindowMaximized = await appWindow.isMaximized()
})
})
function minimize() {
appWindow.minimize()
}
async function toggleMaximize() {
;(await appWindow.isMaximized())
? appWindow.unmaximize()
: appWindow.maximize()
}
let confirmed_close = false
async function close() {
if (!confirmed_close) {
confirmed_close = await ask(
'Are you sure that you want to close this window?',
{
title: 'Tauri API'
}
)
if (confirmed_close) {
appWindow.close()
}
async function switchTheme() {
switch (theme) {
case 'dark':
theme = 'light'
break
case 'light':
theme = 'auto'
break
case 'auto':
theme = 'dark'
break
}
applyTheme()
}
// dark/light
let isDark
onMount(() => {
isDark = localStorage && localStorage.getItem('theme') == 'dark'
applyTheme(isDark)
function applyTheme() {
const isDark = theme === 'auto' ? preferDark.current : theme === 'dark'
if (isDark) {
document.documentElement.classList.add('dark')
document.documentElement.classList.remove('light')
} else {
document.documentElement.classList.remove('dark')
document.documentElement.classList.add('light')
}
setTheme(theme === 'auto' ? null : theme)
localStorage.setItem('theme', theme)
}
$effect(() => {
applyTheme()
})
function applyTheme(isDark) {
const html = document.querySelector('html')
isDark ? html.classList.add('dark') : html.classList.remove('dark')
localStorage && localStorage.setItem('theme', isDark ? 'dark' : '')
}
function toggleDark() {
isDark = !isDark
applyTheme(isDark)
}
// Console
let messages = writable([])
let consoleTextEl
async function onMessage(value) {
const messages = writable<string[]>([])
let consoleTextEl: HTMLDivElement
// this function is renders HTML without sanitizing it so it's insecure
// we only use it with our own input data
async function insecureRenderHtml(html: string) {
messages.update((r) => [
...r,
{
html:
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
(typeof value === 'string' ? value : JSON.stringify(value, null, 1)) +
'</pre>'
}
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ${html}</pre>`
])
await tick()
if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight
consoleTextEl.scrollTop = consoleTextEl.scrollHeight
}
// this function renders HTML without sanitizing it so it's insecure
// we only use it with our own input data
async function insecureRenderHtml(html) {
messages.update((r) => [
...r,
{
html:
`<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
html +
'</pre>'
}
])
await tick()
if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight
async function onMessage(value: unknown) {
const valueStr =
typeof value === 'string'
? value
: JSON.stringify(
value instanceof ArrayBuffer
? Array.from(new Uint8Array(value))
: value,
null,
1
)
insecureRenderHtml(valueStr)
}
function clear() {
messages.update(() => [])
}
let consoleEl, consoleH, cStartY
let minConsoleHeight = 50
function startResizingConsole(e) {
let consoleEl: HTMLDivElement
let consoleH = 0
let cStartY = 0
const minConsoleHeight = 50
function startResizingConsole(e: MouseEvent) {
cStartY = e.clientY
const styles = window.getComputedStyle(consoleEl)
consoleH = parseInt(styles.height, 10)
const moveHandler = (e) => {
const moveHandler = (e: MouseEvent) => {
const dy = e.clientY - cStartY
const newH = consoleH - dy
consoleEl.style.height = `${
newH < minConsoleHeight ? minConsoleHeight : newH
}px`
consoleEl.style.height = `${newH < minConsoleHeight ? minConsoleHeight : newH}px`
}
const upHandler = () => {
document.removeEventListener('mouseup', upHandler)
@@ -265,21 +257,19 @@
document.addEventListener('mousemove', moveHandler)
}
let isWindows
onMount(async () => {
isWindows = (await os.platform()) === 'windows'
})
let isWindows = os.platform() === 'windows'
// mobile
let isSideBarOpen = false
let sidebar
let sidebarToggle
let isSideBarOpen = $state(false)
let sidebar: HTMLElement
let sidebarToggle: HTMLElement
let isDraggingSideBar = false
let draggingStartPosX = 0
let draggingEndPosX = 0
const clamp = (min, num, max) => Math.min(Math.max(num, min), max)
const clamp = (min: number, num: number, max: number) =>
Math.min(Math.max(num, min), max)
function toggleSidebar(sidebar, isSideBarOpen) {
function toggleSidebar() {
sidebar.style.setProperty(
'--translate-x',
`${isSideBarOpen ? '0' : '-18.75'}rem`
@@ -287,10 +277,9 @@
}
onMount(() => {
sidebar = document.querySelector('#sidebar')
sidebarToggle = document.querySelector('#sidebarToggle')
document.addEventListener('click', (e) => {
if (!(e.target instanceof Node)) return
if (sidebarToggle.contains(e.target)) {
isSideBarOpen = !isSideBarOpen
} else if (isSideBarOpen && !sidebar.contains(e.target)) {
@@ -299,6 +288,7 @@
})
document.addEventListener('touchstart', (e) => {
if (!(e.target instanceof Node)) return
if (sidebarToggle.contains(e.target)) return
const x = e.touches[0].clientX
@@ -330,76 +320,22 @@
})
})
$: {
const sidebar = document.querySelector('#sidebar')
if (sidebar) {
toggleSidebar(sidebar, isSideBarOpen)
}
}
$effect(() => {
toggleSidebar()
})
</script>
<!-- custom titlebar for Windows -->
{#if isWindows}
<div
class="w-screen select-none h-8 pl-2 flex justify-between items-center absolute text-primaryText dark:text-darkPrimaryText"
data-tauri-drag-region
>
<span class="lt-sm:pl-10 text-darkPrimaryText">Tauri API Validation</span>
<span
class="
h-100%
children:h-100% children:w-12 children:inline-flex
children:items-center children:justify-center"
>
<button
aria-label="Toggle dark mode"
title={isDark ? 'Switch to Light mode' : 'Switch to Dark mode'}
class="bg-inherit border-none hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
on:click={toggleDark}
>
{#if isDark}
<div class="i-ph-sun"></div>
{:else}
<div class="i-ph-moon"></div>
{/if}
</button>
<button
aria-label="Minimize window"
title="Minimize"
class="bg-inherit border-none hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
on:click={minimize}
>
<div class="i-codicon-chrome-minimize"></div>
</button>
<button
aria-label="Maximize window"
title={isWindowMaximized ? 'Restore' : 'Maximize'}
class="bg-inherit border-none hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
on:click={toggleMaximize}
>
{#if isWindowMaximized}
<div class="i-codicon-chrome-restore"></div>
{:else}
<div class="i-codicon-chrome-maximize"></div>
{/if}
</button>
<button
aria-label="Close window"
title="Close"
class="bg-inherit border-none hover:bg-red-700 dark:hover:bg-red-700 hover:text-darkPrimaryText active:bg-red-700/90 dark:active:bg-red-700/90 active:text-darkPrimaryText"
on:click={close}
>
<div class="i-codicon-chrome-close"></div>
</button>
</span>
</div>
<TitleBar {appWindow} {theme} {switchTheme} />
{/if}
<!-- Sidebar toggle, only visible on small screens -->
<div
id="sidebarToggle"
class="z-2000 sidebar-toggle hidden lt-sm:flex justify-center absolute items-center w-8 h-8 rd-8
bg-accent dark:bg-darkAccent active:bg-accentDark dark:active:bg-darkAccentDark"
bind:this={sidebarToggle}
class="z-2000 hidden lt-sm:flex justify-center absolute items-center w-8 h-8 rd-8
bg-accent dark:bg-darkAccent active:bg-accentDark dark:active:bg-darkAccentDark text-accentText dark:text-darkAccentText"
>
{#if isSideBarOpen}
<span class="i-codicon-close animate-duration-300ms animate-fade-in"></span>
@@ -409,10 +345,11 @@
</div>
<div
class="flex h-screen w-screen overflow-hidden children-pt8 children-pb-2 text-primaryText dark:text-darkPrimaryText"
class="flex h-screen w-screen overflow-hidden text-primaryText dark:text-darkPrimaryText"
>
<aside
id="sidebar"
bind:this={sidebar}
class="lt-sm:h-screen lt-sm:shadow-lg lt-sm:shadow lt-sm:transition-transform lt-sm:absolute lt-sm:z-1999
bg-darkPrimaryLighter transition-colors-250 overflow-hidden grid select-none px-2"
>
@@ -420,13 +357,16 @@
<img class="p-7" src="tauri_logo.png" alt="Tauri logo" />
</a>
{#if !isWindows}
<a href="##" class="nv justify-between h-8" on:click={toggleDark}>
{#if isDark}
Switch to Light mode
<div class="i-ph-sun"></div>
{:else}
<a href="##" class="nv justify-between" onclick={switchTheme}>
{#if theme === 'auto'}
Switch to Dark mode
<div class="i-ph-circle-half-fill"></div>
{:else if theme === 'dark'}
Switch to Light mode
<div class="i-ph-moon"></div>
{:else if theme === 'light'}
Switch to Auto mode
<div class="i-ph-sun"></div>
{/if}
</a>
<br />
@@ -435,7 +375,7 @@
{/if}
<a
class="nv justify-between h-8"
class="nv justify-between"
target="_blank"
href="https://tauri.app/v1/guides/"
>
@@ -443,7 +383,7 @@
<span class="i-codicon-link-external"></span>
</a>
<a
class="nv justify-between h-8"
class="nv justify-between"
target="_blank"
href="https://github.com/tauri-apps/tauri"
>
@@ -451,7 +391,7 @@
<span class="i-codicon-link-external"></span>
</a>
<a
class="nv justify-between h-8"
class="nv justify-between"
target="_blank"
href="https://github.com/tauri-apps/tauri/tree/dev/examples/api"
>
@@ -461,23 +401,19 @@
<br />
<div class="bg-white/5 h-2px"></div>
<br />
<div
class="flex flex-col overflow-y-auto children-h-10 children-flex-none gap-1"
>
<div class="flex flex-col overflow-y-auto children-flex-none gap-1">
{#each views as view}
{#if view}
<a
href="##"
class="nv {selected === view ? 'nv_selected' : ''}"
on:click={() => {
select(view)
isSideBarOpen = false
}}
>
<div class="{view.icon} mr-2"></div>
<p>{view.label}</p></a
>
{/if}
<a
href="##"
class="nv {selected === view ? 'nv_selected' : ''}"
onclick={() => {
selected = view
isSideBarOpen = false
}}
>
<div class="{view.icon} mr-2"></div>
<p>{view.label}</p></a
>
{/each}
</div>
</aside>
@@ -489,11 +425,7 @@
<h1>{selected.label}</h1>
<div class="overflow-y-auto">
<div class="mr-2">
<svelte:component
this={selected.component}
{onMessage}
{insecureRenderHtml}
/>
<selected.component {onMessage} />
</div>
</div>
</div>
@@ -503,20 +435,21 @@
id="console"
class="select-none h-15rem grid grid-rows-[2px_2rem_1fr] gap-1 overflow-hidden"
>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
on:mousedown={startResizingConsole}
role="button"
tabindex="0"
onmousedown={startResizingConsole}
class="bg-black/20 h-2px cursor-ns-resize"
></div>
<div class="flex justify-between items-center px-2">
<p class="font-semibold">Console</p>
<button
aria-label="Clear Console"
class="cursor-pointer h-85% rd-1 p-1 flex justify-center items-center border-none bg-inherit
class="cursor-pointer h-85% rd-1 p-1 flex justify-center items-center border-none
hover:bg-hoverOverlay dark:hover:bg-darkHoverOverlay
active:bg-hoverOverlay/25 dark:active:bg-darkHoverOverlay/25
"
on:click={clear}
onclick={clear}
>
<div class="i-codicon-clear-all"></div>
</button>
@@ -525,8 +458,8 @@
bind:this={consoleTextEl}
class="px-2 overflow-y-auto all:font-mono code-block all:text-xs select-text mr-2"
>
{#each $messages as r}
{@html r.html}
{#each $messages as messageHtml}
{@html messageHtml}
{/each}
</div>
</div>
+32 -9
View File
@@ -1,3 +1,14 @@
:root {
line-height: 1.5;
}
:root.dark {
color-scheme: dark;
}
:root.light {
color-scheme: light;
}
*:not(h1, h2, h3, h4, h5, h6) {
margin: 0;
padding: 0;
@@ -8,6 +19,11 @@
font-family: 'Rubik', sans-serif;
}
button {
color: inherit;
background-color: inherit;
}
::-webkit-scrollbar {
width: 0.25rem;
height: 3px;
@@ -33,27 +49,34 @@ code.code-block {
width: 18.75rem;
}
@media screen and (max-width: 640px) {
@media screen and (width < 640px) {
#sidebar {
--translate-x: -18.75rem;
transform: translateX(var(--translate-x));
}
}
.sidebar-toggle {
margin-top: 0.5rem;
margin-left: 0.5rem;
#sidebar,
main {
padding-top: calc(env(safe-area-inset-top) + 2rem);
}
body {
overflow: hidden;
padding: env(safe-area-inset-top) env(safe-area-inset-right)
env(safe-area-inset-bottom) env(safe-area-inset-left);
#sidebar {
padding-left: calc(env(safe-area-inset-left) + 0.5rem);
}
main {
padding-right: env(safe-area-inset-right);
}
#sidebarToggle {
margin-top: calc(env(safe-area-inset-top) + 0.5rem);
margin-left: calc(env(safe-area-inset-left) + 0.5rem);
}
#sidebar,
#console {
padding-bottom: calc(env(safe-area-inset-bottom) + 24px);
padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem);
}
.transparent {
+103
View File
@@ -0,0 +1,103 @@
<script lang="ts">
import type { Theme, Window } from '@tauri-apps/api/window'
import { ask } from '@tauri-apps/plugin-dialog'
import { onMount } from 'svelte'
let {
appWindow,
switchTheme,
theme
}: { appWindow: Window; switchTheme: () => void; theme: Theme | 'auto' } =
$props()
// Window controls
let isWindowMaximized = $state(false)
onMount(async () => {
isWindowMaximized = await appWindow.isMaximized()
appWindow.onResized(async () => {
isWindowMaximized = await appWindow.isMaximized()
})
})
async function minimize() {
await appWindow.minimize()
}
async function toggleMaximize() {
await appWindow.toggleMaximize()
}
let confirmedClose = false
async function close() {
if (!confirmedClose) {
confirmedClose = await ask(
'Are you sure that you want to close this window?',
{
title: 'Tauri API'
}
)
if (confirmedClose) {
appWindow.close()
}
}
}
</script>
<div
class="w-screen select-none h-8 flex justify-between items-center absolute text-primaryText dark:text-darkPrimaryText"
data-tauri-drag-region
>
<span
class="h-100% pl-2 flex-1 flex items-center lt-sm:pl-10 lt-lg:text-darkPrimaryText [app-region:drag]"
>Tauri API Validation</span
>
<span
class="
h-100%
children:h-100% children:w-12 children:inline-flex
children:items-center children:justify-center"
>
<button
aria-label="Toggle dark mode"
title={theme ? 'Switch to Light mode' : 'Switch to Dark mode'}
class="border-none hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
onclick={switchTheme}
>
{#if theme === 'auto'}
<div title="Switch to Dark mode" class="i-ph-circle-half-fill"></div>
{:else if theme === 'dark'}
<div title="Switch to Light mode" class="i-ph-moon"></div>
{:else if theme === 'light'}
<div title="Switch to Auto mode" class="i-ph-sun"></div>
{/if}
</button>
<button
aria-label="Minimize window"
title="Minimize"
class="border-none hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
onclick={minimize}
>
<div class="i-codicon-chrome-minimize"></div>
</button>
<button
aria-label="Maximize window"
title={isWindowMaximized ? 'Restore' : 'Maximize'}
class="border-none hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
onclick={toggleMaximize}
>
{#if isWindowMaximized}
<div class="i-codicon-chrome-restore"></div>
{:else}
<div class="i-codicon-chrome-maximize"></div>
{/if}
</button>
<button
aria-label="Close window"
title="Close"
class="border-none hover:bg-red-700 dark:hover:bg-red-700 hover:text-darkPrimaryText active:bg-red-700/90 dark:active:bg-red-700/90 active:text-darkPrimaryText"
onclick={close}
>
<div class="i-codicon-chrome-close"></div>
</button>
</span>
</div>
+1 -2
View File
@@ -5,7 +5,6 @@
import { readFile } from '@tauri-apps/plugin-fs'
export let onMessage
export let insecureRenderHtml
let text = 'clipboard message'
function writeText() {
@@ -41,7 +40,7 @@
const image = await clipboard.readImage()
arrayBufferToBase64(await image.rgba(), function (base64) {
const src = 'data:image/png;base64,' + base64
insecureRenderHtml('<img src="' + src + '"></img>')
onMessage('<img src="' + src + '"></img>')
})
return
} catch (_) {}
+87 -77
View File
@@ -1,129 +1,138 @@
<script>
import { open, save, confirm, message } from "@tauri-apps/plugin-dialog";
import { readFile } from "@tauri-apps/plugin-fs";
<script lang="ts">
import {
open,
save,
confirm,
message,
type PickerMode,
type FileAccessMode
} from '@tauri-apps/plugin-dialog'
import { readFile } from '@tauri-apps/plugin-fs'
import type { ViewProps } from '../App.svelte'
export let onMessage;
export let insecureRenderHtml;
let defaultPath = null;
let filter = null;
let multiple = false;
let directory = false;
let pickerMode = "document";
let fileAccessMode = "scoped";
let { onMessage }: ViewProps = $props()
function arrayBufferToBase64(buffer, callback) {
let defaultPath: string | undefined = $state()
let filter = $state('')
let multiple = $state(false)
let directory = $state(false)
let pickerMode: PickerMode = $state('document')
let fileAccessMode: FileAccessMode = $state('scoped')
function arrayBufferToBase64(
buffer: ArrayBuffer,
callback: (base64: string) => void
) {
var blob = new Blob([buffer], {
type: "application/octet-binary",
});
var reader = new FileReader();
type: 'application/octet-binary'
})
var reader = new FileReader()
reader.onload = function (evt) {
var dataurl = evt.target.result;
callback(dataurl.substr(dataurl.indexOf(",") + 1));
};
reader.readAsDataURL(blob);
var dataurl = evt.target!.result as string
callback(dataurl.split(',')[1])
}
reader.readAsDataURL(blob)
}
async function prompt() {
confirm("Do you want to do something?")
.then((res) => onMessage(res ? "Yes" : "No"))
.catch(onMessage);
confirm('Do you want to do something?')
.then((res) => onMessage(res ? 'Yes' : 'No'))
.catch(onMessage)
}
async function promptCustom() {
confirm("Is Tauri awesome?", {
okLabel: "Absolutely",
cancelLabel: "Totally",
confirm('Is Tauri awesome?', {
okLabel: 'Absolutely',
cancelLabel: 'Totally'
})
.then((res) =>
onMessage(
res ? "Tauri is absolutely awesome" : "Tauri is totally awesome"
res ? 'Tauri is absolutely awesome' : 'Tauri is totally awesome'
)
)
.catch(onMessage);
.catch(onMessage)
}
async function msg() {
await message("Tauri is awesome!");
await message('Tauri is awesome!').then((res) => onMessage(res))
}
async function msgCustom(result) {
const buttons = { yes: "awesome", no: "amazing", cancel: "stunning" };
async function msgCustom() {
const buttons = { yes: 'awesome', no: 'amazing', cancel: 'stunning' }
await message(`Tauri is: `, { buttons })
.then((res) => onMessage(`Tauri is ${res}`))
.catch(onMessage);
.catch(onMessage)
}
async function openDialog() {
try {
var result = await open({
title: "My wonderful open dialog",
const result = await open({
title: 'My wonderful open dialog',
defaultPath,
filters: filter
? [
{
name: "Tauri Example",
extensions: filter.split(",").map((f) => f.trim()),
},
name: 'Tauri Example',
extensions: filter.split(',').map((f) => f.trim())
}
]
: [],
multiple,
directory,
pickerMode,
fileAccessMode,
fileAccessMode
})
if (Array.isArray(result)) {
onMessage(result);
onMessage(result)
} else if (result === null) {
onMessage('user cancelled the selection')
} else {
var pathToRead = result;
var isFile = pathToRead.match(/\S+\.\S+$/g);
const pathToRead = result
const 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);
}
await readFile(pathToRead).then(function (res) {
if (isFile) {
if (
pathToRead.includes('.png')
|| pathToRead.includes('.jpg')
|| pathToRead.includes('.jpeg')
) {
arrayBufferToBase64(res.buffer, function (base64) {
const src = 'data:image/png;base64,' + base64
onMessage('<img src="' + src + '"></img>')
})
} else {
onMessage(res);
// Convert byte array to UTF-8 string
const decoder = new TextDecoder('utf-8')
const text = decoder.decode(res)
onMessage(text)
}
} else {
onMessage(res)
}
})
}
} catch(exception) {
} catch (exception) {
onMessage(exception)
}
}
function saveDialog() {
save({
title: "My wonderful save dialog",
title: 'My wonderful save dialog',
defaultPath,
filters: filter
? [
{
name: "Tauri Example",
extensions: filter.split(",").map((f) => f.trim()),
},
name: 'Tauri Example',
extensions: filter.split(',').map((f) => f.trim())
}
]
: [],
})
: []
})
.then(onMessage)
.catch(onMessage);
.catch(onMessage)
}
</script>
@@ -171,15 +180,16 @@
<br />
<div class="flex flex-wrap flex-col md:flex-row gap-2 children:flex-shrink-0">
<button class="btn" id="open-dialog" on:click={openDialog}>Open dialog</button>
<button class="btn" id="save-dialog" on:click={saveDialog}
<button class="btn" id="open-dialog" onclick={openDialog}>Open dialog</button>
<button class="btn" id="save-dialog" onclick={saveDialog}
>Open save dialog</button
>
<button class="btn" id="prompt-dialog" on:click={prompt}>Prompt</button>
<button class="btn" id="custom-prompt-dialog" on:click={promptCustom}
<button class="btn" id="prompt-dialog" onclick={prompt}>Prompt</button>
<button class="btn" id="custom-prompt-dialog" onclick={promptCustom}
>Prompt (custom)</button
>
<button class="btn" id="message-dialog" on:click={msg}>Message</button>
<button class="btn" id="message-dialog" on:click={msgCustom}>Message (custom)</button>
<button class="btn" id="message-dialog" onclick={msg}>Message</button>
<button class="btn" id="message-dialog" onclick={msgCustom}
>Message (custom)</button
>
</div>
+3 -3
View File
@@ -5,7 +5,7 @@
import { arrayBufferToBase64 } from '../lib/utils'
import { onDestroy, onMount } from 'svelte'
const { onMessage, insecureRenderHtml } = $props()
const { onMessage } = $props()
let path = $state('')
let img
@@ -118,12 +118,12 @@
new Uint8Array(response),
function (base64) {
const src = 'data:image/png;base64,' + base64
insecureRenderHtml('<img src="' + src + '"></img>')
onMessage('<img src="' + src + '"></img>')
}
)
} else {
const value = String.fromCharCode.apply(null, response)
insecureRenderHtml(
onMessage(
'<textarea id="file-response"></textarea><button id="file-save">Save</button>'
)
setTimeout(() => {
+9 -21
View File
@@ -11,31 +11,19 @@
"example:api:dev": "pnpm run --filter \"api\" tauri dev"
},
"devDependencies": {
"@eslint/js": "9.39.2",
"@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.39.2",
"eslint": "10.4.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-security": "3.0.1",
"prettier": "3.8.1",
"rollup": "4.57.1",
"eslint-plugin-security": "4.0.1",
"prettier": "3.8.3",
"rollup": "4.60.3",
"tslib": "2.8.1",
"typescript": "5.9.3",
"typescript-eslint": "8.54.0"
"typescript": "6.0.3",
"typescript-eslint": "8.58.2"
},
"minimumReleaseAge": 4320,
"pnpm": {
"overrides": {
"esbuild@<0.25.0": ">=0.25.0",
"lodash@>=4.0.0 <=4.17.22": ">=4.17.23"
},
"onlyBuiltDependencies": [
"esbuild"
]
},
"engines": {
"pnpm": "^10.16.0"
}
"packageManager": "pnpm@11.13.0"
}
+1 -1
View File
@@ -24,6 +24,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.10.1"
"@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"]
}
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## \[2.4.5]
- [`d8645ab3`](https://github.com/tauri-apps/plugins-workspace/commit/d8645ab3e5b508456681eb53275c0837db25aeee) ([#3393](https://github.com/tauri-apps/plugins-workspace/pull/3393) by [@AlexisZankowitch](https://github.com/tauri-apps/plugins-workspace/../../AlexisZankowitch)) Fixed a crash on iOS when `cancel()` is invoked by running the cancel handler on the main thread.
## \[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.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-barcode-scanner"
version = "2.4.4"
version = "2.4.5"
description = "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS"
edition = { workspace = true }
authors = { workspace = true }
+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__})}
@@ -337,10 +337,11 @@ class BarcodeScannerPlugin: Plugin, AVCaptureMetadataOutputObjectsDelegate {
}
@objc private func cancel(_ invoke: Invoke) {
self.invoke?.reject("cancelled")
destroy()
invoke.resolve()
DispatchQueue.main.async { [self] in
self.invoke?.reject("cancelled")
self.destroy()
invoke.resolve()
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-barcode-scanner",
"version": "2.4.4",
"version": "2.4.5",
"description": "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS",
"license": "MIT OR Apache-2.0",
"authors": [
@@ -25,6 +25,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.10.1"
"@tauri-apps/api": "^2.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.10.1"
"@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.10.1"
"@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.10.1"
"@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"]
}
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## \[2.4.9]
- [`e6cdc9f5`](https://github.com/tauri-apps/plugins-workspace/commit/e6cdc9f52e2cd975b11b8e4c12879d597f1f76c3) ([#3396](https://github.com/tauri-apps/plugins-workspace/pull/3396) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix broken iOS custom URL schemes
## \[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.
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-deep-link"
version = "2.4.7"
version = "2.4.9"
description = "Set your Tauri application as the default handler for an URL"
authors = { workspace = true }
license = { workspace = true }
@@ -39,8 +39,8 @@ url = { workspace = true }
[target."cfg(windows)".dependencies]
dunce = "1"
windows-registry = "0.5"
windows-result = "0.3"
windows-registry = "0.6"
windows-result = "0.4"
[target."cfg(target_os = \"linux\")".dependencies]
rust-ini = "0.21"
@@ -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__})}
+3 -1
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,7 +139,7 @@ fn main() {
let deep_link_domains = config
.mobile
.iter()
.filter(|domain| domain.is_app_link())
.filter(|domain| !domain.is_app_link())
.collect::<Vec<_>>();
if deep_link_domains.is_empty() {
@@ -1,5 +1,17 @@
# Changelog
## \[2.2.12]
### Dependencies
- Upgraded to `deep-link-js@2.4.9`
## \[2.2.11]
### Dependencies
- Upgraded to `deep-link-js@2.4.8`
## \[2.2.10]
### Dependencies
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "deep-link-example",
"private": true,
"version": "2.2.10",
"version": "2.2.12",
"type": "module",
"scripts": {
"dev": "vite",
@@ -10,12 +10,12 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-deep-link": "2.4.7"
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-deep-link": "workspace:*"
},
"devDependencies": {
"@tauri-apps/cli": "2.10.0",
"typescript": "^5.7.3",
"vite": "^7.3.1"
"@tauri-apps/cli": "2.11.4",
"typescript": "^6.0.0",
"vite": "^8.0.1"
}
}
@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.deep_link_example" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="Theme.deep_link_example" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.deep_link_example" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="Theme.deep_link_example" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-deep-link",
"version": "2.4.7",
"version": "2.4.9",
"description": "Set your Tauri application as the default handler for an URL",
"license": "MIT OR Apache-2.0",
"authors": [
@@ -25,9 +25,9 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.10.1"
"@tauri-apps/api": "^2.11.0"
},
"devDependencies": {
"@tauri-apps/cli": "2.10.0"
"@tauri-apps/cli": "2.11.4"
}
}
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## \[2.7.1]
### Dependencies
- Upgraded to `fs-js@2.5.1`
## \[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 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-dialog"
version = "2.6.0"
version = "2.7.1"
description = "Native system dialogs for opening and saving files along with message dialogs on your Tauri application."
edition = { workspace = true }
authors = { workspace = true }
@@ -37,7 +37,7 @@ tauri = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
tauri-plugin-fs = { path = "../fs", version = "2.4.5" }
tauri-plugin-fs = { path = "../fs", version = "2.5.1" }
[target.'cfg(target_os = "ios")'.dependencies]
tauri = { workspace = true, features = ["wry"] }
+2 -2
View File
@@ -35,8 +35,8 @@ android {
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("com.google.android.material:material:1.13.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
@@ -5,7 +5,6 @@
package app.tauri.dialog
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.net.Uri
import android.os.Handler
@@ -21,6 +20,7 @@ import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@InvokeArg
class Filter {
@@ -158,7 +158,7 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
Handler(Looper.getMainLooper())
.post {
val builder = AlertDialog.Builder(activity)
val builder = MaterialAlertDialogBuilder(activity)
if (args.title != null) {
builder.setTitle(args.title)
@@ -191,8 +191,7 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
}
}
val dialog = builder.create()
dialog.show()
builder.show()
}
}
+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)
+44 -22
View File
@@ -405,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
@@ -427,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';
@@ -458,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';
@@ -488,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 {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-dialog",
"version": "2.6.0",
"version": "2.7.1",
"license": "MIT OR Apache-2.0",
"authors": [
"Tauri Programme within The Commons Conservancy"
@@ -24,6 +24,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.10.1"
"@tauri-apps/api": "^2.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`"
}
]
}
+11 -90
View File
@@ -9,9 +9,8 @@ use tauri::{command, Manager, Runtime, State, Window};
use tauri_plugin_fs::FsExt;
use crate::{
Dialog, FileAccessMode, FileDialogBuilder, FilePath, MessageDialogBuilder,
MessageDialogButtons, MessageDialogKind, MessageDialogResult, PickerMode, Result, CANCEL, NO,
OK, YES,
Dialog, FileAccessMode, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind,
MessageDialogResult, PickerMode, Result,
};
#[derive(Serialize)]
@@ -259,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);
@@ -284,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())
}
+5 -4
View File
@@ -61,8 +61,11 @@ pub enum FileAccessMode {
}
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 {
@@ -197,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)]
@@ -246,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,
}
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "guest-js"
},
"include": ["guest-js/*.ts"]
}
+10
View File
@@ -1,5 +1,15 @@
# Changelog
## \[2.5.1]
- [`ec054013`](https://github.com/tauri-apps/plugins-workspace/commit/ec0540138bece081e9a87982091947360e61987a) ([#3323](https://github.com/tauri-apps/plugins-workspace/pull/3323) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated dependency `toml` from 0.9 to 1
## \[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.
+7 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-fs"
version = "2.4.5"
version = "2.5.1"
description = "Access the file system."
authors = { workspace = true }
license = { workspace = true }
@@ -20,8 +20,8 @@ ios = { level = "partial", notes = "Access is restricted to Application folder b
tauri-plugin = { workspace = true, features = ["build"] }
schemars = { workspace = true }
serde = { workspace = true }
toml = "0.9"
tauri-utils = { workspace = true, features = ["build"] }
toml = "1.0"
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 -8
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,9 +858,7 @@ async function readTextFileLines(
return { value: null, done }
}
const line = new TextDecoder().decode(
bytes.slice(0, bytes.byteLength - 1)
)
const line = decoder.decode(bytes.slice(0, bytes.byteLength - 1))
return {
value: line,
@@ -1348,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,
@@ -1396,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.5",
"version": "2.5.1",
"description": "Access the file system.",
"license": "MIT OR Apache-2.0",
"authors": [
@@ -25,6 +25,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.10.1"
"@tauri-apps/api": "^2.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!()
}
}
}
+592 -78
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>,
@@ -452,7 +707,7 @@ pub async fn read_text_file_lines_next<R: Runtime>(
let lines = resource_table.get::<StdLinesResource>(rid)?;
let ret = StdLinesResource::with_lock(&lines, |lines| -> CommandResult<Vec<u8>> {
// This is an optimization to include wether we finished iteration or not (1 or 0)
// This is an optimization to include whether we finished iteration or not (1 or 0)
// at the end of returned vector so we can use `tauri::ipc::Response`
// and avoid serialization overhead of separate values.
match lines.next() {
@@ -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"]
}
+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.10.1"
"@tauri-apps/api": "^2.11.0"
}
}

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